web-dev-qa-db-fra.com

Quand devons-nous définir des destructeurs?

J'ai lu que les destructeurs doivent être définis lorsque nous avons des membres pointeurs et lorsque nous définissons une classe de base, mais je ne suis pas sûr de bien comprendre. L'une des choses dont je ne suis pas sûr est de savoir si la définition d'un constructeur par défaut est inutile ou non, car nous avons toujours un constructeur par défaut par défaut. De plus, je ne sais pas si nous devons définir le constructeur par défaut pour implémenter le principe RAII (avons-nous juste besoin de mettre l'allocation des ressources dans un constructeur et de ne définir aucun destructeur?).

class A
{

public:
    ~Account()
    {
        delete [] brandname;
        delete b;

        //do we need to define it?

    };

    something(){} =0; //virtual function (reason #1: base class)

private:
    char *brandname; //c-style string, which is a pointer member (reason #2: has a pointer member)
    B* b; //instance of class B, which is a pointer member (reason #2)
    vector<B*> vec; //what about this?



}

class B: public A
{
    public something()
    {
    cout << "nothing" << endl;
    }

    //in all other cases we don't need to define the destructor, nor declare it?
}
22
user3435009

La règle de trois et la règle de zéro

La bonne façon de gérer les ressources était avec la Règle de trois (maintenant la Règle de cinq en raison du déplacement sémantique), mais récemment une autre règle prend le relais: la Règle de zéro =.

L'idée, mais vous devriez vraiment lire l'article, est que la gestion des ressources devrait être laissée à d'autres classes spécifiques.

À cet égard, la bibliothèque standard fournit un bel ensemble d'outils tels que: std::vector, std::string, std::unique_ptr et std::shared_ptr, supprimant efficacement le besoin de destructeurs personnalisés, de constructeurs de déplacement/copie, d'affectation de déplacement/copie et de constructeurs par défaut.

Comment l'appliquer à votre code

Dans votre code, vous disposez de nombreuses ressources différentes, ce qui en fait un excellent exemple.

La chaîne

Si vous remarquez que brandname est effectivement une "chaîne dynamique", la bibliothèque standard vous enregistre non seulement de la chaîne de style C, mais gère automatiquement la mémoire de la chaîne avec std::string .

Le B alloué dynamiquement

La deuxième ressource semble être un B alloué dynamiquement. Si vous allouez dynamiquement pour d'autres raisons que "Je veux un membre facultatif", vous devez absolument utiliser std::unique_ptr qui prendra en charge la ressource (désallocation le cas échéant) automatiquement. D'un autre côté, si vous voulez qu'il soit un membre optionnel vous pouvez utiliser std::optional à la place.

La collection de Bs

La dernière ressource n'est qu'un tableau de Bs. Cela est facilement géré avec un std::vector . La bibliothèque standard vous permet de choisir parmi une variété de conteneurs différents pour vos différents besoins; Pour n'en citer que quelques-uns: std::deque , std::list et std::array .

Conclusion

Pour ajouter toutes les suggestions, vous vous retrouveriez avec:

class A {
private:
    std::string brandname;
    std::unique_ptr<B> b;
    std::vector<B> vec;
public:
    virtual void something(){} = 0;
};

Ce qui est à la fois sûr et lisible.

27
Shoe

Comme le souligne @nonsensickle, les questions sont trop larges ... alors je vais essayer de l'aborder avec tout ce que je sais ...

La première raison de redéfinir le destructeur serait dans La règle des trois qui est en partie le item 6 dans Scott Meyers Efficace C++ mais pas entièrement. La règle des trois stipule que si vous redéfinissez le destructeur, le constructeur de copie ou les opérations d'affectation de copie, cela signifie que vous devez les réécrire tous les trois. La raison en est que si vous deviez réécrire votre propre version, les valeurs par défaut du compilateur ne seront plus valables pour le reste.

Un autre exemple serait celui indiqué par Scott Meyers dans Effective C++

Lorsque vous essayez de supprimer un objet de classe dérivée via un pointeur de classe de base et que la classe de base a un destructeur non virtuel, les résultats ne sont pas définis.

Et puis il continue

Si une classe ne contient aucune fonction virtuelle, cela indique souvent qu'elle n'est pas destinée à être utilisée comme classe de base. Lorsqu'une classe n'est pas destinée à être utilisée comme classe de base, rendre le destructeur virtuel est généralement une mauvaise idée.

Sa conclusion sur les destructeurs pour le virtuel est

L'essentiel est que déclarer gratuitement tous les destructeurs virtuels est tout aussi faux que de ne jamais les déclarer virtuels. En fait, beaucoup de gens résument la situation de cette façon: déclarer un destructeur virtuel dans une classe si et seulement si cette classe contient au moins une fonction virtuelle.

