【深度探索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

// ...

}

在这个例子中,正确的程序语意是要求 Foo 要有一个 default constructor,可以将它的两个 members 初始化为 0。这段代码是否符合上面的 ARM 所说的“在需要的时候”?答案是:NO! 其间的差别在于一个是程序的需要,一个是编译器的需要。程序要如果需要,那是程序员的责任!!!在上面这个例子中,就是
class Foo 的负责人。

是的,上面的代码并不会合成一个 默认构造函数(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 意指可以“无需任何参数就被调用”者。这样的一个构造函数,如果不是没有任何参数,就是每个参数都有默认值。

(第二章内容较多,本次先更新到这里,下一篇文章里,将会看到拷贝构造函数的建构操作,以及构造函数的初始化列表等等。敬请期待。)

【深度探索C++对象模型】第二章 构造函数语意学(上),布布扣,bubuko.com

时间: 2024-07-30 10:19:21

【深度探索C++对象模型】第二章 构造函数语意学(上)的相关文章

深度探索C++对象模型 第二章构造函数语意学

在使用C++时,常常会好奇或者抱怨,编译器为我们做了什么事呢? 为什么构造函数没有为我初始化呢?为什么我还要写默认构造函数呢? 2.1 Default Constructor 的构造操作 如果没有声明默认构造函数,编译器会在需要的时候帮我们产生出来. 为了避免在多个地方被需要导致重复,则编译器将产生的构造函数声明为inline方式. class Foo {public:Foo(), Foo(int) }; class Bar {public: Foo foo;char *str;} Bar ba

《深度探索c++对象模型》chapter3 Data语意学

一个空的class:如 class X{} ; sizeof(X)==1; sizeof为什么为1,他有一个隐晦的1 byte,那是被编译器安插进去的一个char,这使得class2的两个objects得以在内存中配置独一无二的地址: X a,b; if(&a==&b) cerr<<"yipes!"<<endl; class X{}; class Y:public virtual X{}; class Z:public virtual X{};

Android深度探索读书笔记 第二章

第二章介绍的是搭建android底层的开发环境 ,主要包括android应用程序开发环境.android NDK开发环境和交叉编译环境的搭建.首先介绍的是开发测试和调试Linux驱动.HAl程序需要的工具:JDK6或以上版本:Eclipse3.4或以上版本:ADT:CDT:androidSDK:Android NDk:交叉编译环境:Linux内核源代码:android源代码:用于调试的串口工具 minicom.接着介绍的是JDK的安装:首先从官方网站(http://www.oracle.com/

《深度探索C++对象模型》第二章 | 构造函数语意学

默认构造函数的构建操作 默认构造函数在需要的时候被编译器合成出来.这里"在需要的时候"指的是编译器需要的时候. 带有默认构造函数的成员对象 如果一个类没有任何构造函数,但是它包含一个成员对象,该成员对象拥有默认构造函数,那么这个类的隐式默认构造函数就是非平凡的,编译器需要为该类合成默认构造函数.为了避免合成出多个默认构造函数,编译器会把合成的默认构造函数.拷贝构造函数.析构函数和赋值拷贝操作符都以内联的方式完成.一个内联含有具有静态链接,不会被文件以外者看到.如果函数不适合做成内联,就

【C++】深度探索C++对象模型读书笔记--Data语意学(The Semantics of data)

1. 一个空类的大小是1 byte.这是为了让这一类的两个对象得以在内存中配置独一无二的地址. 2. Nonstatic data member 放置的是“个别的class object”感兴趣的数据,static data members则放置的是“整个class”感兴趣的数据. 3. C++对象模型把nonstatic data members直接放在每一个classs object之中.对于继承而来的nonstatic data members(不管是virtual 还是nonvirtua

【C++】深度探索C++对象模型读书笔记--执行期语意学(Runtime Semantics)

对象的构造和析构: 全局对象 C++程序中所有的global objects都被放置在程序的data segment中.如果显式指定给它一个值,此object将以此值为初值.否则object所配置到的内容为0. 如果全局对象如果有构造函数或析构函数的话,我们说它需要静态的初始化操作和内存释放操作.编译器的执行步骤如下: 1.为每一个需要静态初始化的文件产生一个_sti()函数,内含必要的构造函数调用操作. 2. 在每一个需要静态的内存释放操作的文件中,产生一个_std()函数,内含必要的析构操作

Android深度探索-卷1 第二章

本章主要介绍如何搭建Android 底层开发的环境,主要包括Android应用程序开发环境,Android NDK 开发环境和交叉编译环境的搭建. 开发.测试和调试Linux驱动.HAL程序库需要的工具:jdk6或以上版本.eclipse3.4或以上版本 ADT, CDT ,Android SDK ,Android NDK, 交叉编译环境.Linux内核源代码.Android源代码.用于调试开发板的串口工具:minicom. 安装JDK 1.从官方网站下载JDK压缩包(tar.gz)(http:

【C++】深度探索C++对象模型读书笔记--构造函数语义学(The Semantics of constructors)(四)

成员们的初始化队伍(member Initia 有四种情况必须使用member initialization list: 1. 当初始化一个reference member时: 2. 当初始化一个const member时: 3. 当调用一个base class的constructor,而它拥有一组参数时: 4.当调用一个member class的constructor,而它拥有一组参数时: 在这四种情况下,程序可以被正确编译运行,但是效率不高.例如: 1 class Word { 2 Stri

深度探索c++对象模型 第一章

1,声明与定义. //声明式如下: extern int x;   //对象式(变量式)声明 std::size_t numDigits(int number);  //函数式声明 class Widget;    //类声明 template<typename T>    //模板类声明 class GraphNode; //定义式如下: int x;    //对象的定义 std::size_t numDigits(int number)    //函数的定义 { ... } class