首页 > 代码库 > 【深度探索C++对象模型】第二章 构造函数语意学(上)
【深度探索C++对象模型】第二章 构造函数语意学(上)
第二章 构造函数语意学(The Semantics of Constructors)
—— 本书作者:Stanley B.Lippman
一、前言
首先让我们来梳理一个概念:
- 默认构造函数(Default Constructor) : 是在没有显示提供初始化式时调用的构造函数。它由不带任何参数的构造函数,或是为所有形参提供默认实参的构造函数定义。如果定义的某个类的成员变量没有提供显示的初始化式时,就会调用默认构造函数(Default Contructor)。
如果用户的类里面,没有显示的定义任何构造函数,编译器就会为该类合成一个默认构造函数。这种说法对吗?
在 C++ Annotated Reference Manual (ARM)[ELLIS90] 中的 Section 12.1 告诉我们:"default contructors ... 在需要的时候被编译器产生出来"。关键字眼是"在需要的时候"。被谁需要?何时需要?请看下面这段代码:
class Foo { public: int val; Foo *pNext; }; //... void foo_bar() { Foo bar; // 这里程序要求 bar‘s data members 都被清为 0 if ( bar.val || bar.pNext ) // do something // ... } |
是的,上面的代码并不会合成一个 默认构造函数(default constructor)。
【注】Global objects 的内存保证会在程序激活时被清 0。Local objects 配置于程序的堆栈中,heap objects 配置于自由空间中,都不一定会被清为 0,它们的内容将是内存上次被使用后留下的痕迹。 【再注】上面这一段是原书中的注释,我现在对这翻译疑惑的是,heap objects 不就是配置于堆中吗?local objects 不就是配置于栈中吗? |
那么,在什么时候才会合成一个 默认构造函数 呢? 当编译器需要的时候!而且,被合成出来的 contructor 只执行编译器所需的行动。我们慢慢来理解这句话。
C++ Standard 已经修改了 C++ Annotated Reference Manual (ARM)[ELLIS90] 的说法,虽然其行为事实上仍然是相同的:
对于 class X,如果没有任何 用户声明的构造函数( user-declared constructor ),那么会有一个 默认构造函数(default constructor) 被暗中( implicitly ) 声明出来……一个被暗中声明出来的 默认构造函数 将是一个 trivial constructor(没啥用的构造函数)。 |
说实话,上面这段话我没有理解,如果大家有更好的解释,请在评论里告诉我。接下来,我们看一下,在什么情况下,编译器会合成出一个有用的(nontrivial)默认构造函数。
二、构造函数的建构
- "带有 Default Constructor" 的 Member Class Object
如果一个 class 没有任何 constructor,但它内含一个 成员对象(member class object),而这个成员对象有 默认构造函数,那么这个 class 需要一个 有意义的构造函数(Nontrivial default constructor ) 就需要编译器为其合成出来。
在这里,我们要牢记,编译器合成的构造函数只满足编译器的需要。来看个例子:
class Foo { public: Foo(); // 默认构造函数 }; class Bar { public: // 缺少默认构造函数 Foo foo; // Bar 内含了一个 带有默认构造函数的 class : Foo char *str; }; // 测试 void foo_bar() { Bar bar; // 编译器需要 Bar::foo 在这里初始化 if ( str ) // ... } |
被合成出来的构造函数,只会调用 其内含的 class Foo 的默认构造函数来处理 Bar::foo,但并不会产生任何代码来初始化 Bar::str。将 Bar::foo 初始化是编译器的责任,而 Bar::str 则是程序员的责任!这点要牢记。
如果 Bar 已经有一个默认的构造函数了,如下:
class Foo { public: Foo(); // 默认构造函数 }; class Bar { public: Bar() { str = 0; } // 默认构造函数,初始化了 str,但是没有初始化 foo Foo foo; // Bar 内含了一个 带有默认构造函数的 class : Foo char *str; }; |
如上面这个例子,Bar 的构造函数还是不满足编译器需要,因此,编译器会再合成一个构造函数?答案是NO。
“如果 class A 内含一个或多个成员对象(member class objects),那 class A 的每一个构造函数 必须调用每一个 成员对象的默认构造函数(default constructor), 调用顺序依照这些 成员对象 在 class A 中的声明次序。”
编译器会扩张已存在构造函数,具体的做法是,在用户代码之前安插相应代码以满足编译器需要。
被编译器扩张后的构造函数看起来可能如下:
Bar() { // 编译器安插的代码 foo(); // 原书的写法是:foo.Foo::Foo(); // 用户代码 str = 0; } |
- “带有 Default Constructor”的 Base Class
如果一个没有任何 构造函数 的 class 派生子一个“带有 默认构造函数”的基类,那编译器会为这个 class 合成默认构造函数。它将调用上一层 base classes 的 默认构造函数(根据派生链的顺序,自上而下的调用基类构造函数)。
如果这个 class 有多个 构造函数,但其中都没有 默认构造函数,编译器则会扩张每一个构造函数,用以“调用所有必要的 默认构造函数”。注意,编译器所做的是扩张,而不是合成一个新的构造函数。
- “带有一个 Virtual Function”的 Class
一下两种情况,也需要合成出 默认构造函数。
1. class 声明(或继承)一个 virtual function。
2. class 派生自一个继承串链,其中有一个或更多的 virtual base classes。
在第一章中,我们了解了 c++ 对象模型是如何支持 virtual 的。编译器必须为每一个 object 的 vptr 设定初值,使其指向相关的 vtbl。对于 class 所定义的每一个构造函数,编译器会安插一些代码来做这样的事情,对于没有任何构造函数的 class,编译器会为他们合成一个默认的构造函数,以便初始化类的每一个 object 的vptr。
- “带有一个 Virtual Base Class”的 Class
上面一段话已经提过,这里单独说一点:Virtual base class 的实现方法在不同编译器之间存在差异,但他们的共同点在于必须使 virtual base class 在其每一个 derived class object 中的位置,能够于执行期准备妥当。
【小结】 C++ 新手一般有两个常见的误解: 1. 任何 class 如果没有定义 默认构造函数( default constructor ) ,就会被合成出一个来。 2. 编译器合成出来的 默认构造函数( default constructor ) 会明确设定“class 内每一个 成员变量 的默认值”。 实际上,只有四种情况会导致“编译器必须为没有任何构造函数的 类 合成一个 默认构造函数”。被合成出来的 默认构造函数只满足编译器需要,而不满足程序需要。 至于没有上述四种情况同时又没有任何构造函数的类,我们说它们拥有的是 implicit trivial default constructors(隐式的,无用的默认构造函数),它们实际上并不会被合成出来。 |
【引申】 在 Effective C++ 中,对 默认构造函数(default constructors) 的介绍如下: Default constructor 意指可以“无需任何参数就被调用”者。这样的一个构造函数,如果不是没有任何参数,就是每个参数都有默认值。 |
(第二章内容较多,本次先更新到这里,下一篇文章里,将会看到拷贝构造函数的建构操作,以及构造函数的初始化列表等等。敬请期待。)
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。