Et si ce n'est pas un cas de la règle des trois, alors peut-être que vous avez un membre pointeur à l'intérieur de votre objet, et peut-être que vous lui avez alloué de la mémoire à l'intérieur de votre objet, alors, vous devez gérer cette mémoire dans le destructeur, c'est le point 6 de son livre

Assurez-vous de vérifier la réponse de @ Jefffrey sur la règle de zéro

11
Claudiordgz

Il y a précisément deux choses qui nécessitent de définir un destructeur:

  1. Lorsque votre objet est détruit, vous devez effectuer une action autre que la destruction de tous les membres de la classe.

    La grande majorité de ces actions libérait autrefois la mémoire, avec le principe RAII, ces actions se sont déplacées dans les destructeurs des conteneurs RAII, que le compilateur se charge d'appeler. Mais ces actions peuvent être n'importe quoi, comme fermer un fichier, ou écrire des données dans un journal, ou .... Si vous suivez strictement le principe RAII, vous écrirez des conteneurs RAII pour toutes ces autres actions, afin que seuls les conteneurs RAII aient des destructeurs définis.

  2. Lorsque vous devez détruire des objets via un pointeur de classe de base.

    Lorsque vous devez le faire, vous devez définir le destructeur comme virtual dans la classe de base. Sinon, vos destructeurs dérivés ne seront pas appelés, qu'ils soient définis ou non, qu'ils soient virtual ou non. Voici un exemple:

    #include <iostream>
    
    class Foo {
        public:
            ~Foo() {
                std::cerr << "Foo::~Foo()\n";
            };
    };
    
    class Bar : public Foo {
        public:
            ~Bar() {
                std::cerr << "Bar::~Bar()\n";
            };
    };
    
    int main() {
        Foo* bar = new Bar();
        delete bar;
    }
    

    Ce programme affiche uniquement Foo::~Foo(), le destructeur de Bar n'est pas appelé. Il n'y a aucun avertissement ou message d'erreur. Objets seulement partiellement détruits, avec toutes les conséquences. Assurez-vous donc de repérer vous-même cette condition lorsqu'elle se produit (ou faites un point pour ajouter virtual ~Foo() = default; à chaque classe non définie que vous définissez.

Si aucune de ces deux conditions n'est remplie, vous n'avez pas besoin de définir un destructeur, le constructeur par défaut suffira.


Passons maintenant à votre exemple de code:
Lorsque votre membre est un pointeur vers quelque chose (soit comme pointeur soit comme référence), le compilateur ne sait pas ...

  • ... s'il existe d'autres pointeurs vers cet objet.

  • ... que le pointeur pointe sur un objet ou sur un tableau.

Par conséquent, le compilateur ne peut pas déduire si ou comment détruire tout ce que pointe le pointeur. Ainsi, le destructeur par défaut ne détruit jamais rien derrière un pointeur.

Cela s'applique à la fois à brandname et à b. Par conséquent, vous avez besoin d'un destructeur, car vous devez faire la désallocation vous-même. Vous pouvez également utiliser des conteneurs RAII pour eux (std::string Et une variante de pointeur intelligent).

Ce raisonnement ne s'applique pas à vec car cette variable inclut directement un std::vector<> dans les objets. Par conséquent, le compilateur sait que vec doit être détruit, ce qui à son tour détruira tous ses éléments (c'est un conteneur RAII, après tout).

2
cmaster

Nous savons que si aucun destructeur n'est fourni, le compilateur en générera un.

Cela signifie que tout ce qui dépasse le simple nettoyage, comme les types primitifs, nécessitera un destructeur.

Dans de nombreux cas, l'allocation dynamique ou l'acquisition de ressources pendant la construction, a une phase de nettoyage. Par exemple, il peut être nécessaire de supprimer la mémoire allouée dynamiquement.

Si la classe représente un élément matériel, l'élément peut devoir être désactivé ou placé dans un état sûr.

Les conteneurs peuvent avoir besoin de supprimer tous leurs éléments.

En résumé, si la classe acquiert des ressources ou nécessite un nettoyage spécialisé (disons dans un ordre déterminé), il devrait y avoir un destructeur.

1
Thomas Matthews

Si vous allouez dynamiquement de la mémoire et que vous souhaitez que cette mémoire soit désallouée uniquement lorsque l'objet lui-même est "terminé", vous devez disposer d'un destructeur.

L'objet peut être "terminé" de deux manières:

  1. S'il a été alloué statiquement, il est "terminé" implicitement (par le compilateur).
  2. S'il a été alloué dynamiquement, il est alors "terminé" explicitement (en appelant delete).

Lorsque "terminé" explicitement en utilisant un pointeur de type classe de base, le destructeur doit être virtual.

1
barak manos