深度探索C++对象模型-第二章

第二章 构造函数语意学

2.1 Default Constructor的构造操作

默认构造函数在需要时被编译器合成出来。

这里的需要分为:程序的需要 和 编译器的需要。

  • 程序的需要:即如果编译器不合成出来,不会发生错误,但程序语意不是自己想要的。
  • 编译器的需要:即如果编译器不合成出来,将发生错误的时候。

当编译器需要的时候,默认构造函数会被合成出来,并且只执行编译器所需要的任务。(也不会帮你赋初始值)

1.类中带有 Default Constructor 的类对象成员

class Foo{ public: Foo(), Foo( int ) ... };
class Bar{ public: Foo foo; char *str; };

void foo_bar(){
    Bar bar;    
    
    if( str ){  }....
}

如果一个class没有任何构造函数,但它内含一个对象成员,而后者有默认构造函数,那编译器需要为该class合成一个默认构造函数(如下)。

Bar::Bar(){
    //被合成的默认构造函数只会满足编译器的需要,因此不会为你初始化str
    foo.Foo::Foo(); 
}

若程序员定义了一个默认构造函数:

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

现在程序的需求满足了,但是编译器还需要初始化foo。由于默认构造函数已经被显式定义,编译器没法合成第二个。

那么编译器的行动是:

  • 如果一个class 内含一个或者一个以上的类对象成员 ,那么class的每一个构造函数必须调用每一个类成员的默认构造函数 。
  • 编译器会扩张已存在的构造函数,在其中安插一些代码,使得在执行用户代码之前,先调用(调用顺序与对象成员在class 的声明次序一致)必要的默认构造函数

2.类继承于带有 Default Constructor 的基类

与前面道理相同:

  • 若没有,则在类中没有默认构造函数,便合成;
  • 若有,则在类中的默认构造函数们在执行用户代码之前,先调用(调用次序根据他们的声明次序)所有基类的默认构造函数。

3.“带有一个虚函数”的类

下面两种情况,同样需要合成默认构造函数:

  1. class 声明(或继承)一个虚函数;
  2. class派生自一个继承串链,其中一个或者更多的 virtual base class(虚基类);

因为一个类中若存在虚函数,那就少不了vptr与vtbl。

因此在编译期间发生:

  1. 一个虚函数表会被编译器产生出来,内放class 的虚函数们的地址;

  2. 每一个类对象中,一个额外的pointer member(vptr)会被编译器合成出来,内含相关的虚函数表的地址;

    【注】每一个类对象的意思是每一个有虚函数的类,就是说若基类与子类中都有虚函数,那么在构造函数中,都会执行这两步。

在合成的构造函数中,编译器会为每一个“带有一个虚函数”的类(或者其派生类)的对象实例设定vptr的初值,并在其中放置虚函数表的地址。

4.“带有一个虚基类”的类

这一小节有点笼统。

Virtual base class的实现法在不同编译器之间有很大差异。然而,<u>每一个实现的共同点在于必须使 虚基类 在其每一个 派生类 中的位置,能够在执行期准备妥当。</u>

对于class所定义的每一个constructor ,编译器都会安插那些“允许每一个virtual base class 的执行期存取操作”的代码。

意思是:因为没有办法在编译期确定 “虚基类中成员” 的实际偏移位置,因此只能在构造函数中加一些代码,合法化 “每一个虚基类的执行期存取操作”。

总结

以上4种情况,会导致“编译器必须为未声明构造函数的类合成一个默认构造函数 ”,这只是编译器(而非程序)的需要。

至于没有存在这4种情况,而又没有声明构造函数的class ,默认构造函数不会被合成出来的。

所有其他的非静态变量 ,如整数,整数指针,整数数组等是不会被初始化的,这些初始化操作对程序是必须的,但对编译器则并非需要的。

C++新手一般有两个误解:

  1. 任何class 如果没有定义默认构造函数,就会被合成出来一个;
  2. 编译器合成出来的默认构造函数会明确设定 class 内每一个data member的默认值;

2.2 Copy Constructor的构造操作

