C + + 构造函数/析构函数继承

编辑: 答案摘要

在下面的例子中,B 是 A 的一个子类。

这是一个术语问题; ctor 和 dtor 是 没有继承的,从这个意义上说,B 的 ctor/dtor 将从 A 的接口借用 没有。一个类至少有一个构造函数,并且只有一个析构函数。

  • 构造函数 :
    • B 不从 A 继承构造函数;
    • 除非 B 的 ctor 显式地调用 其中之一 A 的 ctor,否则 A 的默认 ctor 将自动调用 之前 B 的 ctor 体(这个想法是,在创建 B 之前需要初始化 A)。
  • 破坏者 :
    • B 不继承 A 的父亲;
    • 退出之后,B 的析构函数将自动调用 A 的析构函数。

鸣谢: 我想特别感谢奥利查尔斯沃思和科斯的答案,我把科斯的答案设置为解决方案,因为这是我最了解的一个。


原帖

当你在 Google 上搜索“ c + + 析构函数继承网站: stackoverflow.com”时,你会发现下面的帖子:

  1. 构造函数和析构函数继承 : 两个声誉超过30k 的用户说它是继承的,而不是继承的
  2. 虚析构函数是继承的吗? : 这里没有提到指向不继承析构函数的内容
  3. C + + ? 中的析构函数和继承: 注释似乎表明析构函数是继承的

Q1: 我从实践中也知道,如果不为派生类显式定义一个构造函数,就不能使用与其父构造函数相同的原型来初始化一个派生对象,对吗?


尽管从帖子中可以很清楚地看出析构函数似乎是继承的,但我仍然对一个拥有32K 声誉的用户会说它不是这样的事实感到困惑。我写了一个小例子,应该澄清每个人的想法:

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

问题2: 有谁认为这不是遗传的,请解释一下?

Q3: 那么当你用输入调用子类的构造函数时会发生什么呢?是否也调用了超类的“空构造函数”?

99836 次浏览

Q1: 我从实践中也知道,如果不为派生类显式定义构造函数,就不能用与其父构造函数相同的原型初始化派生对象,对吗?

除了在超类中定义了一个缺省构造函数这种琐碎的情况之外,是的,你是正确的。


问题2: 有谁认为这不是遗传的,请解释一下?

这可能是术语定义的问题。虽然很明显虚拟析构函数存在并且“按预期”工作,但是我们在 C + + 标准([ class.virtual ])中看到:

即使析构函数不是继承的 ,派生类中的析构函数也会覆盖声明为 Virtual 的基类析构函数

(强调我的)


Q3: 那么当你用输入调用子类的构造函数时会发生什么呢?是否也调用了超类的“空构造函数”?

如果您没有显式地调用特定的超类构造函数,那么将调用默认的超类构造函数(假设它是可见的)。

术语,术语..。

好吧,我们说的“ Foo 是遗传的”是什么意思?我们的意思是,如果类 A的对象在它的接口中有 Foo,那么类 B的对象(它是 A的子类)在它的接口中也有 Foo

  • 构造函数 不是对象接口的一部分。它们直接属于类。类 AB可以提供完全不同的构造函数集。这里没有“继承”。

    (实现细节: 每个 B 的构造函数调用一些 A 的构造函数。)

  • 析构函数 确实是每个对象接口的一部分,因为对象的用户负责调用它们(例如,直接使用 delete或间接使对象超出作用域)。每个对象只有一个析构函数: 它自己的析构函数,可以选择是一个虚拟的析构函数。它总是自己的,不是遗传的。

    (实现细节: B 的析构函数调用 A 的析构函数。)

所以: 在基本的和派生的构造函数和析构函数之间是有联系的,但这并不像“它们是继承的”。

我希望这能回答你的问题。

析构函数是 没有继承的。如果一个类没有定义一个,那么编译器 产生定义一个。对于一些琐碎的情况,析构函数只调用基类的析构函数,这通常意味着它的析构函数没有显式代码(它模仿继承)。但是,如果一个类有具有析构函数的成员,生成的析构函数将在调用基类的析构函数之前调用这些成员的析构函数。这是一个继承的函数不会做的事情。

继承是这样的: 重用和扩展现有类而不修改它们的机制,从而在它们之间产生层次关系。

继承 几乎就像是将一个对象嵌入到一个类中。

当类继承一个基类时,基类的构造函数首先是 电话,然后是派生类的构造函数,而析构函数的 打电话的顺序是相反的。

因此,为什么要调用基类构造函数(调用非继承类可以使用参数/默认值) : 以保证在执行派生类的构造函数时基类被正确构造。

现在调用析构函数(调用不继承) : 当基对象超出作用域时,就会自己调用析构函数。所以存在析构函数继承的 np 问题。

现在你们的问题是:

是的,第一个问题你答对了。
Ans 2 -因此,在对象的作用域消失后,将不再继承析构函数。
& 安老院3 -如果在派生类中给出带参数的调用,那么只有那个构造函数会被调用,没有其他构造函数会被调用。
在创建对象时调用同一对象的2个构造函数没有任何问题,如 在创建对象时调用的构造函数。它为 use.so 准备新对象,因此没有使用不同构造函数两次准备对象的逻辑。

从技术上讲,析构函数是继承的。但在正常情况下,继承的析构函数并不直接用于派生类; 它们之所以被调用,是因为派生类自己的析构函数调用它们是为了销毁自己的“基类子对象”,作为销毁较大对象的一个步骤。在一些不寻常的情况下,如果你在派生对象上直接使用基类析构函数,那么很难避免未定义行为。

这个例子直接来自 C + + 标准(12.4 p12)。

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... ... 您已经两次运行了 abc的析构函数。

下面是同样的代码,注释显示析构函数将在何时自动运行:

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. 初始化这个内存区域。

    对象的构造函数是类(对于这个对象)的函数(方法) ,它初始化分配的内存区域并自动调用。 继承是将一个类的对象嵌入到另一个类的对象中。有些戏剧“这个”“盖子下面”。“ this”被隐式地传递给 class 的方法。

    代码“ B”完成后会发生什么。首先为对象 b 分配内存区域。类 b 有自己的缺省构造函数 b () ,它被自动调用来初始化这个内存。B ()是一个函数,因此堆栈帧是为一个函数创建的。这个构造函数的地址是 b (隐式)。但是 A 的对象必须嵌入到对象 b 中。A 的对象没有名称。B 的构造函数知道必须创建 A 的非名称嵌入对象(因此编译器 C + + 可以工作)。因此,在 B 的构造函数中调用 A 类的构造函数来初始化 A 类的非名称嵌入对象。调用新的堆栈帧并初始化 noname 对象。在此之后,堆栈帧将被关闭,类 B 的对象 b 已经完成。我认为 b 的地址和非名对象是一致的。

    析构函数也是类的方法。当我们调用 ~ B ()时,b 不会被破坏。析构函数是在销毁对象时自动调用的函数。但这并不意味着当我们调用析构函数时,对象必须被销毁。如果调用了 B 的析构函数,则为其创建堆栈帧。B 的默认析构函数知道类 A 的非名称嵌入对象(因此编译器 C + + 可以工作)。因此,析构函数调用 A 的析构函数。