一、default constructor的构造操作

先看一个小例子:

class Foo { public: int val; Foo *pnext; };

此处正确的程序语意是要求Foo有一个默认构造函数,能将它的两个成员初始化为0,那么编译器会为我们合成这样的默认构造函数吗?答案是不会!(事实上,这里合成的默认构造函数是trival的构造函数,也就是它什么也不干,连初始化也不干。在概念上我们把这种构造函数叫做implicit trival default constructors,但实际上这种构造函数根本不会被合成出来)。这是因为初始化成员是程序的需要,而不是编译器的需要,本例中要承担成员初始化责任的是设计者。

在四种情况下,non-trival default constructor会被编译器合成出来,下面一一讨论这四种情况。

1. 带有Default constructor的Member Class Object

如果一个class没有任何constructor,但它内含一个member object,而后者有default constructor,那么编译器就需要为该class合成出一个default constructor,不过这个合成操作只有在constructor真正需要被调用时才会发生。

这就引出一个问题,在C++的各个不同的文件中,编译器如何避免合成出多个default constructor?解决办法是把合成的default constructor、copy constructor、deconstructor,copy assignment operator都以inline方式完成,一个inline函数具有静态链接,不会被文件以外者看到。如果函数太复杂不适合做成inline,就会合成出一个explicit non-inline static实例。(这一段没太明白)

下面是一个小例子:

class Foo { public: Foo(), Foo(int) ... };
class Bar { public: Foo foo; char *str; };void foo_bar()
{Bar bar;   //Bar::foo在这里必须被初始化if( str ) { }...
}

在本例中,编译器将为class Bar合成一个default constructor,内含必要的代码,能够调用class Foo的default constructor来处理member object Bar::foo,但是要注意,这个default constructor依然不会负责str的初始化!

但是如果class Bar已经有了构造函数,那么编译器会怎么做呢?由于Bar有构造函数,所以编译器不能再为class Bar合成一个默认构造函数。编译器的做法是:如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member class的default constructor。编译器会扩张已存在的constructors,在其中安插一些代码,使得user code被执行之前,先调用必要的default constructors。值得注意的是,调用的default constructor的顺序和member objects在class中的声明顺序相同。看下面这个例子:

class Dopey { public: Dopey(); ... };
class Sneezy { public: Sneezy( int ); Sneezy(); ... };
class Bashful { public: Bashful(); ... };class Snow_White{
public:Dopey dopey;Sneezy sneezy;Bashful bashful;
private:int mumble;
};// Snow_White 包含dopey,sneezy,bashful三个类的对象成员

假设class Snow_white有这样的构造函数:

Snow_White::Snow_White() : sneezy( 1024 )
{mumble = 2048;
}

那么它会被编译器扩张为:

Snow_White::Snow_White() : sneezy( 1024 )
{//插入member class objectsdopey.Dopey::Dopey();sneezy.Sneezy::Sneezy(1024);bashful.Bashful::Bashful();mumble = 2048;
}

值得注意的是,以上这个例子除了体现了上面的知识点外,还能看到sneezy既存在于初始化列表中,又存在于隐式构造函数的调用中,那么这两者之间有什么联系吗?这个问题将在后面的内容中回答。

2. “带有Default Constructor”的Base Class

这里默认构造函数合成的逻辑与1中很相似,因为class里面包含一个类的对象成员和class里面有一个base class的part这两种情况在内存布局上就很相似。

这里需要注意的是,如果一个class里面既有基类的部分也有类的对象成员,那么先调用基类的默认构造函数,再调用类的对象成员的默认构造函数。

3. “带有一个Virtual Function”的Class

假设一个class声明(或继承)了一个virtual function,那么以下两个扩张行动会在编译期间发生:(1) 一个virtual function table会被编译器产生出来,里面包含class的virtual functions地址。(2) 在每一个class object中,一个额外的vptr会被编译器合成出来,指向相关的class vtbl的地址。

所以编译器必须为继承体系中的每一个类的对象中的vptr设定初值。如果class定义了构造函数,那么编译器会在构造函数中安插代码做这些事情;如果class没有定义构造函数,那么编译器就会生成一个默认构造函数,用以正确地初始化每一个class object的vptr。

4. “带有一个Virtual Base Class”的Class(这一部分理解起来有点tricky)

(这一部分对构造函数影响的逻辑和3有点类似,都是因为class具有某些特性,所以编译器必须为我们多做一些事情,要么是在已有的构造函数中安插代码,要么是为我们合成一个默认构造函数。至于这个特性到底是什么,编译器需要为我们做些什么,可以抽象出来考虑。以下就来聊一聊,对于有虚基类的class,编译器需要额外做点什么。)

