深入探索C++对象模型(二)
构造函数语义学(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种情况:
- 当class内含一个member object而后者的class声明有一个copy constructor时。(不论是被class设计者明确声明,或是被编译器合成)
- 当class继承自一个base class而后者存在一个copy constructor时(再次强调,不论是被显式声明或是被合成而得)
- 当class声明了一个或多个virtual function时
- 当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中的解决办法是一个双阶段的转化:
- 首先加上一个额外参数,其类型是class object的一个reference,这个参数将被用来放置被“拷贝建构”而得的返回值
- 在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;
但是在第二行和第三行中,语法明显提供了两个步骤的初始化操作:
- 将一个暂时性的object设以初值1024
- 将暂时性的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++对象模型(二)的更多相关文章
- 拾遗与填坑《深度探索C++对象模型》3.2节
<深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...
- 《深度探索C++对象模型》读书笔记(一)
前言 今年中下旬就要找工作了,我计划从现在就开始准备一些面试中会问到的基础知识,包括C++.操作系统.计算机网络.算法和数据结构等.C++就先从这本<深度探索C++对象模型>开始.不同于& ...
- 读书笔记《深度探索c++对象模型》 概述
<深度探索c++对象模型>这本书是我工作一段时间后想更深入了解C++的底层实现知识,如内存布局.模型.内存大小.继承.虚函数表等而阅读的:此外在很多面试或者工作中,对底层的知识的足够了解也 ...
- Android开发艺术探索笔记——View(二)
Android开发艺术探索笔记--View(二) View的事件分发机制 学习资料: 1.Understanding Android Input Touch Events System Framewo ...
- 柔性数组-读《深度探索C++对象模型》有感 (转载)
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
- 柔性数组-读《深度探索C++对象模型》有感
最近在看<深度探索C++对象模型>,对于Struct的用法中,发现有一些地方值得我们借鉴的地方,特此和大家分享一下,此间内容包含了网上搜集的一些资料,同时感谢提供这些信息的作者. 原文如下 ...
- [读书系列] 深度探索C++对象模型 初读
2012年底-2014年初这段时间主要用C++做手游开发,时隔3年,重新拿起<深度探索C++对象模型>这本书,感觉生疏了很多,如果按前阵子的生疏度来说,现在不借助Visual Studio ...
- 拾遗与填坑《深度探索C++对象模型》3.3节
<深度探索C++对象模型>是一本好书,该书作者也是<C++ Primer>的作者,一位绝对的C++大师.诚然该书中也有多多少少的错误一直为人所诟病,但这仍然不妨碍称其为一本好书 ...
- 深度探索C++对象模型
深度探索C++对象模型 什么是C++对象模型: 语言中直接支持面向对象程序设计的部分. 对于各个支持的底层实现机制. 抽象性与实际性之间找出平衡点, 需要知识, 经验以及许多思考. 导读 这本书是C+ ...
随机推荐
- Python3 数据类型-集合
在Python中集合set是基本数据类型的一种,它有可变集合(set)和不可变集合(frozenset)两种.创建集合set.集合set添加.集合删除.交集.并集.差集的操作都是非常实用的方法. 集合 ...
- POJ 2986 A Triangle and a Circle(三角形和圆形求交)
Description Given one triangle and one circle in the plane. Your task is to calculate the common are ...
- 系统常量对话框QT实现
1.运行结果: 2.代码 main.cpp #include "constantdiag.h" #include <QtWidgets/QApplication> in ...
- lintcode-167-链表求和
167-链表求和 你有两个用链表代表的整数,其中每个节点包含一个数字.数字存储按照在原来整数中相反的顺序,使得第一个数字位于链表的开头.写出一个函数将两个整数相加,用链表形式返回和. 样例 给出两个链 ...
- C# Designer.cs
designer.cs 是窗体设计器生成的代码文件,作用是对窗体上的控件做初始化工作(在函数InitializeComponent()中) VS2003以前都把这部分代码放到窗体的cs文件中,由于这部 ...
- 苹果IOS、安卓推送功能开发
IOS推送开发:以下是基于开源javapns推送开发1.DerInputStream.getLength(): lengthTag=111, too big.先排除是否由于打包时证书 .p12 文件被 ...
- EasyUI 学习笔记
EasyUI常见错误 1 . 无论是用HMTL形式实现组件还是使用代码 + HTML 形式实现组件 , 在为组件设置属性时 , 要注意属性值的类型问题 string:必须加引号 number:不加任何 ...
- asp.net中缓存的使用
刚学到asp.net怎么缓存,这里推荐学习一下 www.cnblogs.com/wang726zq/archive/2012/09/06/cache.html http://blog.csdn.net ...
- BZOJ 1179 Atm(强连通分量缩点+DP)
题目说可以通过一条边多次,且点权是非负的,所以如果走到图中的一个强连通分量,那么一定可以拿完这个强连通分量上的money. 所以缩点已经很明显了.缩完点之后图就是一个DAG,对于DAG可以用DP来求出 ...
- Java基础之开关语句详解
switch 语句是单条件多分支的开关语句,它的一般格式定义如下(其中break语句是可选的): switch(表达式) { case 常量值: 若干个语句 break; case 常量值: 若干个 ...