一点帮助理解C++代码的流程执行

4

我正在阅读来自网站The C++ Programming LanguageC++ Super-FAQ关于构造函数初始化顺序的内容。以下代码被呈现出来:

#include <iostream>
class Y {
public:
  Y();
  void f();
};
Y::Y()      { std::cout << "Initializing Y\n"; }
void Y::f() { std::cout << "Using Y\n"; }
class X {
public:
  X(Y& y);
};
X::X(Y& y) { y.f(); }
class Z {
public:
  Z();
protected:
  X x_;
  Y y_;
};
Z::Z() 
  : y_()
  , x_(y_)
{ }
int main()
{
  Z z;
  return 0;
}

这段代码打印的顺序是:

使用 Y

初始化 Y

我无法理解为什么打印顺序会是这样,因为在类Z的构造函数中,先实例化了Y类的实例y_,然后才实例化X类的实例x_。换句话说,如果要先使用方法Y :: f(),则需要先实例化一个Y,它肯定会调用其构造函数和打印例程 std::cout <<“ Initializing Y \n”;

2个回答

4

class Z 的定义中,X x_ 出现在 Y y_ 之前,因此 x_ 首先被构造和初始化。无论您将成员放置在初始化程序列表中的顺序如何,Z::Z() : y_(), x_(y_) {},x_都会首先初始化。

由于 y_ 的构造函数尚未被调用,某些内部元素(如虚表)可能尚未初始化。实际上,在 X 的构造函数中使用此对象可能导致段错误。也许这个定义对您更好。

class Z {
public:
  Z();
protected:
  Y y_;
  X x_;
};

由于y_x_之前定义,所以y_将首先被调用,您可以在x_的构造函数中安全地使用它。
您可能希望阅读本文的初始化顺序部分。
编辑: C++重视速度并信任程序员,因此不会尝试验证传递给它的参数。您可以将空指针转换为引用,然后使用该引用。
Y* p = 0;
X x(reinterpret_cast<Y&>(p));

如果编译器不需要解引用我们的空指针,则此代码将成功编译并运行。如果您将f定义为虚函数,或者尝试访问任何成员变量,则这将导致段错误。


如果x_先被构造和初始化,但是x没有默认构造函数。 - user1438832
这种混淆实际上是导致gcc在初始化列表中不遵守顺序时打印警告的原因。 - koalo
它不使用默认构造函数,而是使用您在初始化列表中指定的构造函数。它只是不按照您编写的顺序执行,而是按照定义的顺序执行。 - Owen Delahoy
我理解你的观点。然而,不知何故,在初始化过程中调用了方法 **Y::f()**,但没有类 Y 的实例与之对应,这本不可能发生。我说得对吗?那么,这是怎么发生的呢? - Randerson
1
传递给 f 的对象本质上是垃圾。该对象已被分配但未初始化。使用 Y 的成员、子类或虚函数表将不会被初始化,并且会导致未定义的行为,但在此示例中您不使用它们。 - Owen Delahoy

2

您的类Z按照以下顺序声明了两个成员:

X x_;
Y y_;

然而,它们是按相反顺序初始化的:

Z::Z()
    : y_()
    , x_(y_)
{ }

您需要注意这一点,例如请参见此问题此问题。更改声明和初始化的顺序以匹配可以得到您期望的结果。如果顺序不匹配,则会出现未定义的行为,并且某些编译器将警告您有关顺序不匹配的信息。您链接的特定帖子在评论中说:"// Bad: should have listed x_ before y_"并强调了这一点。

请注意,在初始化之前使用了y_(Y::f())。


我认为无序的初始化列表不会导致未定义的行为。 - Owen Delahoy
我发布的问题之一声称它是正确的。我会到其他地方检查。 - doctorlove
但问题就在这里。方法(Y::f())在没有Y的实例的情况下被调用,而程序并没有崩溃。这种行为真的很奇怪。 - Randerson

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