默认构造函数的构造行为

编译器在什么时候会创造默认构造函数?会如何创造构造函数?(《深度探索C++对象模型》读书笔记)

默认构造函数 (Default Constructor)

在《Effective C++》这本书有提到,C++编译器会在没有声明定义任何构造函数的情况下为 class 生成一个构造函数。在《深度探索C++对象模型》这本书里,对编译器何时生成构造函数做了更详细的讨论。(有很多词语这本书的译者都没有翻译,我觉得这个做法挺好,避免了歧义和不必要的解释。这篇笔记也仿照这种方法,一些词不做翻译,混入大量英文。)

默认构造函数可以认为有两种,一种是 trivial(浅薄而无能,没什么用的) constructor ,一种是 nontrivial constructor。如果一个类的成员全都是内建类型(built-in type),也就是没有任何成员对象,那么产生的 default constructor 就是 trivial constructor。因为由编译器产生的 default constructor 是不会自动初始化内建类型的成员的,所以这个由编译器产生的构造函数其实什么都没做。

而 nontrivial constructor 会在编译器需要的时候被编译器产生出来。对于下面这一段代码,Foo 没有构造函数,程序也需要 bar’s members 都被清零,但是编译器不会为 Foo 生成默认构造函数。因为这里的需要,指的是编译器的需要,而不是程序员的需要。程序员的需要,应该由程序员来保证,这里应该让 class Foo 的作者负责。

1
2
3
4
5
6
7
8
9
class Foo { public: int val; Foo *pnext; };

void foo_bar()
{
// 程序要求 bar's members 都被清零
Foo bar;
if (bar.val || bar.pnext)
// do something
}

有 4 种情况会产生 nontrivial constructor:

  1. 类中有一个或多个成员对象(即Member Class Object),并且它们都有 Default Constructor
  2. 派生自某个基类,并且该基类带有 Default Constructor
  3. 该类带有 Virtual Function
  4. 该类虚继承于基类(A Class with Virtual Base Class)

第 1 种情况和第 2 种情况强调了成员对象或基类都要有 Default Constructor,如果没有,是无法通过编译的。(不展开,需要理解)当然,成员对象或基类的 Default Constructor,并没有限制必须是用户声明的(user-declared),也可以是编译器生成的。

“带有 Default Constructor” 的 Member Class Object

如果 class 中有一个 member object,并且没有写任何构造函数,那么出于初始化这个 member object 的需要,编译器会生成一个 Default Constructor来初始化这个 member object。此时编译器生成的 Default Constructor 显然不是 trivial constructor,因为它确实做了一些工作,即初始化 member object。

1
2
3
4
5
6
7
class Foo { public: Foo();  };
class Bar { public: Foo foo; char *str; }

void foo_bar
{
Bar bar; // Bar::foo 是一个 member object,需要在此处被初始化
}

对于上面这个例子,编译器生成的 Default Constructor 可能看起来是这样的:

1
2
3
4
inline Bar::Bar()
{
foo.Foo::Foo();
}

生成的 Default Constructor 完成了对 foo 的初始化需要。当然,没有对 str 进行初始化,因为初始化 str 不是编译器的需要。如果程序对初始化 str 有需要,那么应该请程序员来初始化 str。

题外话:上面这一段代码中出现的 inline,不是为了什么优化。如果有多个翻译单元,编译器可能会为每个翻译单元都合成一个 Default Constructor,链接器就没法完成链接了。(不知道现代C++编译器会怎么处理,这是书上的说法)

程序确实对初始化 str 有需要,于是程序员写了如下默认构造函数:

1
Bar::Bar() { str = 0; }

现在程序的需要满足了,但是编译器的需要却没有满足。编译器还需要初始化 member object foo。由于现在已经有一个构造函数了,编译器没法再生成第二个。在这种情况下,编译器的行为是:对现有的每一个构造函数进行扩张,在其中安插一些代码,使得 user code 被执行前,完成对 member object 的初始化。

延续上一个例子,扩张后的 constructor 可能像这样:

1
2
3
4
5
Bar::Bar() 
{
foo.Foo::Foo(); // 编译器扩张的代码
str = 0; // user code
}

上面只讨论了一个 member object 的情况。如果有多个 member objects,编译器的行为是差不多的,编译器会按照声明顺序对每一个 member object 完成初始化。

“带有 Default Constructor” 的 Base Class

编译器的行为与 “带有 Default Constructor” 的 Member Class Object 类似。编译器会创造或扩张 Constructor,从而满足对 Base Class 的初始化需求。

“带有一个 Virtual Function”的 Class

如果有 virtual function,编译器会创建一个虚表(virtual function table),用于存放 class 的 virtual funtions 地址。

此外,编译器会给 class object 额外加入一个 pointer member 指向虚表。

这个时候由编译器产生的 Default Constructor 也是 nontrivial constructor,因为需要对虚表和额外加入的 pointer member 进行初始化。

“带有一个 Virtual Base Class”的 Class

“带有一个 Virtual Function”的 Class 的情况差不多,编译器为了实现运行时多态,会加入一些东西,并需要对加入的东西完成初始化。

// 未完待续…

作者

uint128.com

发布于

2020-07-19

更新于

2022-08-22

许可协议

评论