关于C++中结构体构造函数和析构函数的行为

11

我不明白为什么这个程序的输出结果是这样的。为什么没有编译错误?我认为当尝试构造B时,编译器会找不到名为foo()的函数并报告一个错误。

#include <iostream>
using namespace std;

struct A{
    int a;
    A(int i=0) : a(i) { cout << "A" << endl; }
    ~A() { cout << "Bye A" << endl; }
    int foo() { return a; }
};
struct B{
    int b;
    B(int i=0) : b(i) { cout << "B" << endl; }
    ~B() { cout << "Bye B" << endl; }
    int bar() { return b; }
};
struct C : B, A {
    C(int i=0) : B(foo()), A(i) {}
};

int main() {
    cout << C(10).bar() << endl;
    return 0;
}

输出:

B
A
0
Bye A
Bye B

通常情况下,我想知道在存在多重继承时,父结构体的构造和初始化顺序是什么?在类中可以期待类似的行为吗?

对于构造函数和析构函数调用顺序的任何解释都将不胜感激。

注意:这不是作业。我已经查找了类似的主题,但没有关于这个问题的结果。


1
顺便说一下,在C++中严格来说没有结构体,只有类。(struct也声明了一个类。)因此,我希望它们的行为方式相同。 - HolyBlackCat
1
我期望类的行为与结构体完全相同。两者之间的主要区别在于结构体的成员始终是公共的。 - TeaMonkie
2
fooC可访问、明确的基类成员,那么问题在哪里呢? - Kerrek SB
3
@Mattia说"结构体的成员总是公共的"并不正确,只是结构体默认的可见性是public,但你也可以在其中指定protected和/或private的内容。 - roalz
2
@KerrekSB: 问题在于在 A::A 之前调用 A::foo。这将返回一个未初始化的 A::a - MSalters
显示剩余4条评论
3个回答

19

未定义行为

在对象完全初始化之前调用foo将会导致未定义的行为。引用自C++标准中的12.6.2:

成员函数(包括虚成员函数,10.3)可以在正在构造的对象上调用。 同样,在typeid运算符(5.2.8)或dynamic_cast(5.2.7)的操作数中也可以使用一个正在构建的对象。然而,如果这些操作在ctor-initializer(或在直接或间接从ctor-initializer调用的函数中)在所有基类的mem-initializers完成之前执行,则操作的结果是未定义的。 [例如:

class A {
public:
  A(int);
};

class B : public A {
  int j;
public:
  int f();
  B() : A(f()),       // undefined: calls member function
                      // but base A not yet initialized
          j(f()) { }  // well-defined: bases are all initialized
};

class C {
public:
  C(int);
};

class D : public B, C {
  int i;
public:
  D() : C(f()),       // undefined: calls member function
                      // but base C not yet initialized
          i(f()) { }  // well-defined: bases are all initialized
};

—— 结束示例 ]

换句话说,根据标准,这样做是可以的:

C(int i=0) : B(), A(i) {
    B::b = foo();
}

并且这将打印10而不是你得到的0(由于这是未定义的行为,因此可能是任何其他值)。

初始化顺序

撇开这个未定义行为的问题,回答您的问题,初始化发生的顺序是明确定义的:

在非委托构造函数中,初始化按照以下顺序进行:

— 首先,并且仅对于最派生类(1.8)的构造函数,在基类的有向无环图的深度优先从左到右遍历中以它们出现的顺序初始化虚拟基类,其中“从左到右”是基类在派生类的base-specifier-list中出现的顺序。

— 然后,直接基类按照它们在base-specifier-list中出现的声明顺序初始化(不考虑mem-initializers的顺序)。

— 然后,非静态数据成员按照它们在类定义中声明的顺序初始化(同样不考虑mem-initializers的顺序)。

— 最后,执行构造函数体的compound-statement

[ 注意: 声明顺序是为了确保基础子对象和成员子对象按初始化的相反顺序销毁。—end note]

所以,在您的代码中,初始化顺序是:BB::b),AA::a),C()。

如下面的评论中所述,但是改变此初始化顺序(例如使用struct C : A, B而不是struct C : B, A)将无法消除未定义的行为。在B部分初始化之前调用A::foo仍然是未定义的,即使A部分已经被初始化。


