构造函数语义学(The Semantics of Constructors)

Default Constructor的构造操作

对于class X,如果没有任何user-declared constructor,那么会有一个default constructor被隐式(implicitly)声明出来...一个被隐式声明出来的default constructor将是一个trivial(浅薄而无能,没啥用的)constructor...

一个nontrivial default constructor在ARM(注释参考手册)的术语中就是编译器需要的那种,必要的话由编译器合成出来。下面4小节分别讨论nontrivial default constructor的4种情况

“带有Default Constructor”的member class object

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

举例如下:编译器会为class Bar合成一个default constructor:

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

被合成的Bar default constructor内含必要的代码,能够调用class Foo的default constructor来处理member Bar::foo,但它并不产生任何码来初始化Bar::str。将Bar::foo初始化时编译器的责任,将Bar::str初始化则是程序员的责任。

如果有多个class member objects都要求constructor初始化操作,将如何? C++语言要求以“member objects在class中的声明顺序”来调用各个constructors

“带有Default constructor”的base class

如果一个没有任何constructors的class派生自一个“带有default constructor”的base class,那么这个derived class的default constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base classes的default constructor(根据它们的声明的顺序)。对于一个后继派生的class而言,这个合成的constructor和一个“被显式提供的default constructor”并没有差异

“带有一个Virtual Funtion”的class

  • class声明(或继承)一个virtual function
  • class派生自一个继承串链,其中有一个或更多的virtual base classes

不管哪一种情况,由于缺乏由user声明的constructors,编译器会详细记录合成一个default constructor的必要信息。以下面程序段为例:

class Widget{
public:
virtual void flip() = 0;
//...
}; void flip(const Widget& widget) { widget.flip();} //假设Bell和Whistle都派生自Widget
void foo(){
Bell b;
Whistle w; flip(b);
flip(w);
}

下面两个扩张行动会在编译期间发生:

  • 一个virtual function table(在cfront中被称为vtbl)会被编译期产生出来,内放class的virtual functions地址
  • 在每一个class object中,一个额外的pointer member(也就是vptr)会被编译期合成出来,内含相关之class vtbl的地址

此外,widget.flip()的虚拟引发操作(virtual invocation)会被重新改写,已使用widget的vptr和vtabl中的flip()条目

//widget.flip()的虚拟引发操作的转变
(*widget.vptr[1])(&widget)

其中:

  • 1 表示flip()在virtual table中的固定索引
  • &widget代表要交给“被调用的某个flip()函数实体”的this指针

“带有一个virtual base class”的class

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

Copy Constructor的构造操作

有三种情况,会以一个object的内容作为另一个class object的初值,最明显的一种情况是当对一个object做明确的初始化操作,像这样:

class X{ ... };
X x; //明确以一个object的内容作为另一个class object的初值
X xx = x;

另外两种情况是当object被当做参数交个某个函数时,例如:

extern void foo(X x);

void bar(){
X xx; //以xx作为foo()第一个参数的初值(不明显的初始化操作)
foo(xx);
}

以及当函数传回一个class object时,例如:

X foo_bar(){
X xx;
//...
return xx;
}

假设class设计者明确定义了一个copy constructor(这是一个constructor,有一个参数的类型是其class type),像下面这样:

//user-defined copy constructor实例
//可以是多参数形式,其第二个参数及后继参数以一个默认值供应之
X::X(const X& x);
Y::Y(const Y& y, int = 0);

那么在大部分情况下,当一个class object以另一个同类实体作为初值时,上述的constructor会被调用,这可能会导致一个暂时性class object的产生或程序代码的蜕变(或两者都有)

Default memberwise initialization

如果class没有提供一个explicit copy constructor又当如何?当class object以“相同class的另一个object”作为初值,其内部是以所谓default memberwise initialization手法完成的,也就是把每一个內建的或派生的data member(例如一个指针或一个数组)的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。例如:

class String{
public:
//... 没有explicit copy constructor
private:
char *str;
int len;
};

一个String object的default memberwise initialization发生在这种情况之下:

String noun("book");
String verb = noun;

其完成方式就好像个别设定每一个members一样:

//语义相等
verb.str = noun.str;
verb.len = noun.len;

一个class object可用两种方式复制得到,一种是被初始化,另一种是被指定。从概念上看,这两种操作分别是以copy constructor和copy assignment operator完成的。

Bitwise Copy Semantics(位逐次拷贝)

什么时候一个class不展现出“bitwise copy semantics”呢?有4种情况:

  1. 当class内含一个member object而后者的class声明有一个copy constructor时。(不论是被class设计者明确声明,或是被编译器合成)
  2. 当class继承自一个base class而后者存在一个copy constructor时(再次强调,不论是被显式声明或是被合成而得)
  3. 当class声明了一个或多个virtual function时
  4. 当class派生自一个继承串链,其中有一个或多个virtual base classes时

