继承中构造函数/析构函数的调用顺序

46

关于创建对象的一个小问题。比如说我有这两个类:

struct A{
    A(){cout << "A() C-tor" << endl;}
    ~A(){cout << "~A() D-tor" << endl;}
};

struct B : public A{
    B(){cout << "B() C-tor" << endl;}
    ~B(){cout << "~B() D-tor" << endl;}

    A a;
};

在main函数中我创建了一个B实例:

int main(){
    B b;
}

请注意,B 继承自 A 并且也有一个类型为 A 的字段。

我正在尝试弄清楚规则。我知道在构造对象时首先调用其父级构造函数,反之亦然,在析构时同样如此。

那么字段呢(在这种情况下是 A a;)?当创建 B 时,它何时调用 A 的构造函数?我没有定义初始化列表,是否有某种默认列表?如果没有默认列表呢?析构时同样的问题。


3
如果您在析构函数的消息与构造函数的消息不同的话,您的示例可能会更具说明性。另外,那些 std::sort 是做什么用的呢? - Tom
此外,在进行实验时,请比较 B bB* b = new B(); delete b;A* a = new b(); delete a; 的构造和销毁过程(比较在析构函数中使用 virtual 关键字时会发生什么,即 virtual ~A() {cout<<"A D-tor"<<endl;})。 - Tom
@Tom,你是对的。正在消除编译器错误。 - iammilind
我已经编辑了你问题中的代码,使得四个方法都打印不同的信息。现在你只需要实例化B并观察stdout就可以自己弄清楚了。如果这不是你想要的,请进行审核并回滚。 - Caleb
6个回答

83
  • 构造始终从基类 class 开始。如果有多个基类,那么构造将从最左边的基类开始。(: 如果存在虚拟继承,则会优先考虑它)。
  • 然后构造成员字段。它们按照声明的顺序进行初始化。
  • 最后构造 class 本身。
  • 析构函数的顺序完全相反。

无论初始化列表如何,调用顺序都是这样的:

  1. 基类 A 的构造函数
  2. B 类的名为 a 的字段(类型为 A)将被构造
  3. 派生类 B 的构造函数

3
最后,类本身被构造。这里你是在谈论构造函数体吗? - Wolf
1
@Wolf:我猜是的。 - Ludwik
1
@Wolf。它指的是派生类。 - MSD561

24
假设没有虚拟/多重继承(这会使事情变得相当复杂),则规则很简单:
  1. 对象内存被分配
  2. 执行基类的构造函数,以最派生类结束
  3. 执行成员初始化
  4. 对象成为其类的真实实例
  5. 执行构造函数代码
重要的一点是,在第4步之前,对象还不是其类的实例,因为仅在构造函数开始执行后,它才获得这个名称。这意味着,如果在成员构造函数期间抛出异常,则不会执行对象的析构函数,但是只有已经构造的部分(例如成员或基类)将被销毁。这也意味着,如果在成员或基类的构造函数中调用对象的任何虚成员函数,则调用的实现将是基类的,而不是派生类的。 另一个重要的事项是,在初始化列表中列出的成员将按照它们在类中声明的顺序进行构造,而不是按照它们出现在初始化列表中的顺序进行构造(幸运的是,大多数合理的编译器都会发出警告,如果您按照与类声明不同的顺序列出成员)。
请注意,即使在构造函数代码执行期间,this对象已经获得了其最终类(例如,关于虚拟分派),除非构造函数完成执行,否则不会调用类的析构函数。只有当构造函数完成执行后,对象实例才是实例中真正的第一等公民...在那之前,只是一个“想成为实例的”对象(尽管具有正确的类)。
销毁的过程恰好相反:首先执行对象析构函数,然后它失去了其类(即从这一点上,该对象被认为是基对象),然后按照相反的声明顺序销毁所有成员,最后执行基类销毁过程直到最抽象的父级。与构造函数一样,如果您在基类或成员析构函数中直接或间接调用了对象的任何虚成员函数,则执行的实现将是父级的,因为当类析构函数完成时,对象已经失去了其类名称。

@Wolf:是的,这是有保证的。如果你有一个派生自B的类D,那么在实例化D时首先执行B的构造函数(最抽象的),然后执行D的构造函数。实际上,在所有基类和其他成员的构造完成之后,对象才真正成为一个真正的D(关于在成员的构造或基类子对象的构造期间调用D的虚方法的技巧点)。 - 6502
我所询问的并不是从基类到派生类的构造顺序,而是我认为“抽象”这个词有误导性。据我所学,抽象类是至少有一个纯虚方法的类。请看这个例子 - Wolf
@Wolf:当术语被不同解释时,讨论当然很困难(“当我使用一个词时,”亨普蒂·韦尔德说,语气相当轻蔑,“它的意思就是我选择的意思——既不多也不少。”):-)。然而,如果你有 A <= B <= C <= D(其中 X <= Y 的意思是 "XY 的基类"),那么最常用的说法是 A 是“最抽象”的类,D 是“最具体”的类,与纯虚方法无关。无论如何,我会重新用“最派生”的方式表述。 - 6502

7

在构造对象时,基类会比数据成员先被构造。数据成员按照类中声明的顺序进行构造,与初始化列表无关。当正在初始化某个数据成员时,它会查找初始化列表中的参数,并在无匹配时调用默认构造函数。销毁数据成员时总是以相反的顺序调用析构函数。


3

基类构造函数总是先执行。因此,当您编写语句B b;时,将首先调用A的构造函数,然后是B类的构造函数。因此,构造函数的输出将按以下顺序进行:

A() C-tor
A() C-tor
B() C-tor

1
#include<iostream>

class A
{
  public:
    A(int n=2): m_i(n)
    {
    //   std::cout<<"Base Constructed with m_i "<<m_i<<std::endl;
    }
    ~A()
    {
    // std::cout<<"Base Destructed with m_i"<<m_i<<std::endl; 
     std::cout<<m_i;
    }

  protected:
   int m_i;
};

class B: public A
{
  public:
   B(int n ): m_a1(m_i  + 1), m_a2(n)
   {
     //std::cout<<"Derived Constructed with m_i "<<m_i<<std::endl;
   }

   ~B()
   {
   //  std::cout<<"Derived Destructed with m_i"<<m_i<<std::endl; 
     std::cout<<m_i;//2
     --m_i;
   }

  private:
   A m_a1;//3
   A m_a2;//5
};

int main()
{
  { B b(5);}
  std::cout <<std::endl;
  return 0;
}

在这种情况下的答案是2531。构造函数的调用顺序如下:
  1. 调用B::A(int n=2)构造函数
  2. 调用B::B(5)构造函数
  3. 调用B.m_A1::A(3)
  4. 调用B.m_A2::A(5)
相同方式的析构函数被调用:
  1. 调用B::~B(),即m_i = 2,在A中将m_i减少到1。
  2. 调用B.m_A2::~A(),即m_i = 5
  3. 调用B.m_A1::~A(),即m_i = 3
  4. 调用B::~A(),即m_i = 1
在这个例子中,m_A1和m_A2的构造与初始化列表的顺序无关,而与它们的声明顺序有关。

0

修改后的代码输出结果为:

A() C-tor
A() C-tor
B() C-tor
~B() D-tor
~A() D-tor
~A() D-tor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接