it-swarm.com.de

Vererbung von C++ - Konstruktor/Destruktor

EDIT: Zusammenfassung der Antworten

Im Folgenden ist B eine Unterklasse von A.

Es ist eine Frage der Terminologie. ctors und dtors werden not vererbt, in dem Sinne, dass der ctor/dtor von B not von der Schnittstelle von A entliehen wird. Eine Klasse hat mindestens einen Konstruktor und genau einen Destruktor.

  • Konstruktoren:
    • B erbt Konstruktoren nicht von A;
    • Wenn der ctor von B nicht explizit einer von As ctor aufruft, wird der voreingestellte ctor von A automatisch vorher Bs ctor-Body aufgerufen (die Idee ist, dass A initialisiert werden muss, bevor B erstellt wird). .
  • Destruktoren:
    • B erbt nicht A's dtor;
    • After wird beendet, der Destruktor von B ruft automatisch den Destruktor von A auf.

Danksagung: Ich möchte mich vor allem bei Oli Charlesworth und Kos für ihre Antworten bedanken. Ich habe die Antwort von Kos als Lösung gewählt, weil ich sie am besten verstanden habe.


ORIGINAL POST

Wenn Sie bei Google nach "C++ - Destruktor-Vererbungssite: stackoverflow.com" suchen, finden Sie derzeit die folgenden Einträge:

  1. Konstruktor- und Destruktor-Vererbung : Zwei Benutzer mit mehr als 30.000 Reputation geben an, dass es vererbt wird und nicht
  2. Werden virtuelle Destruktoren vererbt? : Hier wird nichts erwähnt, was darauf hindeutet, dass Destruktoren nicht vererbt werden
  3. Destruktoren und Vererbung in C++? : Die Kommentare scheinen darauf hinzudeuten, dass die Destruktoren vererbt werden

Q1: Was ich auch aus der Praxis weiß, ist, dass Sie ein abgeleitetes Objekt nicht mit demselben Prototyp wie sein übergeordneter Konstruktor initialisieren können, ohne explizit einen Konstruktor für die abgeleitete Klasse zu definieren. Ist das korrekt?


Auch wenn aus den Postings ziemlich klar hervorgeht, dass Destruktoren vererbt zu sein scheinen, bin ich immer noch verwirrt darüber, dass ein Benutzer mit 32k Reputation sagen würde, dass dies nicht der Fall wäre. Ich habe ein kleines Beispiel geschrieben, das alle verdeutlichen sollte:

#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();

}

und hier ist die Ausgabe (kompiliert mit 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)

F2: Kann jemand, der denkt, es sei nicht vererbt, das bitte erklären?

Q3: Was passiert also, wenn Sie den Konstruktor einer Unterklasse mit Eingaben aufrufen? Wird auch der "leere Konstruktor" der Superklasse aufgerufen?

51
Sheljohn

Terminologie, Terminologie ...

OK, was meinen wir mit "Foo wird vererbt"? Wir meinen, wenn Objekte der Klasse AFoo in ihrer Schnittstelle haben, dann haben Objekte der Klasse B, die eine Unterklasse von A ist, auch Foo in ihrer Schnittstelle.

  • Konstruktoren sind nicht Teil der Objektschnittstelle. Sie gehören direkt zu den Klassen. Die Klassen A und B können völlig unterschiedliche Konstruktor-Sets enthalten. Kein "Vererben" hier.

    (Implementierungsdetail: Die Konstruktoren jedes B rufen den Konstruktor eines A auf.)

  • Destruktoren sind in der Tat ein Teil der Schnittstelle jedes Objekts, da der Benutzer des Objekts für den Aufruf dieser Objekte verantwortlich ist (d. H. Direkt mit delete oder indirekt, indem ein Objekt außerhalb des Gültigkeitsbereichs gelassen wird). Jedes Objekt hat genau einen Destruktor : seinen eigenen Destruktor, der optional ein virtueller sein kann. Es ist immer etwas Eigenes und wird nicht vererbt.

    (Implementierungsdetail: Der Destruktor von B ruft den Destruktor von A auf.)

Also: Es gibt eine Verbindung zwischen Basis- und abgeleiteten Konstruktoren und Destruktoren, aber es ist nicht so, als wären sie vererbt.