前两种情况中,编译器必须将member或base class的"copy constructors调用操作"安插到被合成的copy constructor中。

重新设定Virtual Table的指针

回忆编译期间的两个程序扩张操作(只要有一个class声明了一个或多个virtual functions就会如此):

  • 增加一个virtual function table(vtbl),内含每一个有作用的virtual function的地址
  • 将一个指向virtual function table的指针(vptr),安插在每一个class object内

当编译器导入一个vptr到class之中时,该class就不再展现bitwise semantics了。现在编译器需要合成出一个copy constructor,以求将vptr适当地初始化。下面是个例子

class ZooAnimal{
public:
ZooAnimal();
virtual ~ZooAnimal();
virtual void animate();
virtual void draw();
//...
private:
//ZooAnimal的animate()和draw()所需要的数据
}; class Bear : public ZooAnimal{
public:
Bear();
void animate(); //虽未写明是virtual,但其实是virtual
void draw(); //同上
virtual void dance();
//...
private:
//Bear的animate()和draw()和dance()所需要的数据
};

ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠"bitwise copy semantics"完成。举个例子

Bear yogi;
Bear winnie = yogi;

yogi会被default Bear constructor初始化,而在constructor中,yogi的vptr被设定指向Bear class的virtual table(靠编译器安插的码完成),因此,把yogi的vptr值拷贝给winnie的vptr是安全的。

当一个base class object以其derived class的object内容做初始化操作时,其vptr复制操作也必须保证安全,例如:

ZooAnimal franny = yogi;   //这会发生切割(sliced)行为

franny的vptr不可以被设定指向Bear class的virtual table(但如果yogi的vptr被直接“bitwise copy”的话,就会导致此结果),否则当下面程序片段中的draw()被调用而franny被传进去时,就会“炸毁”(blow up):

void draw(const ZooAnimal& zoey) { zoey.draw(); }
void foo(){
//franny的vptr指向ZooAnimal的virtual table,
//而非Bear的virtual table(彼由yogi的vptr指出)
ZooAnimal franny = yogi; draw(yogi); //调用Bear::draw()
draw(franny); //调用ZooAnimal::dram()
}

也就是说,合成出来的ZooAnimal copy constructor会明确设定object的vptr指向ZooAnimal class的virtual table, 而不是直接从右手边的class object中将其vptr现值拷贝过来。

处理virtual base class subobject

virtual base class的存在需要特别处理。一个class object如果以另一个object作为初值,而后者有一个virtual base class subobject,那么也会使“bitwise copy semantics”失效

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

class Raccoon : public virtual ZooAnimal{
public:
Raccoon() { /*设定private data初值*/ }
Raccoon(int val) { /*设定private data初值*/}
//...
private:
//所有必要的数据
};

编译器所产生的代码(用以调用ZooAnimal的default constructor、将Raccoon的vptr初始化,并定位出Raccoon中的ZooAnimal subobject)被安插在两个Raccoon constructors之内,成为其先头部队

在"memberwise 初始化"呢? 一个virtual base class的存在会使bitwise copy semantics无效,注意,这个问题并不发生在“一个class object以另一个同类的object作为初值”之时,而是发生在“一个class object以其derived classes的某个object作为初值”之时。举例如下:

class RedPanda : public Raccoon{
public:
RedPanda() { /*设定private data初值*/ }
RedPanda(int val){ /*设定private data初值*/ }
//...
private:
//所有必要的数据
};

强调,如果以一个Raccoon object作为另一个Raccoon object的初值,那么bitwise copy就绰绰有余了。

//简单的bitwise copy就足够了
Raccoon rocky;
Raccoon little_critter = rocky;

然而如果企图以一个RedPanda object作为little_critter的初值,编译器必须判断“后续当程序员企图存取其ZooAnimal subobject时是否能够正确地执行”

//简单的bitwise copy还不够
//编译器必须明确将little_critter的
//virtual base class pointer/ooset初始化
RedPanda little_red;
Raccoon little_critter = little_red;

在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个copy constructor,安插一些码以设定virtual base class pointer/offset的初值(或只是简单地确定它没有被抹消),对每一个members执行毕业得memberwise初始化操作,以及执行其它的内存相关工作。

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

显式的初始化操作

必要的程序转化有两个阶段

  • 重写每一个定义,其中的初始化操作会被剥除
  • class的copy constructor调用操作会被安插进去

参数的初始化

C++ Standard说,把一个class object当做参数传给一个函数(或是作为一个函数的返回值),相当于以下形式的初始化操作:

X xx = arg;

其中xx代表形式参数(或返回值)而arg代表真正的参数值,因此,若已知如下函数:

void foo(X xo);

下面的调用方式:

X xx;
//...
foo(xx);

将会要求局部实体(local instance) xo以membeerwise的方式将xx当作初值。

在编译器实现技术上,有一种策略是导入所谓的临时性object,并调用copy constructor将它初始化,然后将此临时性object交给函数。例如,前一段的代码转换如下:

//C++伪码
//编译器产生出来的临时对象
X _temp0; //编译器对copy constructor的调用
_temp0.X::X(xx);
//重新改写函数调用操作,以便使用上述的暂时对象
foo(_temp0);

然而这样的转换只做了一半功夫而已,残留问题如下:问题出在foo()的声明。暂时性object先以class X的copy constructor正确设定了初值,然后再以bitwise防守拷贝到xo这个局部实体中(所以,不能按照以往的声明)。因此,foo()的声明因而也必须被转化,形式参数必须从原先一个class X object改变为一个class X reference。如下:

void foo(X& xo);

其中class X声明了一个destructor,它会在foo()函数完成之后被调用,对付那个暂时性的object。

另外一种实现方法是以“拷贝建构”(copy construct)的方式把实际参数直接建构在其应该的位置上,此位置视函数活动范围的不同,记录于程序堆栈中。在函数返回之前,局部对象(local object)的destructor(如果有定义的话)会被执行。

返回值得初始化

已知下面这个函数定义:

X bar(){
X xx;
//处理xx ...
return xx;
}

bar()的返回值如何从局部对象xx中拷贝过来? Stroustrup在cfront中的解决办法是一个双阶段的转化:

  1. 首先加上一个额外参数,其类型是class object的一个reference,这个参数将被用来放置被“拷贝建构”而得的返回值
  2. 在return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当做上述新增参数的初值。

真正的返回值是什么? 最后一个转换操作会重新改写函数,使它不传回任何值。bar()转换如下:

//函数转换,以反映copy constructor的应用
void bar(X& _result){
X xx; //编译器所产生的default constructor调用操作
xx.X::X(); //...处理 xx //编译器所产生的copy constructor调用操作
_result.X::X(xx); return;
}

在编译器层面做优化

在一个如bar()这样的函数,所有的return指令传回相同的具名数值(name value,即是指函数中的xx),因此编译器有可能自己做优化,方法是以result参数取代name return val。例如原bar()函数,可能被转换为:

void bar(X& _result){
//default constructor被调用
_result.X::X(); //...直接处理_result; return;
}

这样的编译器优化操作,有时被称为Named Return Value(NRV)优化。 NRV优化如今被视为是标准C++编译器的一个义不容辞的优化操作——虽然其需求其实超越了正式标准之外。

虽然NRV优化提供了重要的效率改善,它还是饱受批评。其中一个原因是,优化由编译器默认完成,而它是否真的被完成,并不十分清楚。第二个原因是,一旦函数变得比较复杂,优化也就变得比较难以施行。

下面例子,三个初始化操作在语义上相等:

X xx0(1024);
X xx1 = X(1024);
X xx2 = (X)1024;

但是在第二行和第三行中,语法明显提供了两个步骤的初始化操作:

  1. 将一个暂时性的object设以初值1024
  2. 将暂时性的object以拷贝建构的方式作为explicit object的初值

换句话说,xx0是被单一的constructor操作设定初值:

xx0.X::X(1024);

而xx1或xx2却调用两个constructor,产生一个暂时性object,并针对该暂时性object调用class X的destructor

X _temp0;
_temp0.X::X(1024);
xx1.X::X(_temp0);
_temp0.X::~X();

一般而言,面对“以一个class object作为另一个class object的初值”的情形,语言允许编译器有大量的自由发挥空间。其利益当然是导致机器码产生时有明显的效率提升。缺点则是你不能安全地规划你的copy constructor的副作用,必须视其执行而定。

Copy Constructor:要还是不要?

copy constructor的应用,迫使编译器多多少少对你的程序代码做部分优化。尤其当一个函数以传值(by value)的方式传回一个class object,而该class有一个copy constructor(不论是明确定义出来的,或是合成的)时。这将导致深奥的程序转化——不论在函数的定义或使用上,此外编译器也将copy constructor的调用操作优化,以一个额外的第一参数(数值被直接存放在其中)取代NRV。

成员们的初始化队伍(Memeber Initialization List)

在下列情况下,为了让你的程序能够顺利编译,你必须使用member initialization list:

  • 当初始化一个reference member时
  • 当初始化一个const member时
  • 当调用一个base class的constructor,而它拥有一组参数时
  • 当调用一个member class的constructor,而它拥有一组参数时