virtual base class的实现法在不同编译器之间有着极大的差异。然而,每一种实现法的共同点在于必须使virtual base class在其每一个derived class object中的位置,能够于执行期准备妥当。看以下这个例子:

class X { public: int i; };
class A : public virtual X { public: int j; };
class B : public virtual X { public: double d; };
class C : public A, public B { public: int k; };void foo( const A* pa ) { pa->i = 1024 };

编译器在编译期间无法确定经由pa存取的X::i的实际偏移位置,因为pa的实际类型可以改变,所以编译器必须改变执行存取操作的那些代码,从而使X::i延迟到执行期再确定下来。

二、Copy Constructor的构造操作

什么时候会调用一个类的拷贝构造函数呢?当使用一个类的对象对另一个类的对象进行初始化时。

1. Default Memberwise Initialization

当class object以相同class的另一个object作为初值,且class没有提供一个显式的拷贝构造函数时,其内部是以所谓的“逐成员初始化”来完成的,也就是把每一个内建的或派生的data member的值,从一个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的形式实行“逐成员初始化”。

合成默认拷贝构造函数的逻辑和合成默认构造函数的逻辑是类似的,也分为trivial和non-trivial两种,像只有逐成员初始化这种就是trivial的,只有non-trivial的实例才会被合成于程序之中。那么怎么判断是否trivial的情况呢?这取决于class是否展现出所谓的“bitwise copy semantics(位逐次拷贝)”

2. Bitwise Copy Semantics

在以下4中情况下,一个class不展现出“bitwise copy semantics”

(1) 当class内含一个member object而后者的class声明有一个copy constructor时,不论是被class设计者显式地声明还是被编译器合成(也就是说,递归地执行逐成员初始化这种情况也是non-trivial的?)

(2) 当class继承自一个base class而后者存在一个copy constructor(再次强调,无论是显式声明还是被编译器合成)

(3) 当class声明了一个或多个virtual function时

(4) 当class派生自一个继承串链,其中有一个或多个virtual base class时

接下来重点讨论情况(3)和(4)

3. 重新设定virtual table的指针

如果编译器对于每一个新产生的class object的vptr不能成功且正确地设好初值,将导致可怕的后果,所以当编译器导入一个vptr到class之中时,class就不再呈现出bitwise semantics了。

所以在这种情况下,编译器会合成default copy constructor,指定object的vptr指向正确class的virtual table。因为copy constructor的参数是一个引用,所以也可能是派生类的类型。

4. 处理Virtual Base Class Subobject

一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使bitwise semantics失效。

当用相同class的object初始化另一个object时,bitwise copy是足够使用的,但是如果使用派生类的object初始化基类的object时,编译器就必须合成一个copy constructor,安插一些代码以设定virtual base class pointer/offset 的初值,对每一个member执行必要的memberwise初始化,以及执行其他的内存相关工作。

这里还存在一个有趣的问题:当一个初始化操作存在并保持着bitwise copy semantics时,编译器是否应该抑制copy constructor的调用?下一个部分会专门讨论一下这个问题。

三、程序转化语意学(Program Transformation Semantics)

(这部分内容不做总结)

四、成员们的初始化队伍(Member Initialization List)

在下列4中情况下,为了保证程序正确编译,必须使用member initialization list:

(1) 初始化一个reference member时

(2) 初始化一个const member时

(3) 当调用一个base class的constructor,而它拥有一组参数时

(4) 当调用一个member class的constructor,而它拥有一组参数时

除这4种情况之外,如果不使用member initialization list也可以正确编译,但是效率不高,比如:

class Word{String _name;int _cnt;public:Word(){_name = 0;_cnt = 0;}
}

这里Word的构造函数会先产生一个临时的String对象,然后将它初始化,之后以一个赋值运算符将临时对象指定给_name,然后再销毁这个临时性对象。

成员初始化列表中到底发生了什么?编译器会一一操作初始化列表,以适当顺序在构造函数之内安插初始化操作,并且在任何explicit user code之前。并且要注意的是:成员初始化的顺序是由class中members的声明顺序决定的,不是由Initialization list中的排列顺序决定的。

