web-dev-qa-db-fra.com

Quand appelle-t-on un destructeur C++?

Question de base: quand un programme appelle-t-il la méthode de destruction d'une classe en C++? On m'a dit qu'il est appelé chaque fois qu'un objet sort de la portée ou est soumis à un delete

Des questions plus spécifiques:

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

2) Suite à la question 1, qu'est-ce qui définit le moment où un objet sort du domaine (ne concerne pas 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 liée?

3) Voulez-vous un jour appeler un destructeur manuellement?

90
Pat Murray

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

Cela dépend du type de pointeur. 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 quand un pointeur est fait pour 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 cette intelligence. Ils ne possèdent qu'une adresse et vous permettent d'effectuer des opérations sur les objets vers lesquels ils pointent en le faisant spécifiquement.

2) Suite à la question 1, qu'est-ce qui définit le moment où un objet sort du domaine (ne concerne pas 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 liée?

C'est à la mise en œuvre de la liste liée. Les collections typiques détruisent tous les objets qu’ils contiennent lorsqu’ils sont détruits.

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

Une liste chaînée de pointeurs intelligents pourrait supprimer automatiquement les objets lorsque ceux-ci sont supprimés, ou encore s'ils ne contenaient plus de références. C'est à vous de choisir les pièces qui font ce que vous voulez.

3) Voulez-vous un jour 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 pour le réallouer. Vous pouvez détruire l'ancien objet en place et en construire un nouveau. (Cependant, généralement c'est 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");
}
59
David Schwartz

D'autres ont déjà abordé les autres problèmes. Je vais donc examiner un point: voulez-vous jamais supprimer manuellement un objet?.

La réponse est oui. @DavidSchwartz a donné un exemple, mais c'est un assez insolite. Je donnerai un exemple qui se situe sous le capot de ce que beaucoup de programmeurs C++ utilisent tout le temps: std::vector (et std::deque, même s’il n’est pas aussi souvent utilisé).

Comme la plupart des gens le savent, std::vector allouera un bloc de mémoire plus important lorsque/si vous ajoutez plus d'éléments que l'allocation actuelle ne peut en contenir. Dans ce cas, cependant, il dispose d'un bloc de mémoire capable de contenir plus objets par rapport au vecteur actuel.

Pour gérer cela, ce que vector fait sous les couvertures est allocate raw memory via l'objet Allocator (ce qui, sauf indication contraire, signifie qu'il utilise ::operator new). Ensuite, lorsque vous utilisez (par exemple) Push_back pour ajouter un élément à la vector, le vecteur utilise en interne un placement new pour créer un élément dans la partie (précédemment) inutilisée de son espace mémoire.

Maintenant, que se passe-t-il lorsque/si vous erase un é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 aucun autre, ou libérer aucun des blocs de mémoire qu'il contrôle (par exemple, si vous erase 5 éléments d'un vecteur, puis immédiatement Push_back 5 autres éléments, il est garanti que le vecteur pas réallouera la mémoire quand vous le ferez.

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

Si, par hasard, quelqu'un d'autre devait écrire un conteneur en utilisant un stockage contigu, plus ou moins comme un vector (ou une variante de celui-ci, comme le fait std::deque), vous voudriez certainement utiliser la même technique.

Par exemple, examinons 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;
    }

    // release the buffer:
~circular_buffer() { operator delete(data); }
};

#endif

Contrairement aux conteneurs standard, ceci utilise directement operator new et operator delete. Pour une utilisation réelle, vous voudrez probablement utiliser une classe allocateur, mais pour le moment, cela ferait plus de distraire que de contribuer (de toute façon, l’OMI).

13
Jerry Coffin
  1. Lorsque vous créez un objet avec new, vous devez appeler delete. Lorsque vous créez un objet avec make_shared, le shared_ptr résultant est chargé de conserver le nombre et d'appeler delete lorsque le nombre d'utilisations passe à zéro.
  2. Sortir de la portée signifie laisser un bloc. C'est à ce moment que le destructeur est appelé, en supposant que l'objet était pas 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 le moment où vous affectez l’objet avec un placement new .
5
dasblinkenlight

Pour donner une réponse détaillée à la question 3: oui, il existe de rares occasions d'appeler explicitement le destructeur, en particulier en tant que contrepartie d'un nouveau placement, comme le fait observer dasblinkenlight.

Pour 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 choses est de découpler l'allocation de mémoire de la construction d'objet.

3
Stuart Golodetz

1) Les objets ne sont pas créés "via des pointeurs". Un pointeur est assigné à tout objet que vous avez "nouveau". En supposant que ce soit ce que vous voulez dire, si vous appelez 'delete' sur le pointeur, il supprimera (et appellera le destructeur) l'objet que le pointeur déréférencera. Si vous attribuez le pointeur à un autre objet, il se produira une fuite de mémoire. rien en C++ ne collectera vos ordures pour vous.

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

3) pas vraiment. Il se peut que Deep Magic suggère le contraire, mais vous souhaitez généralement associer vos «nouveaux» mots-clés à vos mots-clés «supprimer» et mettre tout dans votre destructeur nécessaire pour vous assurer qu'il se nettoie correctement. Si vous ne le faites pas, veillez à commenter le destructeur avec des instructions spécifiques à l'intention des utilisateurs de la classe sur la manière de nettoyer manuellement les ressources de cet objet.

3
Nathaniel Ford
  1. Pointeurs - Les pointeurs normaux ne prennent pas en charge RAII. Sans une delete explicite, il y aura des déchets. Heureusement, C++ a des pointeurs automatiques qui gèrent cela pour vous!

  2. Scope - Pensez au moment où une variable devient invisible votre programme. C'est généralement à la fin de {block}, comme vous le signalez.

  3. Destruction manuelle - Ne tentez jamais ceci. Laissez juste la portée et RAII faire la magie pour vous.

2
chrisaycock

Chaque fois que vous utilisez "nouveau", c’est-à-dire que vous associez une adresse à un pointeur ou que vous réclamez 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 liée est appelé, le destructeur de l'objet est appelé. Mais s’il s’agit de pointeurs, vous devez les supprimer manuellement . 3.quand l’espace est réclamé par "nouveau".

1
cloudygoose

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 contenant l'objet est terminée. Par exemple:

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


le destructeur est appelé lorsque le code est exécuté à 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é de manière explicite par l'utilisateur ou nouvelle adresse avant de la supprimer, la fuite de mémoire est survenue. C'est un bug.

Dans une liste chaînée, si nous utilisons std :: list <>, nous ne devons pas nous soucier du destructeur 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 devrions écrire le descripteur et supprimer le pointeur de manière explicite. Sinon, cela provoquera 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!

0
wyx

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

0
Sunny Khandare

Oui, un destructeur (a.k.a. dtor) est appelé lorsqu'un objet sort de sa portée s'il se trouve sur la pile ou lorsque vous appelez delete sur un pointeur sur un objet.

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

  2. Une bonne implémentation de la liste chaînée appellera le dtor de tous les objets de la liste lorsque la liste est en train d'être détruite (soit parce que vous avez appelé une méthode pour la déstocker ou qu'elle est sortie de sa portée). Ceci dépend de la mise en œuvre.

  3. J'en doute, mais je ne serais pas surpris s'il y a des circonstances étranges.

0
tnecniv