然而,问题是关于基类成员函数的。它基本上是相同的,但值得一提的是,对于 struct C : A, B 是否会出现相同的 UB(我认为这时是合法的,首先构造 A,然后构造 B)。 - grek40
我的意思是:“但是,如果这些操作在 ctor-initializer 中执行[...] 在所有基类的 mem-initializers 完成之前”- 这里的 mem-initializers 是针对谁的 - 成员函数上下文还是具体类型上下文? - grek40
1
@grek40:我加入了标准中说明这一点的例子。它明确指出,在初始化 DC 部分之前调用 B::f 是未定义行为(尽管此时 B 部分已被初始化)。虽然你可能是对的,大多数编译器会以你预期的方式处理它,但标准的原意并不保证如此。 - Sander De Dycker
1
@SJHowe:我不知道为什么标准将该用例视为未定义,但根据我的阅读,它确实如此。访问A::a在其初始化后(当您将其更改为struct C:A,B时)似乎是直观的应该是明确定义的。但似乎不是这样。因此,要么这是一个疏忽(或可能更有可能-简化),要么就是我看不到的原因。无论哪种方式,最好不要依赖于任何关于此的行为。 - Sander De Dycker
1
@SanderDeDycker:我认为这是一种疏忽,由于专注于虚拟情况而引起;请参见CWG2056 - Davis Herring
显示剩余6条评论

5

这只是另一种未定义行为的情况。例如,我的系统会给出以下结果。

B
A
-858993460
Bye A
Bye B

尝试这个 实时演示,它产生了另一个不同的结果(C(10).bar() 产生了32764)。
在这个上下文中可以调用foo(),但它会在A的构造函数之前被调用。这意味着a已经被初始化,这导致读取一个未初始化的变量,进而导致未定义行为。这类似于在它初始化之前访问成员。请考虑以下示例。a被初始化为b的值,然后初始化了b。问题显然是,在读取b以初始化a的位置,b尚未初始化。
struct foo
{
    foo(int x) : a(b), b(x) {}
    int a;
    int b;
};

int main()
{
    foo bar(10);
}

0

我想看看实际发生了什么。既然我有结果,现在就给你们看看。

#include <iostream>
using namespace std;

/*** Structures ***/
struct A{
    int a;
#warning a integer declared in struct A

    A(int i=0) : a(i)
#warning A constuctor declared
#warning A constructor creates variable i, sets it to 0
#warning then assigns i to a
    {
        cout << "inside constructor A" << endl;
        cout << "i = " << i << endl;
        cout << "a = " << a << endl;
        cout << "leaving constructor A" << endl;
    }
#warning A constructor definition provided
    ~A()
#warning A destructor declared
    {
        cout << "inside destructor A" << endl;
        cout << "leaving destructor A" << endl;
    }
#warning A destructor definition provided
    int foo()
#warning foo function declared in struct A
    {
        cout << "inside foo, inside A" << endl;
        cout << "foo will return a = " << a << " then leave foo" << endl;
        return a;
    }
#warning foo function defined in struct A
};

struct B{
    int b;
#warning b integer declared in struct B
    B(int i=0) : b(i)
#warning B constructor declared
#warning B creates int i and initializes it to 0
#warning b is assigned the value of i                 
    {
        cout << "inside constructor B" << endl;
        cout << "i = " << i << endl;
        cout << "b = " << b << endl;
        cout << "leaving constructor B" << endl;
    }
#warning B constructor defined
    ~B()
#warning B destructor declared
    {
        cout << "inside destructor B" << endl;
        cout << "leaving destructor B" << endl;
    }
#warning B destructor defined    
    int bar()
#warning bar function declared in struct B
    {
        cout << "inside bar, inside B" << endl;
        cout << "bar will return b = " << b << " then leave bar" << endl;
        return b;
    }
#warning bar function defined in struct B
};


struct C : B, A 
#warning C structure declared derived from B and A
{
    C(int i=0) : B(foo()), A(i) 
#warning C constructor declared
#warning C constructor creates int i and assigns value 0
#warning C constructor instantiates B and calls foo from A to assign value?
#warning C constructor instantiates A by assigning i to it
    {
        cout << "inside constructor C" << endl;
        cout << "i = " << i << endl;
        cout << "leaving constructor C" << endl;
    }
#warning C constructor defined with no implementation
};