有三种情况,会以一个类的内容作为另一类对象的初值。

  1. 最明显的当然是对一个object做明确的初始化操作;(X xx = x;
  2. 当class object被当做参数交给某个函数;
  3. 当函数返回一个class object;

1.Default Memberwise Initialization(逐个成员初始化)

如果class 没有提供一个显式拷贝构造函数时,当class object以 “相同class的另一个object” 作为初值时,其内部是以所谓的default memberwise initialization(逐个成员初始化)方式完成的。

【注】这里没提拷贝构造函数。

逐个成员初始化:也就是把每一个内建的或派生的数据成员(例如一个数组或指针)的值,从某个object拷贝一份到另一个object上,但不拷贝其具体内容。例如只拷贝指针地址,不拷贝一份新的指针指向的对象,这也就是浅拷贝,不过它并不会拷贝其中member class object,而是以递归的方式实行memberwise initialization(就是再到这个member中,进行浅拷贝)。

memberwise initialization是如何实现的呢?

答案就是Bitwise Copy Semantics和default copy constructor。如果class展现了Bitwise Copy Semantics,则使用bitwise copy,否则编译器会生成默认拷贝函数。

也就是说:判断是否要合成拷贝构造函数的标准,是在于class是否展现出所谓的“bitwise copy semantics”(逐位拷贝语意)。

2.bitwise copy semantics(逐位初始化)

那什么情况下class不展现Bitwise Copy Semantics呢?有四种情况:

(等价于什么时候要用默认拷贝构造函数?)

  • 当class内含有一个类对象成员,而这个类成员内有一个默认的copy 构造函数(不论是class设计者明确声明,或者被编译器合成);

  • 当class 继承自一个基类,而基类内有copy构造函数(不论是class设计者明确声明,或者被编译器合成);

  • 当一个类声明了一个或多个virtual 函数

  • 当class派生自一个继承串链,其中一个或者多个virtual base class

在前2种情况下,编译器必须将“类对象成员”或者“基类”的 copy constructor的调用操作 安插到 被合成的copy constructor中。

后两种单独拿出来讲。

3.重新设定Virtual Table 的指针

第一章也提到,因为class 包含virtual function, 编译时需要做扩张操作:

  1. 增加virtual function table,内含有一个有作用的virtual function的地址;
  2. 创建一个指向virtual function table的指针,安插在class object内。

编译器对于每一个新产生的class object的vptr都必须被正确地赋值,否则将跑去执行其他对象的function了,其后果是很严重的(如图2)。因此,编译器导入一个vptr到class之中时,该class 就不在展现bitwise copy semantics,必须合成copy Constructor并将vptr适当地初始化。

下图这种同个类,若按照bitwise copy,没有错误:

2-1.png

但如下图这种,若按照bitwise copy,vptr有错误:

2-2.png

【注】图中,虽然Bear对draw()和animate()函数重写,但虚函数重写后也是虚函数,因此在虚函数表里。

4.处理Virtual Base Class Subobject

virtual base class的存在需要特别处理。一个class object 如果以另一个 virtual base class subobject那么也会使“bitwise copy semantics”失效。

每一个编译器对于虚拟继承的支持承诺,都是表示必须让 “derived class object 中的virtual base class subobject 位置” 在执行期就准备妥当,维护 “位置的完整性” 是编译器的责任。Bitwise copy semantics 可能会破坏这个位置,所以编译器必须自己合成出copy constructor。

这也就是说,拷贝构造函数和默认构造函数一样,需要的时候会进行构建,而并非程序员不写编译器就帮着构建。

2.4 初始化列表

下面四种情况必须使用初始化列表来初始化class 的成员:

  1. 当初始化一个reference member时;
  2. 当初始化一个const member时;
  3. 当调用一个base class 的 constructor ,而它拥有一组参数(其实就是自定义的构造函数)时;
  4. 当调用一个 member class 的 constructor,而它拥有一组参数时。

编译器会一一操作初始化列表,以适当顺序在构造函数内安插舒适化操作,并且在任何用户代码之前。

不过,初始化的顺序是class members声明次序决定的,不是由初始化列表决定的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容