C++ Конструктор / Деструктор наследования

РЕДАКТИРОВАТЬ: Резюме ответов

В дальнейшем B является подклассом A.

Это вопрос терминологии; ctors и dtors не наследуются, в том смысле, что ctor / dtor B не будет заимствован из интерфейса A. Класс имеет по крайней мере один конструктор и имеет ровно один деструктор.

  • Конструкторы:
    • B не наследует конструкторов от A;
    • Если ctor B явно не вызывает один из ctor A, ctor по умолчанию из A будет вызываться автоматически перед телом ctor B (идея состоит в том, что A необходимо инициализировать до создания B).
  • Деструкторы:
    • B не наследует Dtor A;
    • После выхода деструктор Б автоматически вызовет деструктор А.

Благодарности: Я хотел бы поблагодарить Oli Charlesworth и Kos за их ответы. Я выбрал Kos для решения, потому что это был тот, который я понял лучше всего.


ОРИГИНАЛЬНАЯ ПОЧТА

При поиске в Google сайта наследования деструктора C++:stackru.com в настоящее время вы обнаружите следующие сообщения:

  1. Наследование конструктора и деструктора: два пользователя с репутацией 30 000+ говорят, что это наследуется, а что нет
  2. Унаследованы ли виртуальные деструкторы?: здесь ничего не сказано, что указывало бы на то, что деструкторы не наследуются
  3. Деструкторы и наследование в C++?: Комментарии, кажется, указывают на то, что деструкторы наследуются

Вопрос1: Что я также знаю из практики, так это то, что вы не можете инициализировать производный объект с тем же прототипом, что и его родительский конструктор, без явного определения конструктора для производного класса, это правильно?


Несмотря на то, что из сообщений довольно ясно, что деструкторы, кажется, унаследованы, я все еще озадачен фактом, что пользователь с репутацией 32 КБ сказал бы, что это не так. Я написал небольшой пример, который должен прояснить мнение каждого:

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

}

и вот вывод (скомпилированный с 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: Может кто-нибудь, кто думает, что он не унаследован, пожалуйста, объясните это?

Q3: Так что же происходит, когда вы вызываете конструктор подкласса с входными данными? Вызывается ли также "пустой конструктор" суперкласса?

7 ответов

Решение

Терминология, терминология...

Хорошо, что мы подразумеваем под "Foo наследуется"? Мы имеем в виду, что если объекты класса A иметь Foo в его интерфейсе, то объекты класса B который является подклассом A Также есть Foo в его интерфейсе.

  • Конструкторы не являются частью интерфейса объектов. Они принадлежат непосредственно к классам. Классы A а также B может предоставлять совершенно разные наборы конструкторов. Никакого "наследования" здесь нет.

    (Детали реализации: конструкторы каждого B вызывают конструктор некоторого A.)

  • Деструкторы действительно являются частью интерфейса каждого объекта, так как пользователь объекта отвечает за их вызов (т.е. непосредственно с delete или косвенно, выпуская объект из области видимости). Каждый объект имеет ровно один деструктор: свой деструктор, который может быть виртуальным. Он всегда свой и не наследуется.

    (Детали реализации: деструктор B вызывает деструктор A.)

Итак: есть связь между базовыми и производными конструкторами и деструкторами, но это не то, что "они наследуются".

Я надеюсь, что это отвечает тому, что вы имеете в виду.

Вопрос1: Что я также знаю из практики, так это то, что вы не можете инициализировать производный объект с тем же прототипом, что и его родительский конструктор, без явного определения конструктора для производного класса, это правильно?

За исключением тривиального случая, когда вы определили конструктор по умолчанию в суперклассе, да, вы правы.


Q2: Может кто-нибудь, кто думает, что он не унаследован, объясните это?

Это может быть вопросом определения терминологии. Хотя очевидно, что виртуальные деструкторы существуют и работают "как положено", мы видим в стандарте C++ ([class.virtual]):

Хотя деструкторы не наследуются, деструктор в производном классе переопределяет деструктор базового класса, объявленный виртуальным

(акцент мой)


Q3: Так что же происходит, когда вы вызываете конструктор подкласса с входными данными? Вызывается ли также "пустой конструктор" суперкласса?

Если вы явно не вызовете конкретный конструктор суперкласса, будет вызван конструктор суперкласса по умолчанию (при условии, что он видим).

Деструкторы не наследуются. Если класс не определяет его, компилятор создает его. Для тривиальных случаев этот деструктор просто вызывает деструктор базового класса, и часто это означает, что нет явного кода для его деструктора (который имитирует наследование). Но если у класса есть члены с деструкторами, сгенерированный деструктор вызывает деструкторы для этих членов перед вызовом деструктора базового класса. Это то, что унаследованная функция не будет делать.

Наследование - это то, что: механизм повторного использования и расширения существующих классов без их изменения, что создает иерархические отношения между ними.

Наследование почти похоже на встраивание объекта в класс.

когда класс наследует базовый класс, сначала вызывается конструктор базового класса, а затем производный класс, и вызов деструктора происходит в обратном порядке.

Так почему конструктор базового класса вызывается (вызывается без наследования, может быть с параметрами / по умолчанию): чтобы гарантировать, что базовый класс правильно сконструирован при выполнении конструктора для производного класса.

Теперь вызов Деструктора (вызов без наследования): когда базовый объект выходит из области видимости, деструктор вызывается сам по себе. Так что возникает проблема наследования деструктора.

Теперь ваши вопросы:

Ответ 1 - да, вы правы для первого вопроса.
ans 2 - поэтому деструктор вызывается не наследуемым после того, как область объекта выходит за пределы.
& ans 3 - если в производном классе вы передаете вызов с параметрами, то будет вызван только этот конструктор, при этом никакой другой конструктор не будет вызван.
нет никакого смысла в том, что 2 конструктора одного и того же объекта вызывались бы при создании объекта, как конструктор, вызываемый при создании объекта. Он подготавливает новый объект для использования. Так что нет логики готовить объект дважды с разными конструкторами.

Технически, деструкторы являются наследственными. Но в обычных обстоятельствах унаследованные деструкторы не используются напрямую для производного класса; они вызываются потому, что собственный деструктор производного класса вызывает их, чтобы уничтожить свои собственные "подобъекты базового класса" как шаг в разрушении более крупного объекта. И в необычных обстоятельствах, когда вы непосредственно используете деструктор базового класса в производном объекте, очень трудно избежать неопределенного поведения.

Этот пример взят прямо из стандарта 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
}

