Quand un destructeur C ++ est-il appelé?

118

Question de base: quand un programme appelle-t-il une méthode destructrice de classe en C ++? On m'a dit qu'il est appelé chaque fois qu'un objet sort du champ d'application ou est soumis à undelete

Questions plus spécifiques:

1) Si l'objet est créé via un pointeur et que ce pointeur est ultérieurement supprimé ou donné une nouvelle adresse à pointer, est-ce que l'objet vers lequel il pointait appelle son destructeur (en supposant que rien d'autre ne pointe vers lui)?

2) Suite à la question 1, qu'est-ce qui définit quand un objet sort de la portée (pas en ce qui concerne le moment où un objet quitte un {bloc} donné). Donc, en d'autres termes, quand un destructeur est-il appelé sur un objet dans une liste chaînée?

3) Souhaitez-vous jamais appeler un destructeur manuellement?

Pat Murray
la source
3
Même vos questions spécifiques sont trop larges. «Ce pointeur est supprimé ultérieurement» et «une nouvelle adresse à pointer» sont très différents. Recherchez plus (certaines d'entre elles ont reçu une réponse), puis posez des questions distinctes pour les pièces que vous n'avez pas trouvées.
Matthew Flaschen

Réponses:

74

1) Si l'objet est créé via un pointeur et que ce pointeur est ultérieurement supprimé ou donné une nouvelle adresse à pointer, est-ce que l'objet vers lequel il pointait appelle son destructeur (en supposant que rien d'autre ne pointe vers lui)?

Cela dépend du type de pointeurs. Par exemple, les pointeurs intelligents suppriment souvent leurs objets lorsqu'ils sont supprimés. Les pointeurs ordinaires ne le font pas. La même chose est vraie lorsqu'un pointeur est amené à pointer vers un objet différent. Certains pointeurs intelligents détruiront l'ancien objet ou le détruiront s'il n'a plus de références. Les pointeurs ordinaires n'ont pas une telle intelligence. Ils détiennent simplement une adresse et vous permettent d'effectuer des opérations sur les objets qu'ils pointent en le faisant spécifiquement.

2) Suite à la question 1, qu'est-ce qui définit quand un objet sort de la portée (pas en ce qui concerne le moment où un objet quitte un {bloc} donné). Donc, en d'autres termes, quand un destructeur est-il appelé sur un objet dans une liste chaînée?

Cela dépend de la mise en œuvre de la liste chaînée. Les collections typiques détruisent tous les objets qu'elles contiennent lorsqu'elles sont détruites.

Ainsi, une liste liée de pointeurs détruirait généralement les pointeurs mais pas les objets vers lesquels ils pointent. (Ce qui peut être correct. Il peut s'agir de références par d'autres pointeurs.) Cependant, une liste chaînée spécialement conçue pour contenir des pointeurs peut supprimer les objets lors de sa propre destruction.

Une liste liée de pointeurs intelligents peut supprimer automatiquement les objets lorsque les pointeurs sont supprimés, ou le faire s’ils n’ont plus de références. C'est à vous de choisir les pièces qui font ce que vous voulez.

3) Souhaitez-vous jamais appeler un destructeur manuellement?

Sûr. Un exemple serait si vous voulez remplacer un objet par un autre objet du même type mais ne voulez pas libérer de la mémoire juste pour l'allouer à nouveau. Vous pouvez détruire l'ancien objet sur place et en construire un nouveau sur place. (Cependant, c'est généralement une mauvaise idée.)

// pointer is destroyed because it goes out of scope,
// but not the object it pointed to. memory leak
if (1) {
 Foo *myfoo = new Foo("foo");
}


// pointer is destroyed because it goes out of scope,
// object it points to is deleted. no memory leak
if(1) {
 Foo *myfoo = new Foo("foo");
 delete myfoo;
}

// no memory leak, object goes out of scope
if(1) {
 Foo myfoo("foo");
}
David Schwartz
la source
2
Je pensais que le dernier de vos exemples avait déclaré une fonction? C'est un exemple de "l'analyse la plus vexante". (L'autre point plus trivial est que je suppose que vous vouliez dire new Foo()avec un 'F' majuscule.)
Stuart Golodetz
1
Je pense que ce Foo myfoo("foo")n'est pas l'analyse la plus vexante, mais elle l' char * foo = "foo"; Foo myfoo(foo);est.
Cosine
C'est peut-être une question stupide, mais ne devrait-elle pas delete myFooêtre appelée avant Foo *myFoo = new Foo("foo");? Ou bien vous supprimeriez l'objet nouvellement créé, non?
Matheus Rocha
Il n'y a pas myFooavant la Foo *myFoo = new Foo("foo");ligne. Cette ligne crée une toute nouvelle variable appelée myFoo, ombrageant toute variable existante. Bien que dans ce cas, il n'y en ait pas, car ce qui myFooprécède est dans le champ d'application du if, qui a pris fin.
David Schwartz
1
@galactikuh Un "pointeur intelligent" est quelque chose qui agit comme un pointeur vers un objet mais qui possède également des fonctionnalités qui facilitent la gestion de la durée de vie de cet objet.
David Schwartz
20

D'autres ont déjà résolu les autres problèmes, alors je vais juste regarder un point: voulez-vous jamais supprimer manuellement un objet.

La réponse est oui. @DavidSchwartz a donné un exemple, mais c'est un exemple assez inhabituel. Je vais donner un exemple qui est sous le capot de ce que beaucoup de programmeurs C ++ utilisent tout le temps: std::vector(et std::deque, bien qu'il ne soit pas autant utilisé).