下列情况下,程序可以被正确编译并执行,但是效率不彰,例如:

class Word{
String _name;
int _cnt;
public:
//没有错误,只不过太天真
Work(){
_name = 0;
_cnt = 0;
}
};

在这里,Word constructor会先产生一个暂时性的String object,然后将它初始化,再以一个assignment运算符将暂时性object指定给_name,然后再摧毁那个暂时性对象。以下是constructor可能的内部扩张结果:

Word::Word( /*this pointer goes here*/ ){
//调用String的default constructor
_name.String::String(); //产生暂时性对象
String temp = String(0); //"memberwise"地拷贝_name
_name.String::operator=(temp); //摧毁暂时性对象
temp.String::~String(); _cnt = 0;
}

对程序代码反复审查并修正之,得到一个明显更有效率的实现方法:

//较佳的方式
Word::Word : _name(0){
_cnt = 0;
}

它会被扩张成如下样子:

Word::Word( /*this pointer goes here*/ ){
//调用String(int) constructor
_name.String::String(0);
_cnt = 0;
}

member initialization list中到底会发生什么事情?编译器会一一操作initialization list,以适当顺序在constructor之内安插初始化操作,并且在任何explicit user code之前。

initialization list中的项目顺序是由class中的members声明顺序决定的,不是由initialization list中的排列顺序决定的。

深入探索C++对象模型(二)的更多相关文章

  1. 拾遗与填坑《深度探索C++对象模型》3.2节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  2. 《深度探索C++对象模型》读书笔记(一)

    前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...

  3. 读书笔记《深度探索c++对象模型》 概述

    <深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...

  4. Android开发艺术探索笔记——View(二)

    Android开发艺术探索笔记--View(二) View的事件分发机制 学习资料: 1.Understanding Android Input Touch Events System Framewo ...

  5. 柔性数组-读《深度探索C++对象模型》有感 (转载)

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  6. 柔性数组-读《深度探索C++对象模型》有感

    最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...

  7. [读书系列] 深度探索C++对象模型 初读

    2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...

  8. 拾遗与填坑《深度探索C++对象模型》3.3节

    <深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...

  9. 深度探索C++对象模型

    深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...

随机推荐

  1. SIG蓝牙mesh笔记3_网络结构

    目录 3. Mesh Networking 3.1 Bearers 承载层 3.2 Network Layer 网络层 3.2.3 Address validity 地址有效性 3.2.4 Netwo ...

  2. hibernate 异常a different object with the same identifier value was already associated with the session

    在使用hibernate的时候发现了一个问题,记录一下解决方案. 前提开启了事务和事务间并无commit,进行两次save,第二次的时候爆出下面的异常a different object with t ...

  3. 操作系统及Python解释器工作原理讲解

    操作系统介绍 操作系统位于计算机硬件与应用软件之间 是一个协调.管理.控制计算机硬件资源与软件资源的控制程序 操作系统功能: 控制硬件 把对硬件复杂的操作封装成优美简单的接口(文件),给用户或者应用程 ...

  4. Mysql 工作原理

    刚开始接触一个新的事物的时候,我觉得很有必要从其工作原理入手,弄清楚这个东西的来龙去脉,为接下来的继续深入学习做好铺垫,掌握好其原理有助于我们从整体上来把握这个东西,并且帮助我们在排错过程中理清思路. ...

  5. lintcode-158-两个字符串是变位词

    158-两个字符串是变位词 写出一个函数 anagram(s, t) 判断两个字符串是否可以通过改变字母的顺序变成一样的字符串. 说明 What is Anagram? Two strings are ...

  6. Android api level对照表

    转自:blog.csdn.net/lihenair/article/details/49869299 Platform Version API Level VERSION_CODE Notes And ...

  7. (转)centos6.5下Zabbix系列之Zabbix安装搭建及汉化

    最近在研究zabbix,在整理完成之后就有了写一下总结博客的想法,在我研究zabbix的时候给我很大帮助的是it你好,博客地址 http://itnihao.blog.51cto.com/他做的zab ...

  8. springBoot定制内嵌的Tomcat

    此篇仅介绍配置方式,详细配置内容百度查阅 工程结构: 可以通过配置的方式设置参数,如下 application.properties #配置tomcat端口 # server.port= 8888 # ...

  9. winform全局异常捕获

    /// <summary> /// 应用程序的主入口点. /// </summary> public static ApplicationContext context; [S ...

  10. Atom Editor 插件 atom-less 的使用方法

    一.下载安装 atom-less atom-less 项目在这里:https://github.com/schmuli/atom-less 安装方法请参考这篇博文:http://blog.csdn.n ...