Если ~B не были унаследованным членом DПервое утверждение в f будет плохо сформирован. На самом деле это законный C++, хотя и чрезвычайно опасный.

В вашем примере вы явно вызываете функции деструктора. Это законно (очевидно, поскольку оно скомпилировано и запущено), но почти всегда неверно.

Для динамически размещаемых объектов, созданных с new, деструктор будет запущен после удаления объекта delete,

Для статически размещенных объектов, которые создаются просто путем объявления объекта в области действия функции, деструктор запускается, когда область действия объекта исчезает. То есть когда main() выходы, деструкторы объектов будут запущены. Но вы уже запустили деструкторы для этих объектов, вызвав их вручную! Вот почему результат вашего примера показывает, что число уменьшается до -3... вы запустили деструкторы для a, b, а также c дважды.

Вот тот же код, аннотированный, чтобы показать, когда деструкторы будут автоматически запускаться:

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.
I would want to express my thoughts. Creating any object is done in two stages:

1. Выделение области памяти для объекта.

  1. Инициализация этой области памяти.

    Конструктор объекта - это функция (метод) класса (для этого объекта), которая инициализирует выделенную область памяти и вызывается автоматически. Наследование - это встраивание объекта одного класса в объект другого класса. Есть пьесы с указателями "это", "под крышкой". "Это" неявно передается методу класса.

    Что происходит, когда код "B b" готов. Сначала область памяти выделяется для объекта b. Класс B имеет собственный конструктор по умолчанию B(), который автоматически вызывается для инициализации этой памяти. B() - функция, поэтому стековый фрейм создан для рабочего. Этот конструктор имеет адрес b (безупречность). Но объект A должен быть встроен в объект b. Объект А не имеет имени. Конструктор B знает, что также должен быть создан вложенный объект A без имени (поэтому работает компилятор C++). Поэтому конструктор класса A для инициализации встраиваемого объекта без имени класса A вызывается в конструкторе B. Вызывается новый кадр стека и инициализируется объект без имени. После этого стековые фреймы закрываются, и наш объект b класса B завершен. Я думаю, что адрес b и объект noname совпадают.

    Деструктор - это тоже метод класса. Когда мы вызываем ~B(), b не разрушается. Деструктор - это функция, которая вызывается автоматически при уничтожении объекта. Но это не значит, что когда мы вызываем деструктор, объект должен быть уничтожен. Если вызывается деструктор B, кадр стека создается для одного. Дескриптор по умолчанию для B знает о внедренном объекте noname класса A (поэтому работает компилятор C++). Поэтому деструктор называет деструктором А.

Другие вопросы по тегам