Comme la plupart des gens le savent, std::vectorallouera un plus grand bloc de mémoire lorsque / si vous ajoutez plus d'éléments que son allocation actuelle ne peut en contenir. Quand il le fait, cependant, il dispose d'un bloc de mémoire capable de contenir plus d' objets que ce qui est actuellement dans le vecteur.

Pour gérer cela, ce qui vectorfait sous couvertures est d'allouer de la mémoire brute via l' Allocatorobjet (ce qui, sauf indication contraire, signifie qu'il utilise ::operator new). Ensuite, lorsque vous utilisez (par exemple) push_backpour ajouter un élément à vector, le vecteur utilise en interne a placement newpour créer un élément dans la partie (précédemment) inutilisée de son espace mémoire.

Maintenant, que se passe-t-il quand / si vous eraseun élément du vecteur? Il ne peut pas simplement utiliser delete- cela libérerait tout son bloc de mémoire; il doit détruire un objet dans cette mémoire sans en détruire d'autres, ni libérer aucun des blocs de mémoire qu'il contrôle (par exemple, si vous erase5 éléments d'un vecteur, puis immédiatement push_back5 éléments supplémentaires, il est garanti que le vecteur ne se réallouera pas mémoire quand vous le faites.

Pour ce faire, le vecteur détruit directement les objets de la mémoire en appelant explicitement le destructeur, et non en utilisant delete.

Si, par hasard, quelqu'un d'autre devait écrire un conteneur en utilisant un stockage contigu à peu près comme un vectordo (ou une variante de cela, comme le fait std::dequevraiment), vous voudrez presque certainement utiliser la même technique.

Par exemple, considérons comment vous pourriez écrire du code pour un tampon circulaire circulaire.

#ifndef CBUFFER_H_INC
#define CBUFFER_H_INC

template <class T>
class circular_buffer {
    T *data;
    unsigned read_pos;
    unsigned write_pos;
    unsigned in_use;
    const unsigned capacity;
public:
    circular_buffer(unsigned size) :
        data((T *)operator new(size * sizeof(T))),
        read_pos(0),
        write_pos(0),
        in_use(0),
        capacity(size)
    {}

    void push(T const &t) {
        // ensure there's room in buffer:
        if (in_use == capacity) 
            pop();

        // construct copy of object in-place into buffer
        new(&data[write_pos++]) T(t);
        // keep pointer in bounds.
        write_pos %= capacity;
        ++in_use;
    }

    // return oldest object in queue:
    T front() {
        return data[read_pos];
    }

    // remove oldest object from queue:
    void pop() { 
        // destroy the object:
        data[read_pos++].~T();

        // keep pointer in bounds.
        read_pos %= capacity;
        --in_use;
    }
  
~circular_buffer() {
    // first destroy any content
    while (in_use != 0)
        pop();

    // then release the buffer.
    operator delete(data); 
}

};

#endif

Contrairement aux conteneurs standard, cela utilise operator newet operator deletedirectement. Pour une utilisation réelle, vous souhaitez probablement utiliser une classe d'allocateur, mais pour le moment, cela ferait plus de distraire que de contribuer (IMO, en tout cas).

Jerry Coffin
la source
9
  1. Lorsque vous créez un objet avec new, vous êtes responsable de l'appel delete. Lorsque vous créez un objet avec make_shared, le résultat shared_ptrest chargé de tenir compte et d'appeler deletelorsque le nombre d'utilisation passe à zéro.
  2. Sortir du champ d'application signifie laisser un bloc. C'est à ce moment que le destructeur est appelé, en supposant que l'objet n'a pas été alloué avec new(c'est-à-dire qu'il s'agit d'un objet de pile).
  3. Le seul moment où vous devez appeler explicitement un destructeur est lorsque vous attribuez un emplacement ànew l'objet .
dasblinkenlight
la source
1
Il existe un comptage de références (shared_ptr), mais évidemment pas pour les pointeurs simples.
Pubby
1
@Pubby: Bon point, encourageons les bonnes pratiques. Réponse modifiée.
MSalters
6

1) Les objets ne sont pas créés «via des pointeurs». Un pointeur est affecté à tout objet que vous «nouveau». En supposant que c'est ce que vous voulez dire, si vous appelez 'delete' sur le pointeur, cela supprimera (et appellera le destructeur) l'objet que le pointeur déréférencera. Si vous affectez le pointeur à un autre objet, il y aura une fuite de mémoire; rien en C ++ ne collectera vos déchets pour vous.

2) Ce sont deux questions distinctes. Une variable sort de la portée lorsque le cadre de pile dans lequel elle est déclarée est sorti de la pile. C'est généralement lorsque vous quittez un bloc. Les objets d'un tas ne sont jamais hors de portée, bien que leurs pointeurs sur la pile le puissent. Rien en particulier ne garantit qu'un destructeur d'un objet dans une liste chaînée sera appelé.