Ich hoffe, das beantwortet, was Sie vorhaben.

31
Kos

Q1: Was ich auch aus der Praxis weiß, ist, dass Sie ein abgeleitetes Objekt nicht mit demselben Prototyp wie sein übergeordneter Konstruktor initialisieren können, ohne explizit einen Konstruktor für die abgeleitete Klasse zu definieren. Ist das richtig?

Abgesehen von dem trivialen Fall, in dem Sie einen Standardkonstruktor in der Superklasse definiert haben, sind Sie richtig.


Q2: Kann jemand, der denkt, es sei nicht vererbt, das bitte erklären?

Dies kann eine Frage der Terminologie sein. Während es klar ist, dass virtuelle Destruktoren existieren und "wie erwartet" funktionieren, sehen wir im C++ - Standard ([class.virtual]):

Obwohl Destruktoren nicht vererbt werden , überschreibt ein Destruktor in einer abgeleiteten Klasse einen als virtuell deklarierten Destruktor der Basisklasse

(Hervorhebung meines)


Q3: Was passiert also, wenn Sie den Konstruktor einer Unterklasse mit Eingaben aufrufen? Wird auch der "leere Konstruktor" der Superklasse aufgerufen?

Wenn Sie nicht explizit einen bestimmten Superklassenkonstruktor aufrufen, wird der standardmäßige Superklassenkonstruktor aufgerufen (vorausgesetzt, er ist sichtbar).

7

Destruktoren werden nicht vererbt. Wenn eine Klasse keine Klasse definiert, generiert der Compilerone. In einfachen Fällen ruft der Destruktor nur den Destruktor der Basisklasse auf, was oft bedeutet, dass es keinen expliziten Code für den Destruktor gibt (der die Vererbung imitiert). Wenn eine Klasse über Member mit Destruktoren verfügt, ruft der generierte Destruktor Destruktoren für diese Member auf, bevor der Destruktor der Basisklasse aufgerufen wird. Eine vererbte Funktion würde das nicht tun.

4
Pete Becker

Destruktoren werden technisch vererbt. Unter normalen Umständen werden die geerbten Destruktoren jedoch nicht direkt für eine abgeleitete Klasse verwendet. Sie werden aufgerufen, weil der eigene Destruktor der abgeleiteten Klasse sie aufruft, um ihre eigenen "Basisklassen-Unterobjekte" als einen Schritt innerhalb der Zerstörung des größeren Objekts zu zerstören. In den ungewöhnlichen Situationen, in denen Sie einen Destruktor der Basisklasse direkt für ein abgeleitetes Objekt verwenden, ist es sehr schwierig, undefiniertes Verhalten zu vermeiden.

Dieses Beispiel stammt direkt vom C++ - Standard (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
}

Wenn ~B kein geerbtes Mitglied von D wäre, würde die erste Anweisung in f falsch gebildet werden. Wie es ist, ist es legales C++, obwohl es extrem gefährlich ist.

3
aschepler

Vererbung ist was: Mechanismus der Wiederverwendung und Erweiterung vorhandener Klassen, ohne sie zu verändern, wodurch hierarchische Beziehungen zwischen ihnen erzeugt werden.

Vererbung ist fast wie das Einbetten eines Objekts in eine Klasse.

wenn die Klasse eine Basisklasse erbt, wird der Konstruktor der Basisklasse zuerst aufgerufen dann die abgeleiteten Klassen und der Aufruf des Destruktors in umgekehrter Reihenfolge.

Warum der Basisklassenkonstruktor aufgerufen wird (der Aufruf wird nicht vererbt, kann über Parameter/default erfolgen): to garantiert, dass die Basisklasse ordnungsgemäß erstellt wird, wenn der Konstruktor für die abgeleitete Klasse ausgeführt wird.

Jetzt Aufruf des Destruktors (Aufruf nicht vererben): Wenn das Basisobjekt den Gültigkeitsbereich verlässt, wird der Destruktor eigenständig aufgerufen. Daher gibt es ein Problem mit der Vererbung des Destruktors.

jetzt deine Fragen:

ans 1 - ja, die erste Frage ist richtig.
ans 2 - Der Destruktor wird also nicht geerbt, nachdem der Gültigkeitsbereich des Objekts erloschen ist.
& ans 3 - Wenn Sie in einer abgeleiteten Klasse den Aufruf mit Parametern durchführen, wird nur der Konstruktor aufgerufen, mit der kein anderer Konstruktor ..__ aufgerufen wird.
Es gibt keinen Punkt von issuse, dass ein Konstruktor desselben Objekts bei der Objekterstellung aufgerufen würde, wie der Konstruktor , der bei der Erstellung eines Objekts aufgerufen wurde. Es bereitet das neue Objekt für die Verwendung vor. Es gibt also keine Logik, das Objekt zweimal mit verschiedenen Konstruktoren aufzubereiten.

3
sourcecode

In Ihrem Beispiel rufen Sie explizit die Destruktorfunktionen auf. Dies ist legal (offensichtlich, da es kompiliert und ausgeführt wurde), aber fast immer falsch.

Bei dynamisch zugewiesenen Objekten, die mit new erstellt wurden, wird der Destruktor ausgeführt, wenn das Objekt mit delete entfernt wird.

Bei statisch zugewiesenen Objekten, die einfach durch Deklarieren des Objekts im Gültigkeitsbereich einer Funktion erstellt werden, wird der Destruktor ausgeführt, wenn der Gültigkeitsbereich des Objekts verschwindet. Das heißt, wenn main() beendet wird, werden die Destruktoren der Objekte ausgeführt. Sie haben jedoch bereits die Destruktoren für diese Objekte ausgeführt, indem Sie sie manuell aufrufen! Aus diesem Grund zeigt die Ausgabe Ihres Beispiels, dass die Anzahl auf -3 sinkt. Sie haben die Destruktoren für a, b und c zweimal ausgeführt.

Hier ist derselbe Code, der kommentiert wird, um anzuzeigen, wann Destruktoren automatisch ausgeführt werden:

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. Zuweisen eines Speicherbereichs für das Objekt.

  1. Dieser Speicherbereich wird initialisiert.

    Der Konstruktor von object ist die Funktion (Methode) der Klasse (für dieses Objekt), die den zugewiesenen Speicherbereich initialisiert und automatisch aufgerufen wird .. Die Vererbung bindet das Objekt der einen Klasse in das Objekt der anderen Klasse ein. Es gibt Spiele mit Spitzen "dieses" "unter dem Deckel". Das "this" wird implizit an die Methode der Klasse weitergegeben. 

    Was passiert, wenn der Code "B b" fertig ist. Zunächst wird der Speicherbereich für Objekt b zugewiesen. Die Klasse B hat einen eigenen Standardkonstruktor B (), der automatisch zum Initialisieren dieses Speichers aufgerufen wird. B() ist die Funktion, daher wird der Stapelrahmen für die Arbeit erstellt. Dieser Konstruktor hat die Adresse b (Implizität). Das Objekt von A muss jedoch in das Objekt b eingebettet sein. Das Objekt von A hat keinen Namen. Konstruktor von B weiß, dass das eingebettete Noname-Objekt von A ebenfalls erstellt werden muss (damit der Compiler C++ funktioniert). Daher wird im Konstruktor von B der Konstruktor der Klasse A zum Initialisieren des nichtnamigen eingebetteten Objekts der Klasse A aufgerufen. Der neue Stapelrahmen wird aufgerufen und das Nichtnamensobjekt wird initialisiert. Danach werden die Stack-Frames geschlossen und unser Objekt b der Klasse B ist fertig. Ich denke, dass die Adresse von b und das Noname-Objekt zusammenfallen.

    Der Destruktor ist auch die Methode der Klasse. Wenn wir ~ B () aufrufen, wird das b nicht zerstört. Der Destruktor ist die Funktion, die avtomatisch genannt wird, wenn das Objekt zerstört wird. Das heißt aber nicht, dass das Objekt zerstört werden muss, wenn wir den Destruktor aufrufen. Wenn der Destruktor von B aufgerufen wird, wird der Stack-Frame für einen erstellt. Der Standarddesructor von B kennt das eingebettete Noname-Objekt der Klasse A (damit der Compiler C++ funktioniert). Daher nennt der Destruktor den Destruktor von A.

0
Konstantin