问题描述
我试图了解类是如何在 C++ 中编译的。因此创建不同的示例来理解初始化规则(以及书中的理论)。我的示例代码如下:
#include <iostream>
using namespace std;
class NAME {
public:
NAME(int p) : x(r) {
cout << x << endl;
}
int &x;
int r=3;
};
int main() {
NAME obj(4);
return 0;
}
我的问题是,我指定数据成员 &x
和 int r=3
的顺序重要吗?我通过颠倒这两个的顺序进行了检查,但程序仍然有效。我已经读过类的编译分两步进行:
- 编译所有成员声明。
- 然后编译函数体。
现在我的问题是根据这个规则,引用类型 x
的声明应该首先发生(因为编译是按照变量声明/定义的顺序发生的),然后应该发生 int 类型 r
。然后应该编译构造函数体。但是因为我们必须总是初始化一个引用类型,当我们没有初始化 x
时,这个程序是如何工作的?另外,我知道构造函数初始值设定项列表用于从 x
初始化 r
。我想我对这样一个事实感到困惑:在使用初始化列表时,是类内声明首先发生还是构造函数列表直接初始化成员?如果是后者,那么为什么初始化列表中的参数顺序重要吗?
PS:我知道我们已经通过初始化列表 x
初始化了 x(r)
但是 x
和 { 的类内定义/声明会发生什么{1}}?当存在初始化列表时,它们会被忽略吗?这一切以什么顺序发生?
解决方法
示例代码是有效的,虽然模式有点危险,类似的代码可能有未定义的行为。
类成员声明然后编译定义并不完全准确,或者至少在您解释它的意义上不准确。确实,类声明中的函数体和对象成员初始值设定项可以使用同一类的其他成员的名称,即使它们尚未声明。但这条规则只影响名称查找,不影响对象生存期或初始化的规则。编译器究竟如何以及何时完成所有这些工作,我们通常不需要担心理解代码会做什么。
对您的一个段落的相关术语更正:
声明 初始化的引用类型x
应该首先发生(因为编译 初始化发生按变量声明/定义的顺序),然后 int
type r
应该发生。然后构造函数体应该编译 执行。
这确实会导致某些情况,例如在您的示例中,可以在成员初始化之前使用该成员,因此该成员的“生命周期”尚未开始。在示例中,obj.x
在 obj.r
之前被初始化。这种情况需要小心处理,因为对象生命周期之外的大多数操作都是未定义的行为。 (有关详细信息,请参阅标准段落 [basic.life]/7。)但只允许绑定对此类对象的引用。当然,那么大多数使用该引用的操作也是未定义行为,直到被引用的对象实际初始化为止。
当我们使用初始化列表时,是先进行类内声明还是构造函数列表直接初始化成员?
每当对象初始化使用带有 mem-initializer-list 的构造函数时,该构造函数指定如何初始化给定成员,该同一成员上的任何类内初始化程序都将被完全忽略。因此,成员 =3
声明中的 r
在您的示例中没有任何作用。其他构造函数仍然可以省略 r
的初始化,并且它们将使用 =3
。如果您添加了默认构造函数 NAME() = default;
,这也会使用它,因为默认构造函数的默认定义就像根本不使用 mem-initializer-list。