3) Pas vraiment. Il peut y avoir Deep Magic qui suggérerait le contraire, mais généralement vous voulez faire correspondre vos "nouveaux" mots-clés avec vos mots-clés "supprimer", et mettre tout ce qui est nécessaire dans votre destructeur pour vous assurer qu'il se nettoie correctement. Si vous ne le faites pas, assurez-vous de commenter le destructeur avec des instructions spécifiques à toute personne utilisant la classe sur la façon dont elle doit nettoyer manuellement les ressources de cet objet.

Nathaniel Ford
la source
3

Pour donner une réponse détaillée à la question 3: oui, il y a de (rares) occasions où vous pourriez appeler explicitement le destructeur, notamment en tant que contrepartie d'un placement nouveau, comme l'observe dasblinkenlight.

Pour en donner un exemple concret:

#include <iostream>
#include <new>

struct Foo
{
    Foo(int i_) : i(i_) {}
    int i;
};

int main()
{
    // Allocate a chunk of memory large enough to hold 5 Foo objects.
    int n = 5;
    char *chunk = static_cast<char*>(::operator new(sizeof(Foo) * n));

    // Use placement new to construct Foo instances at the right places in the chunk.
    for(int i=0; i<n; ++i)
    {
        new (chunk + i*sizeof(Foo)) Foo(i);
    }

    // Output the contents of each Foo instance and use an explicit destructor call to destroy it.
    for(int i=0; i<n; ++i)
    {
        Foo *foo = reinterpret_cast<Foo*>(chunk + i*sizeof(Foo));
        std::cout << foo->i << '\n';
        foo->~Foo();
    }

    // Deallocate the original chunk of memory.
    ::operator delete(chunk);

    return 0;
}

Le but de ce genre de chose est de découpler l'allocation de mémoire de la construction d'objets.

Stuart Golodetz
la source
2
  1. Pointeurs - Les pointeurs réguliers ne prennent pas en charge RAII. Sans explicite delete, il y aura des déchets. Heureusement, C ++ a des pointeurs automatiques qui gèrent cela pour vous!

  2. Portée - Pensez au moment où une variable devient invisible pour votre programme. Habituellement, c'est à la fin {block}, comme vous le faites remarquer.

  3. Destruction manuelle - N'essayez jamais cela. Laissez simplement scope et RAII faire la magie pour vous.