int main() {
    cout << "command is: print the value of C(10).bar()" << endl;
    cout << C(10).bar() << endl;
#warning initialize C with a value of 10
#warning then call the bar function extended from B

#warning declare struct C blah, initialized with C(12)
    cout << endl << "creating struct blah with definition C(12)" << endl;
    struct C blah = C(12);
    cout << "calling blah.foo" << endl;
    cout << blah.foo() << endl;
    cout << "calling blah.bar" << endl;
    cout << blah.bar() << endl;
#warning  printing and then returning 0
    cout << endl << "Some random output before returning 0" << endl;
    return 0;
}

在编译过程中会产生以下结果(稍微整理了一下):
>make
test.cpp:7:2:  warning: #warning a integer declared in struct A [-Wcpp]
test.cpp:10:2: warning: #warning A constuctor declared [-Wcpp]
test.cpp:11:2: warning: #warning A constructor creates variable i, sets it to 0 [-Wcpp]
test.cpp:12:2: warning: #warning then assigns i to a [-Wcpp]
test.cpp:19:2: warning: #warning A constructor definition provided [-Wcpp]
test.cpp:21:2: warning: #warning A destructor declared [-Wcpp]
test.cpp:26:2: warning: #warning A destructor definition provided [-Wcpp]
test.cpp:28:2: warning: #warning foo function declared in struct A [-Wcpp]
test.cpp:34:2: warning: #warning foo function defined in struct A [-Wcpp]
test.cpp:39:2: warning: #warning b integer declared in struct B [-Wcpp]
test.cpp:41:2: warning: #warning B constructor declared [-Wcpp]
test.cpp:42:2: warning: #warning B creates int i and initializes it to 0 [-Wcpp]
test.cpp:43:2: warning: #warning b is assigned the value of i [-Wcpp]
test.cpp:50:2: warning: #warning B constructor defined [-Wcpp]
test.cpp:52:2: warning: #warning B destructor declared [-Wcpp]
test.cpp:57:2: warning: #warning B destructor defined [-Wcpp]
test.cpp:59:2: warning: #warning bar function declared in struct B [-Wcpp]
test.cpp:65:2: warning: #warning bar function defined in struct B [-Wcpp]
test.cpp:70:2: warning: #warning C structure declared derived from B and A [-Wcpp]
test.cpp:73:2: warning: #warning C constructor declared [-Wcpp]
test.cpp:74:2: warning: #warning C constructor creates int i and assigns value 0 [-Wcpp]
test.cpp:75:2: warning: #warning C constructor instantiates B and calls foo from A to assign value? [-Wcpp]
test.cpp:76:2: warning: #warning C constructor instantiates A by assigning i to it [-Wcpp]
test.cpp:82:2: warning: #warning C constructor defined with no implementation [-Wcpp]
test.cpp:88:2: warning: #warning initialize C with a value of 10 [-Wcpp]
test.cpp:89:2: warning: #warning then call the bar function extended from B [-Wcpp]
test.cpp:91:2: warning: #warning declare struct C blah, initialized with C(12) [-Wcpp]
test.cpp:98:2: warning: #warning printing and then returning 0 [-Wcpp]

并提供以下输出:

>test
command is: print the value of C(10).bar()
inside foo, inside A
foo will return a = 4201198 then leave foo
inside constructor B
i = 4201198
b = 4201198
leaving constructor B
inside constructor A
i = 10
a = 10
leaving constructor A
inside constructor C
i = 10
leaving constructor C
inside bar, inside B
bar will return b = 4201198 then leave bar
4201198
inside destructor A
leaving destructor A
inside destructor B
leaving destructor B

creating struct blah with definition C(12)
inside foo, inside A
foo will return a = 4201104 then leave foo
inside constructor B
i = 4201104
b = 4201104
leaving constructor B
inside constructor A
i = 12
a = 12
leaving constructor A
inside constructor C
i = 12
leaving constructor C
calling blah.foo
inside foo, inside A
foo will return a = 12 then leave foo
12
calling blah.bar
inside bar, inside B
bar will return b = 4201104 then leave bar
4201104

Some random output before returning 0
inside destructor A
leaving destructor A
inside destructor B
leaving destructor B

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