《深入探索C++对象模型》第二章 构造函数语义学(The Semantics of Constructors)相关推荐

  1. C++ 对象模型 第二章 构造函数语意学

    目录 2.1 默认构造函数的建构操作 2.2复制构造函数的建构操作 2.2.1 位逐次拷贝 2.2.2 不进行位逐次拷贝 2.2.3 重新设定虚函数表的指针 2.3 成员们的初始化队伍 2.1 默认构 ...

  2. 第2章构造函数语义学读书笔记——深度探索c++对象模型

    深度探索c++对象模型 第2章 构造函数语义学 2.1 Default Constructor的构建操作 2.2 Copy Constructor的构造操作 2.3 程序转化语义学 2.4 成员的初始 ...

  3. 深度探索C++对象模型第2章 构造函数语义学

    默认构造函数 两个误区: 1 任何class如果没有定义默认构造函数,就会被合成一个出来:只有在某些情况下被合成 2 编译器合成出来的默认构造函数会明确设定class中每一个数据成员的默认值 :默认值 ...

  4. 深度探索C++ 对象模型(3)-默认构造函数Default Constructor

    1. Default Constructor只对base class subobjects和member class objects初始化,对data member不做操作 2. 编译器构造Defau ...

  5. Android开发艺术探索学习笔记 第二章IPC

    最近将之前工作做本地的学习笔记上传一下 这里是Android艺术开发探索的前三章内容 文章目录 1. android的多进程模式 2. IPC基础概念介绍 2.1 Serializable 2.2Pa ...

  6. 深度探索C++ 对象模型(3)-默认构造函数Default Constructor续

    (1)带有虚函数的类 class Widget { public: virtual int flip() = 0;//..}; void flip(const Widget* widget ) { w ...

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

    很多人抱怨说C++背着程序员做了太多事,如: if (cin) { /* ... */ } 为了让cin能转换为真假值,为cin定义一个类型转换运算符operator int(),就可以完成以上工作了 ...

  8. 【C++对象模型】第二章 构造函数语意学

    1.Default Constructor 当编译器需要的时候,default constructor会被合成出来,只执行编译器所需要的任务(将members适当初始化). 1.1  带有 Defau ...

  9. 《深度探索C++对象模型》--2 构造函数语意学

    1.default constructor的构造操作 C++standard:对于class X,如果没有任何user-declared constructor,那么会有一个default const ...

最新文章

  1. VTK:彩色海拔地图用法实战
  2. [2020-11-24 contest]糖果机器(二维偏序),手套(状压dp),甲虫(区间dp),选举(线段树 最大子段和)
  3. Windows编程之网络之邮件槽通讯
  4. 微信小程序之发送模板消息(通过openid推送消息给用户)
  5. 计算机工程与应用单像素成像,2011计算机工程与应用基于压缩感知理论的单像素成像系统研究_白凌云.pdf...
  6. 2021.08.24学习内容torch.utils.data.DataLoader以及CUDA与GPU的关系
  7. 2.3_circular_queue_环形队列
  8. OwinStartup不触发
  9. [转载] Python快速编程入门课后程序题答案
  10. Unity文件操作路径
  11. “完数”(C代码+流程图)
  12. 3个好用的3D点云数据标注工具推荐
  13. 02-准备实验环境(批量克隆)-011-DiskPart 遇到错误 介质受写入保护
  14. Ubuntu系统安装及su安装
  15. 【RabbitMQ】java.lang.NoClassDefFoundError: org/springframework/util/backoff/BackOff
  16. 【Python实战】推文助手好用吗?真的能赚钱吗?教您一招,有了这款微信自动发送消息小助手,文字自动跳出来~赚麻了(赶紧收藏)
  17. oracle 存储过程好学吗,想靠Oracle拿高薪,存储过程的优点你是否搞清楚了?
  18. python飞行棋小游戏
  19. 79到85年出生的人的十大尴尬:
  20. 信息经济学 - 信息不对称

热门文章

  1. ISO27001认证是信息安全管理体系认证
  2. BackTrack-R2发布
  3. mysql的R树_GIS笔记——R树:一种用于空间查找的动态索引结构(算是节译)
  4. 【2022-08-27】美团秋招笔试前四道编程题
  5. 程序人生 | 程序员感觉技术停滞了怎么办?找个师傅引导架构之路
  6. PO、BO、VO、POJO、DTO、DAO分别代表什么意思
  7. Windows XP自动登录 auto login
  8. 大学生计算机适合用苹果笔记本吗,2019学生党笔记本推荐 苹果笔记本适合大学生吗...
  9. 数据挖掘基础之数据清洗:用python把“深圳二手房参考价”PDF保存为EXCEL
  10. 老生常谈:让软件留下临终遗言并优雅地关闭