chrisaycock
la source
Remarque: auto_ptr est obsolète, comme le mentionne votre lien.
tnecniv
std::auto_ptrest obsolète en C ++ 11, oui. Si l'OP a réellement C ++ 11, il doit l'utiliser std::unique_ptrpour des propriétaires uniques ou std::shared_ptrpour plusieurs propriétaires comptés par référence.
chrisaycock
«Destruction manuelle - N'essayez jamais cela». Je file très souvent des pointeurs d'objet vers un thread différent en utilisant un appel système que le compilateur ne comprend pas. `` S'appuyer '' sur des pointeurs scope / auto / smart provoquerait un échec catastrophique de mes applications car les objets étaient supprimés par le thread appelant avant qu'ils ne puissent être traités par le thread consommateur. Ce problème affecte les objets et les interfaces à portée limitée et refCounted. Seuls les pointeurs et la suppression explicite feront l'affaire.
Martin James
@MartinJames Pouvez-vous publier un exemple d'appel système que le compilateur ne comprend pas? Et comment implémentez-vous la file d'attente? Non, std::queue<std::shared_ptr>?j'ai trouvé pipe()qu'entre un thread producteur et consommateur rendait la concurrence beaucoup plus facile, si la copie n'est pas trop chère.
chrisaycock
myObject = new myClass (); PostMessage (aHandle, WM_APP, 0, LPPARAM (monObjet));
Martin James
1

Chaque fois que vous utilisez «nouveau», c'est-à-dire que vous attachez une adresse à un pointeur, ou pour dire que vous revendiquez de l'espace sur le tas, vous devez le «supprimer».
1. oui, lorsque vous supprimez quelque chose, le destructeur est appelé.
2.Lorsque le destructeur de la liste chaînée est appelé, le destructeur de ses objets est appelé. Mais s'il s'agit de pointeurs, vous devez les supprimer manuellement. 3.lorsque l'espace est réclamé par "nouveau".

nuageux
la source
0

Oui, un destructeur (aka dtor) est appelé lorsqu'un objet sort de la portée s'il est sur la pile ou lorsque vous appelez deleteun pointeur vers un objet.

  1. Si le pointeur est supprimé via, deletele dtor sera appelé. Si vous réaffectez le pointeur sans appeler d' deleteabord, vous obtiendrez une fuite de mémoire car l'objet existe toujours en mémoire quelque part. Dans ce dernier cas, le dtor n'est pas appelé.

  2. Une bonne implémentation de liste chaînée appellera le dtor de tous les objets de la liste lorsque la liste est détruite (parce que soit vous avez appelé une méthode pour la destituer, soit elle est sortie de sa portée elle-même). Cela dépend de la mise en œuvre.

  3. J'en doute, mais je ne serais pas surpris s'il y avait des circonstances étranges là-bas.

tnecniv
la source
1
"Si vous réaffectez le pointeur sans appeler d'abord delete, vous obtiendrez une fuite de mémoire car l'objet existe toujours en mémoire quelque part.". Pas nécessairement. Il aurait pu être supprimé via un autre pointeur.
Matthew Flaschen
0

Si l'objet n'est pas créé via un pointeur (par exemple, A a1 = A ();), le destructeur est appelé lorsque l'objet est détruit, toujours lorsque la fonction où se trouve l'objet est terminée, par exemple:

void func()
{
...
A a1 = A();
...
}//finish


le destructeur est appelé lorsque le code est exécuté sur la ligne "terminer".

Si l'objet est créé via un pointeur (par exemple, A * a2 = new A ();), le destructeur est appelé lorsque le pointeur est supprimé (delete a2;). Si le point n'est pas supprimé explicitement par l'utilisateur ou s'il reçoit un nouvelle adresse avant de la supprimer, la fuite de mémoire s'est produite. C'est un bug.

Dans une liste chaînée, si nous utilisons std :: list <>, nous n'avons pas besoin de nous soucier du desctructor ou de la fuite de mémoire car std :: list <> a terminé tout cela pour nous. Dans une liste chaînée écrite par nous-mêmes, nous devons écrire le desctructor et supprimer explicitement le pointeur, sinon cela entraînera une fuite de mémoire.

Nous appelons rarement un destructeur manuellement. C'est une fonction assurant le système.

Désolé pour mon mauvais anglais!

wyx
la source
Ce n'est pas vrai que vous ne pouvez pas appeler un destructeur manuellement - vous pouvez (voir le code dans ma réponse, par exemple). Ce qui est vrai, c'est que la grande majorité du temps, vous ne devriez pas :)
Stuart Golodetz
0

Rappelez-vous que le constructeur d'un objet est appelé immédiatement après l'allocation de la mémoire pour cet objet et que le destructeur est appelé juste avant de désallouer la mémoire de cet objet.

Sunny Khandare
la source