成员构造函数和析构函数的调用顺序

156

尊敬的C++大师们,我来请教你们。请用标准语言告诉我,C++是否保证以下程序:

#include <iostream>
using namespace std;

struct A
{
    A() { cout << "A::A" << endl; }
    ~A() { cout << "A::~" << endl; }
};

struct B
{
    B() { cout << "B::B" << endl; }
    ~B() { cout << "B::~" << endl; }
};

struct C
{
    C() { cout << "C::C" << endl; }
    ~C() { cout << "C::~" << endl; }
};

struct Aggregate
{
    A a;
    B b;
    C c;
};

int main()
{
    Aggregate a;
    return 0;
}

将始终产生

A::A
B::B
C::C
C::~
B::~
A::~
换句话说,成员的初始化是否保证按声明顺序进行,销毁是否按相反顺序进行?

14
当类变得庞大且难以掌控时,这是引起微妙错误的一个相当常见的原因。当您拥有50个数据成员,其中很多在构造函数初始化列表中进行初始化时,很容易假设构造顺序与初始化列表中的顺序相同。毕竟,代码编写者已经仔细排列了列表…是吗? - Permaquid
5个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
168
换句话说,成员变量是否保证以声明的顺序初始化并以相反的顺序销毁呢? 是的,两者都是如此。请参阅12.6.2。 6. 初始化按照以下顺序进行: - 首先,只有对于最派生类的构造函数(如下所述),才会按深度优先从左到右遍历基类的有向无环图中出现的虚基类,其中“从左到右”是派生类base-specifier-list中出现的基类名称的顺序。 - 然后,直接基类将按照它们在base-specifier-list中出现的声明顺序进行初始化(不管mem-initializers的顺序)。 - 然后,非静态数据成员将按照它们在类定义中声明的顺序进行初始化(同样不管mem-initializers的顺序)。 - 最后,执行构造函数体的复合语句。[注意:声明顺序被指定为确保基类和成员子对象按初始化的相反顺序销毁。—注]

2
如果我没记错的话,是的,都是这样...把它看作一个栈。最先推入的最后弹出。所以当实例化你的第一个实例时,它按照栈的顺序推入内存中。然后,第二个被推上去,第三个被推到第二个上面,依此类推。然后,在销毁你的实例时,程序将查找要销毁的第一个实例,即最后一个被推入的。但我可能解释得不对,但这是我在做C/C++和ASM时学到的方式。 - Will Marcouiller
https://isocpp.org/wiki/faq/dtors#order-dtors-for-members - bobobobo

30

是的,它们是(指非静态成员)。请参见12.6.2/5以进行初始化(构建)和12.4/6以进行销毁。


18

是的,标准保证对象被销毁的顺序与它们创建的顺序相反。原因是一个对象可能使用另一个对象,从而依赖于它。考虑以下情况:

struct A { };

struct B {
 A &a;
 B(A& a) : a(a) { }
};

int main() {
    A a;
    B b(a);
}
如果 ab 之前被析构,那么 b 将保存一个无效的成员引用。通过按照它们创建的相反顺序析构对象,我们保证正确的销毁。

我从未真正考虑过这个规则也适用于作用域成员的销毁顺序! - yano
@yano 所谓的“作用域成员”是什么? - John
在上面的例子中,main()函数中的两个栈变量ab被按照它们声明的相反顺序销毁。也许“作用域成员”不是一个真正的术语,但这是我对封装在给定作用域(在本例中是main()函数体)中的对象的理解。 - yano

8
是的,没错。对于成员变量来说,销毁顺序总是与构造顺序相反。

0
关于析构函数。这里是标准的12.4.8段,证明了第二个“是”: 在执行析构函数的主体并销毁主体内分配的任何自动对象之后,类X的析构函数调用X的直接非变异非静态数据成员的析构函数,X的直接基类的析构函数以及如果X是最派生类的类型(12.6.2),则其析构函数调用X的虚基类的析构函数。所有析构函数都被称为带限定名称的方式调用,即忽略更派生类中可能存在的任何虚覆盖析构函数。基类和成员按照它们的构造完成顺序的相反顺序进行销毁(参见12.6.2)。析构函数中的返回语句(6.6.3)可能不会直接返回给调用者;在转移控制权给调用者之前,将调用成员和基类的析构函数。数组元素的析构函数按其构造的相反顺序进行调用(参见12.6)。

请注意,容器通常不跟踪内容的时间顺序,因此它们可能表现出非直观的方式。例如,std::vector 可能会从开头到结尾销毁其对象,尽管通常它们是通过 push_back() 或类似方法填充的。 因此,您不能通过容器实现 ctor-dtor 堆栈。

这是我的小插图:

#include <vector>
#include <stdio.h>

struct c
{
        c() : num(++count) { fprintf(stderr, "ctor[%u], ", num);}
        ~c(){ fprintf(stderr, "dtor[%u], ", num);}
private:
        static unsigned count;
        unsigned num;
};
unsigned c::count = 0; 
int main()
{
        std::vector<c> v(5);
}

...然后我得到了:ctor[1],ctor[2],ctor[3],ctor[4],ctor[5],dtor[1],dtor[2],dtor[3],dtor[4],dtor[5]。


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