web-dev-qa-db-fra.com

Héritage C++ Constructeur/Destructeur

EDIT: Résumé des réponses

Dans la suite, B est une sous-classe de A.

C'est une question de terminologie; Les ctors et les détracteurs sont non hérités, en ce sens que le ctor/dtor de B sera non emprunté à l'interface de A. Une classe a au moins un constructeur et a exactement un destructeur.

  • Constructeurs :
    • B n'hérite pas des constructeurs de A;
    • Sauf si le cteur de B appelle explicitement un des cteur de A, le ctor par défaut de A sera appelé automatiquement le corps de ctor de avant B (l'idée étant que A doit être initialisé avant que B ne soit créé) .
  • Destructeurs :
    • B n'hérite pas du maître de A;
    • Après il se ferme, le destructeur de B appellera automatiquement le destructeur de A.

(Remerciements:} Je tiens à remercier tout particulièrement Oli Charlesworth et Kos pour leurs réponses. J'ai choisi la réponse de Kos comme solution car c'était celle que je comprenais le mieux.


ORIGINAL POST

Lorsque vous recherchez "Site d'héritage de destructeurs C++: stackoverflow.com" sur Google, vous trouvez actuellement les articles suivants:

  1. Héritage constructeur et destructeur : deux utilisateurs avec une réputation supérieure à 30 000 $ disent qu'il est hérité, et que ce n'est pas le cas
  2. Les destructeurs virtuels sont-ils hérités? : ici rien n'est mentionné qui indiquerait que les destructeurs ne sont pas hérités
  3. Destructeurs et héritage en C++? : Les commentaires semblent indiquer que les destructeurs sont hérités

Q1: Ce que je sais aussi de la pratique, est que vous ne pouvez pas initialiser un objet dérivé avec le même prototype que son constructeur parent sans définir explicitement un constructeur pour la classe dérivée, est-ce correct?


Même s'il ressort assez clairement des publications que les destructeurs semblent hérités, je suis toujours perplexe devant le fait qu'un utilisateur ayant une réputation de 32k dirait que ce n'est pas le cas. J'ai écrit un petit exemple qui devrait éclairer l'esprit de chacun:

#include <cstdio>

/******************************/

// Base class
struct A
{
    A() { printf( "\tInstance counter = %d (ctor)\n", ++instance_counter ); }
    ~A() { printf( "\tInstance counter = %d (dtor)\n", --instance_counter ); }

    static int instance_counter;
};

// Inherited class with default ctor/dtor
class B : public A {};

// Inherited class with defined ctor/dtor
struct C : public A
{
    C() { printf("\tC says hi!\n"); }
    ~C() { printf("\tC says bye!\n"); }
};

/******************************/

// Initialize counter
int A::instance_counter = 0;

/******************************/

// A few tests
int main()
{
    printf("Create A\n"); A a;
    printf("Delete A\n"); a.~A();

    printf("Create B\n"); B b;
    printf("Delete B\n"); b.~B();

    printf("Create new B stored as A*\n"); A *a_ptr = new B();
    printf("Delete previous pointer\n"); delete a_ptr;

    printf("Create C\n"); C c;
    printf("Delete C\n"); c.~C();

}

et voici le résultat (compilé avec g ++ 4.4.3):

Create A
    Instance counter = 1 (ctor)
Delete A
    Instance counter = 0 (dtor)
Create B
    Instance counter = 1 (ctor)
Delete B
    Instance counter = 0 (dtor)
Create new B stored as A*
    Instance counter = 1 (ctor)
Delete previous pointer
    Instance counter = 0 (dtor)
Create C
    Instance counter = 1 (ctor)
    C says hi!
Delete C
    C says bye!
    Instance counter = 0 (dtor)  // We exit main() now
    C says bye! 
    Instance counter = -1 (dtor)
    Instance counter = -2 (dtor)
    Instance counter = -3 (dtor)

Q2: Est-ce que quelqu'un qui pense que ce n'est pas hérité peut l'expliquer?

Q3: Que se passe-t-il lorsque vous appelez le constructeur d'une sous-classe avec des entrées? Le "constructeur vide" de la super-classe est-il également appelé?

51
Sheljohn

Terminologie, terminologie ...

OK, qu'entendons-nous par "Foo est hérité"? Nous voulons dire que si les objets de la classe A ont Foo dans son interface, alors les objets de la classe B qui est une sous-classe de A ont aussi Foo dans son interface.

  • Les constructeurs ne font pas partie de l'interface des objets. Ils appartiennent directement aux classes. Les classes A et B peuvent fournir des ensembles de constructeurs complètement différents. Aucun "être hérité" ici.

    (Détail d'implémentation: les constructeurs de chaque B appellent le constructeur de A.)

  • Les destructeurs font en effet partie de l’interface de chaque objet, étant donné que l’utilisateur de l’objet est responsable de leur appel (c'est-à-dire directement avec delete ou indirectement en laissant un objet hors de sa portée). Chaque objet a exactement un destructeur : son propre destructeur, qui peut éventuellement être virtuel. C'est toujours le sien et ce n'est pas hérité.

    (Détail d'implémentation: le destructeur de B appelle le destructeur de A.)

Donc: il y a un lien entre les constructeurs et les destructeurs de base et dérivés, mais ce n'est pas comme "ils sont hérités".

J'espère que cela répond à ce que vous avez en tête.

31
Kos

Q1: Ce que je sais aussi de la pratique, est que vous ne pouvez pas initialiser un objet dérivé avec le même prototype que son constructeur parent sans définir explicitement un constructeur pour la classe dérivée, est-ce correct?

Autre que le cas trivial où vous avez défini un constructeur par défaut dans la superclasse, oui, vous avez raison.


Q2: Est-ce que quelqu'un qui pense que ce n'est pas hérité peut l'expliquer?

Cela peut être une question de définitions de la terminologie. S'il est clair que des destructeurs virtuels existent et fonctionnent "comme prévu", nous voyons dans le standard C++ ([class.virtual]):

Même si les destructeurs ne sont pas hérités , un destructeur d'une classe dérivée remplace un destructeur de classe de base déclaré virtuel

(c'est moi qui souligne)


Q3: Alors que se passe-t-il lorsque vous appelez le constructeur d'une sous-classe avec des entrées? Le "constructeur vide" de la super-classe est-il également appelé?

Si vous n'appelez pas explicitement un constructeur de superclasse spécifique, le constructeur de superclasse par défaut sera appelé (en supposant qu'il soit visible).

7

Les destructeurs sont pas hérités. Si une classe ne définit pas un, le compilateur génère un. Pour les cas triviaux, ce destructeur appelle simplement le destructeur de la classe de base, ce qui signifie souvent qu’il n’ya pas de code explicite pour son destructeur (qui imite l’héritage). Mais si une classe a des membres avec des destructeurs, le destructeur généré appelle des destructeurs pour ces membres avant d'appeler le destructeur de la classe de base. C'est quelque chose qu'une fonction héritée ne ferait pas.

4
Pete Becker

Techniquement, les destructeurs sont hérités. Mais dans des circonstances normales, les destructeurs hérités ne sont pas directement utilisés pour une classe dérivée; ils sont appelés parce que le destructeur de la classe dérivée les appelle afin de détruire ses propres "sous-objets de classe de base" en tant qu'étape dans la destruction de l'objet plus grand. Et dans les circonstances inhabituelles où vous utilisez directement un destructeur de classe de base sur un objet dérivé, il est très difficile d'éviter le comportement indéfini.

Cet exemple provient directement de la norme C++ (12.4p12).

struct B {
  virtual ~B() { }
};
struct D : B {
  ~D() { }
};

D D_object;
typedef B B_alias;
B* B_ptr = &D_object;

void f() {
  D_object.B::~B();              // calls B's destructor
  B_ptr->~B();                   // calls D's destructor
  B_ptr->~B_alias();             // calls D's destructor
  B_ptr->B_alias::~B();          // calls B's destructor
  B_ptr->B_alias::~B_alias();    // calls B's destructor
}

Si ~B n'était pas un membre hérité de D, la première instruction dans f serait mal formée. En l'état actuel des choses, le C++ est légal, bien que extrêmement dangereux.

3
aschepler

L'héritage est ce que: mécanisme permettant de réutiliser et d'étendre les classes existantes sans les modifier, ce qui crée des relations hiérarchiques entre elles.

Héritage revient presque à incorporer un objet dans une classe.

lorsque la classe hérite d'une classe de base, le constructeur de la classe de base est d'abord appelé appelé, puis celui de la classe dérivée, et le call du destructeur est inversé.

Alors pourquoi le constructeur de classe de base est appelé (appelé non hérité peut être avec paramètres/par défaut): pour garantir que la classe de base est construite correctement lors de l'exécution du constructeur de la classe dérivée.

Now Calling of Destructor (call not inherit): lorsque l'objet de base sort du champ d'application, le destructeur est appelé de son propre chef. Il existe donc un problème d'héritage de destructeur.

maintenant vos questions:

ans 1 - oui vous avez raison pour la première question.
ans 2 - le destructeur est donc appelé non hérité après la fermeture de la portée de l'objet.
& ans 3 - si dans la classe dérivée vous appelez avec des paramètres, alors seul ce constructeur serait appelé, sans lequel aucun autre constructeur ne serait appelé .
il n’ya pas lieu de dire que 2 constructeur du même objet serait appelé à la création d’objet, comme constructeur appelé à la création d’un objet. Cela prépare le nouvel objet à l’utilisation. Il n’ya donc pas de logique de préparer l’objet deux fois avec différents constructeurs.

3
sourcecode

Dans votre exemple, vous appelez explicitement les fonctions de destructeur. Ceci est légal (évidemment, car il a été compilé et exécuté) mais presque toujours incorrect.

Pour les objets alloués dynamiquement créés avec new, le destructeur sera exécuté lorsque l'objet faisant l'objet de la suppression sera supprimé avec delete.

Pour les objets alloués de manière statique, créés simplement en déclarant l'objet dans l'étendue d'une fonction, le destructeur est exécuté lorsque l'étendue de l'objet disparaît. C'est-à-dire que, lorsque main() sera fermé, les destructeurs des objets seront exécutés. Mais vous avez déjà exécuté les destructeurs de ces objets en les appelant manuellement! C'est pourquoi la sortie de votre exemple montre le nombre décroissant à -3 ... vous avez exécuté les destructeurs pour a, b et c deux fois.

Voici le même code, annoté pour indiquer quand les destructeurs seront automatiquement exécutés:

int main()
{
    printf("Create A\n"); A a;
    printf("Delete A\n"); a.~A();

    printf("Create B\n"); B b;
    printf("Delete B\n"); b.~B();

    printf("Create new B stored as A*\n"); A *a_ptr = new B();
    printf("Delete previous pointer\n");
    delete a_ptr;   // Implicitly calls destructor for a_ptr.  a_ptr is class B,
       // so it would call a_ptr->~B() if it existed. Because B is an A, after
       // its destructor is called, it calls the superclass's destructor,
       // a_ptr->~A().

    printf("Create C\n"); C c;
    printf("Delete C\n"); c.~C();
}
// Function exits here at the close brace, so anything declared in its scope is
// deallocated from the stack and their destructors run.
// First `c` is destroyed, which calls c.~C(), then because C is a subclass of A
// calls c.~B() (which doesn't exist, so a blank implementation is used), then
// because B is a subclass of A calls c.~A().  This decrements the counter, but
// the count is wrong because you already manually called c.~C(), which you
// ordinarily shouldn't have done.
// Then `b` is destroyed, in a similar manner.  Now the count is off by 2,
// because you had already called b.~B().
// Lastly `a` is destroyed, just as above.  And again, because you had already
// called a.~A(), the count is now off by 3.
1
BigPeteB
I would want to express my thoughts. Creating any object is done in two stages:

1. Zone d’allocation de mémoire pour l’objet.

  1. Initialisation de cette zone de mémoire.

    Le constructeur d'objet est la fonction (méthode) de classe (pour cet objet), qui initialise la zone de mémoire allouée et appelée automatiquement . L'héritage incorpore l'objet de la classe à l'objet de l'autre classe. Il y a des jeux avec des pointeurs "ceci" "sous le couvercle". Le "this" est implicitement transmis à la méthode de classe. 

    Que se passe-t-il lorsque le code "B b" est terminé? Tout d'abord, la zone de mémoire est allouée à l'objet b. La classe B a son propre constructeur par défaut B (), qui est automatiquement appelé pour initialiser ce memeory. B() est la fonction, c'est pourquoi le cadre de pile est créé pour fonctionner. Ce constructeur a l'adresse de b (implicite). Mais l'objet de A doit être intégré à l'objet b. L'objet de A n'a pas de nom. Le constructeur de B sait que l'objet incorporé noname de A doit également être créé (pour que le compilateur C++ fonctionne). Par conséquent, le constructeur de la classe A pour initialiser l'objet incorporé de noname de la classe A est appelé dans le constructeur de B. Le nouveau cadre de pile est appelé et l'objet noname est en cours d'initialisation. Après cela, les cadres de la pile sont fermés et notre objet b de la classe B a été créé. Je pense que l'adresse de b et l'objet noname coïncident.

    Le destructeur est aussi la méthode de classe. Lorsque nous appelons ~ B (), le b n'est pas détruit. Le destructeur est la fonction appelée automatiquement lorsque l'objet est en cours de destruction. Mais cela ne signifie pas que lorsque nous appelons le destructeur, l'objet doit être détruit. Si le destructeur de B est appelé, le cadre de pile est créé pour un. Le destructeur par défaut de B connaît l'objet incorporé noname de la classe A (le compilateur C++ fonctionne donc). Par conséquent, le destructeur appelle le destructeur de A.

0
Konstantin