C++中析构函数和构造函数的调用顺序是什么?

65

C++中构造函数和析构函数的调用顺序是什么?使用一些基类和派生类的例子进行说明。

5个回答

81

执行顺序如下:

  1. 基类构造函数
  2. 派生类构造函数
  3. 派生类析构函数
  4. 基类析构函数

示例:

class B
{
public:
  B()
  {  
    cout<<"Construct B"<<endl;
  }

  virtual ~B()
  {
    cout<<"Destruct B"<<endl;
  }
};

class D : public B
{
public:
  D()
  {  
    cout<<"Construct D"<<endl;
  }

  virtual ~D()
  {
    cout<<"Destruct D"<<endl;
  }
};



int main(int argc, char **argv)
{
  D d; 
  return 0;
}

示例输出:

构造B

构造D

析构D

析构B

多继承的工作方式类似于堆栈:

假设将向堆栈中压入一个项目视为构造操作,而将其弹出视为析构操作,则可以将多继承的工作方式看作是一个堆栈。

这适用于任何层数的情况。

例如,D2从D派生而来,D又从B派生而来。

将B压入堆栈,然后将D压入堆栈,最后将D2压入堆栈。因此,构造顺序是B、D、D2。然后要找到析构顺序,请开始弹出,先弹出D2,然后是D,最后是B。

更复杂的示例:

有关更复杂的示例,请参见@JaredPar提供的链接。


22

提供的链接已失效。 - jbx

10

另外请记住,虽然数组元素是按照从前到后的顺序构造的,但它们的析构顺序却是相反的:从后往前。


3
大部分情况下,这个说法是正确的。毁坏的顺序总是与建造的顺序相反。静态变量没有保证的构造顺序,但它们的销毁顺序将会相反。 - David Rodríguez - dribeas
1
这是(预期的)模板化容器行为和/或内置的new [] / delete []行为吗? - franji1

9

我必须补充前面的答案,因为每个人似乎都忽略了它。

当创建派生类实例时,确实会在派生类构造函数内部调用基类构造函数中的代码之前,但要记住,从技术上讲,在基类之前仍然可以创建派生类

而且,在调用派生类析构函数时,确实会在派生类析构函数内部调用代码之前调用基类析构函数内部的代码,但也要记住,在派生类之前销毁基类

当我说创建/销毁时,我实际上是指分配/释放

如果查看这些实例的内存布局,您将看到派生实例组成基实例。例如:

派生类内存:0x00001110 到 0x00001120

基类内存:0x00001114 到 0x00001118

因此,在构建时,必须先为派生类分配空间,然后才能为基类分配空间。在销毁时,必须先销毁基类,然后才能销毁派生类。

如果有以下代码:

class Base 
{
public:
    Base()
    {
        std::cout << "\n  Base created";
    }
    virtual ~Base()
    {
        std::cout << "\n  Base destroyed";
    }
}

class Derived : public Base 
{
public:
    Derived()
    // Derived is allocated here 
    // then Base constructor is called to allocate base and prepare it
    {
        std::cout << "\n  Derived created";
    }
    ~Derived()
    {
        std::cout << "\n  Derived destroyed";
    }   
    // Base destructor is called here
    // then Derived is deallocated
}

如果您创建了Derived d;并使其超出作用域,那么您将得到@Brian答案中的输出。但是内存中对象的行为实际上不是按照相同顺序进行的,更像是这样:

构造:

  1. 分配Derived

  2. 分配Base

  3. 调用Base构造函数

  4. 调用Derived构造函数

析构:

  1. 调用Derived析构函数

  2. 调用Base析构函数

  3. 释放Base

  4. 释放Derived


这是一个很好的澄清。内存首先为派生类分配,然后为基类分配,但构造函数首先为基类,然后为派生类执行。对于析构函数,最后添加到“堆栈”的是被销毁的,即派生类,然后是基类。但是释放内存的顺序相反,首先是基类,然后是派生类。 - nnrales
你知道为什么会发生这种情况吗?这对我来说很有趣。 - nnrales
@nnrales,这遵循组合关系。例如,在类中声明std::string(非指针)时,字符串也会在构造函数之前分配给组合者。派生类组成基类并将其包含在其内存空间中,因此必须先分配派生类。如果你打印地址,这将更加清楚。甚至可以通过将派生类中的“this”强制转换为基类来访问基类实例,并使用它来执行一些操作。 - Everyone

2
这在order-dtors-for-members中有明确说明。基本规则是“先构造,后析构”。
构造函数调用顺序:
  1. 基类的构造函数按出现顺序在“:”后调用
  2. 派生类成员的构造函数按出现顺序在类的构造函数之前调用
析构函数按调用构造函数的相反顺序调用。
例如:
#include <iostream>

struct base0 {  base0(){printf("%s\n", __func__);};~base0(){printf("%s\n", __func__);}; };
struct base1 { base1(){printf("%s\n", __func__);}; ~base1(){printf("%s\n", __func__);};};
struct member0 { member0(){printf("%s\n", __func__);};  ~member0(){printf("%s\n", __func__);};};
struct member1 { member1(){printf("%s\n", __func__);}; ~member1(){printf("%s\n", __func__);};};
struct local0 { local0(){printf("%s\n", __func__);}; ~local0(){printf("%s\n", __func__);}; };
struct local1 { local1(){printf("%s\n", __func__);};  ~local1(){printf("%s\n", __func__);};};
struct derived: base0, base1
{
  member0 m0_;
  member1 m1_;
  derived()
  {
    printf("%s\n", __func__);
    local0 l0;
    local1 l1;
  }
  ~derived(){printf("%s\n", __func__);};
};
int main()
{
  derived d;
}

输出:

base0
base1
member0
member1
derived
local0
local1
~local1
~local0
~derived
~member1
~member0
~base1
~base0

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