C++幕后故事(七)–一个对象的生与死
这节里面我们会学习到以下四点:
1.对象的生成时机
2.对象构造过程和POD类型
3.对象的复制语意
4.析构语意
1.对象生成的时机
根据对象的控制力度不同,对象的生成时机也是不一样的。
我们可以把它分为两类:
1.new操作符用户手动控制时机,随时new,随时生成。
2.编译器控制下也是有细微的差别,请看下面的表格。
全局对象/全局静态对象 | 构造先于main函数的,在main之前还有很多的准备工作 |
局部静态对象 | 第一次调用的时候生成,第二次时不会在构造 |
局部对象 | 每次调用的时候都会生成 |
编译器为VS2013 x86,下面是代码验证:
/*
测试:对象的构造、析构、拷贝语意
*/
namespace object_ctor_dtor_copy_semantic
{
class Cat
{
public:
explicit Cat(const string &name) : mName(name) { cout << mName << endl; }
~Cat() { cout << "~" << mName << endl; }
private:
string mName;
};
// 全局对象
Cat g_Cat("global cat");
// 全局静态对象
Cat g_s_Cat("global static cat");
void test_obj_ctor()
{
// 局部变量
Cat local_cat("local cat");
// 局部静态对象
static Cat local_s_cat("local static cat");
}
};
int main(int argc, char *argv[])
{
cout << "------------start main------------" << endl;
object_ctor_dtor_copy_semantic::test_obj_ctor();
cout << "------------end main------------" << endl;
return 0;
}
// 打印的结果
// global cat
// global static cat
// ------------start main------------
// local cat
// local static cat
// ~local cat
// ------------end main------------
// ~local static cat
// ~global static cat
// ~global cat
关于局部对象这里有个小技巧跟大家分享下:类的实例只有在真正需要的时候再初始化
void test_local_useless(bool find)
{
Dog dog;
// 返回操作,而这里初始化的dog对象没有任何的作用,平白无故的增加dog的构造函数调用降低效率
if (find) { return; }
// 应该将对象的初始化延迟到真正需要的时候在初始化
// 对dog的一系列操作
// ...
}
2.对象构造
2.1 构造函数做了什么?
我们已经知道对象在什么时候生成,但是对象在生成过程除了我们自己写的构造函数里面的动作,编译器在幕后也帮我们做了很多的工作,这节我们就要搞清楚编译器做了什么。
这一节既然要分析,我们就来分析最复杂的模型,虚继承+虚函数模型。因为最难的搞懂了,那简单的还不是毛毛雨。
看如下代码:
class Point
{
public:
Point() : mX(1), mY(2) { cout << "point" << endl; }
virtual ~Point() { cout << "~point" << endl; }
protected:
int mX;
int mY;
};
class Point3D : public virtual Point
{
public:
Point3D() : mZ(3) { cout << "point3d" << endl; }
virtual ~Point3D() { cout << "~point3d" << endl; }
virtual void VirFun1() { cout << "~VirFun1" << endl; }
protected:
int mZ;
};
class Vertex : public virtual Point
{
public:
Vertex() : mAngle(4) { cout << "vertex" << endl; }
virtual ~Vertex() { cout << "~vertex" << endl; }
virtual void VirFun2() { cout << "~VirFun2" << endl; }
protected:
int mAngle;
};
class Vertex3D : public Point3D, public Vertex
{
public:
Vertex3D() { cout << "vertex3D" << endl; }
virtual ~Vertex3D() { cout << "~vertex3D" << endl; }
virtual void VirFun3() { cout << "~VirFun3" << endl; }
};
class PVertex : public Vertex3D
{
public:
PVertex() : mCount(5) { cout << "PVertex3D" << endl; }
virtual ~PVertex() { cout << "~PVertex3D" << endl; }
virtual void VirFun4() { cout << "~VirFun4" << endl; }
void setvalue(int value) { mY = value; }
protected:
int mCount;
};
void test_virtual_inherit_ctor()
{
PVertex pvertex;
// pvertex.PVertex::~PVertex();
// pvertex.setvalue(10);
// 露出海面的表象
// point
// point3d
// vertex
// vertex3D
// PVertex3D
// ~PVertex3D
// ~vertex3D
// ~vertex
// ~point3d
// ~point
}
int main()
{
test_virtual_inherit_ctor();
return 0;
}
调用函数,打印的结果如上面代码中注释的那样,其实那只是冰山一角。我们先看看PVertex布局是啥样的。老规矩将上面的代码保存为main.cpp。
1.借助VS2013开发人员命令提示,进入到main.cpp所在目录。
2.运行命令cl /d1 reportSingleClassLayoutPVertex main.cpp
3.拿出重要的部分我们看看的
class PVertex size(40):
+---
| +--- (base class Vertex3D)
| | +--- (base class Point3D)
0 | | | {vfptr}
4 | | | {vbptr}
8 | | | mZ
| | +---
| | +--- (base class Vertex)
12 | | | {vfptr}
16 | | | {vbptr}
20 | | | mAngle
| | +---
| +---
24 | mCount
+---
+--- (virtual base Point)
28 | {vfptr}
32 | mX
36 | mY
+---
PVertex::$vftable@Point3D@:
| &PVertex_meta
| 0
0 | &Point3D::VirFun1
1 | &Vertex3D::VirFun3
2 | &PVertex::VirFun4
PVertex::$vftable@Vertex@:
| -12
0 | &Vertex::VirFun2
PVertex::$vbtable@Point3D@:
0 | -4
1 | 24 (PVertexd(Point3D+4)Point)
PVertex::$vbtable@Vertex@:
0 | -4
1 | 12 (PVertexd(Vertex+4)Point)
PVertex::$vftable@Point@:
| -28
0 | &PVertex::{dtor}
从导出的结构中看出,这个内存模型真是相当复杂,看着都有点头晕目眩。当对象之间的关系复杂之后,甚至连对象的大小都有膨胀的感觉。
我们关系整理下:
从上面可以看出,PVertex虚函数(除了虚析构函数)是追加在Point3D vfptr表中,而析构函数则是放在Point vfptr表中。
我们把内存模型搞清楚了,剩下的简单多了,我们下图所示:
看了半天发现,其实还是很复杂,复杂到一页word装不下。整个的调用流程,感觉都是在不断的设置虚表,设置虚基类表,不断的重复。而我们写的代码只是其中的一小部分。
好,我们再简化下这张图。(红色的线表示调用过程,蓝色线表示回溯过程)
这样看就简洁多了,整个调用的流程也是一目了然。
问题1:但是是不是觉得有点奇怪,PVertex怎么直接调用Point构造函数,不是应该下面这样图?
但是这样的调用流程会造成将Point构造两次,大大的降低效率。所以编译器会决定由谁构造Point。关于virtual base class constructor如何被调用有着明确的定义:只有当一个完成的class object被定义出来(PVertex)时,它才会被调用;如果object只是某个完整object的subobject(Point3D),它就不会被调用(摘自《深入探索C++对象模型》)。
举个例子:
1.我们定义了一个Vertex3D对象,这时Point3D就是Vertex3D是个subobject对象,所以此时Point3D就不会调用Point构造函数。
2.定义了一个Point3D,它就是个完整的object,所以会直接调用Point构造函数。
问题2:在构造函数调用链中,我们发现整个过程都是在不断的设置虚表地址和虚基类表地址。为什么要来回不断的设置呢,在最开始的时候一次性搞定不就行了。
举个例子:
在不同的对象域中不停的修改虚表和虚基类表地址做法我称之为入乡随俗
我们在构造PVertex时,PVertex先去构造Point。此时Point对象已经构造完毕是个完整的对象,但是PVertex还是残缺对象。如果这个时候我们Point虚表地址还是PVertex的虚表地址。此时我们在Point构造函数间接调用到PVertex虚函数,而此时PVertex还未完全构造完毕(比如一些成员变量还未初始化),这时调用PVertex虚函数就存在安全风险。说的简单点,在父类构造函数中就要把虚表地址设置为父类自己的而不是子类的。虚基类表也是同样的道理。同时会联想到在对象析构的时候也是类似的。
如果感兴趣的同学可以再看下汇编代码,其实这里的汇编代码的思路就是非常的清晰,就是如何把内存给填满的。我把代码就放在最后面了。附录1.1汇编代码填充内存结构。
2.2 POD类型
所谓的POD全称是Plain Old Data。基本数据类型、指针、union、数组、构造函数是 trivial 的 struct 或者 class。其实C的struct极其的相似。
看下面代码:
class Dog
{
public:
int mSize;
int mAge;
};
void test_pod_type()
{
// 1.没有加上括号,注意这里的成员值都是随机值
Dog *dog = new Dog;
// 2.加上括号,注意这里的成员值都为0
Dog *dog1 = new Dog();
}
但是结果却大不相同。加上括号初始化,会将对象中的成员变量做初始化。但是没有加上括号的对象中成员变量却是个随机值。
但是如果Dog有构造函数,但是里面什么都不做。上面的两行初始化的结果却是一样的,对象中成员变量的值都是随机值。
针对上面的代码,做个表格更直观点。
无构造函数 | 存在构造函数(未初始化) | 存在构造函数(初始化) | |
不带()初始化 | 随机值 | 随机值 | 初始化为0 |
带()初始化 | 初始化为0 | 随机值 | 初始化为0 |
所以最佳的实践方式:给类加上构造函数同时给类中的成员变量赋初值,在构造对象的时候采用正规的做法加上括号。
3.对象的复制语意
一说到复制语意,我就想到了build设计模式,当然这两者没有强相关性,硬要说关联那就是它们都是和对象的构造有关。
对象的复制语意分为两种,一种就是拷贝构造,还有一种就是赋值构造****(operator=)。但是有的同学,这两种方式不能很好的区分。其实很简单,拷贝构造是从无到有的过程,赋值构造重新赋值过程,用已经存在的对象去重新赋值另外一个已经存在的对象。(这里不提及std::move构造)。
在复制过程中,编译器也会为我们提供默认的复制构造语意,我们把编译器提供的叫做浅拷贝(bitwise copy)。在拷贝的时候,每个对象都拥有自己独立的一份资源而不是共享资源,这种方式叫做深拷贝(memberwise copy)。
为什么会有两种方式?
编译器提供两种方式,是因为两种方式各有优缺点。浅拷贝效率略高于深拷贝,但是存在资源释放问题。深拷贝是把资源也会对应的拷贝一份,这样就会造成效率的下降。当类中不含有任何的资源,那么编译器提供的浅拷贝就已经胜任任务。
最后如果我们不想要复制语意,可以将拷贝构造函数或者operator=设置为private属性。还可以使用c++11 delete语法禁止复制语意。
4.析构语意
对象的析构可以看成对象构造的逆向过程。对象的析构函数是个非常重要的函数,因为在对象消失的那一刻对释放资源,做一些清理的工作。
我们接着第二节对象构造里面的代码,画下析构的流程。
这里我就画了简易的示意图,其实它里面设置虚表和虚基类表地址的套路和它的构造流程是十分的相似,我就不再重复了。
5.总结
这一节提到的拷贝构造,赋值构造,析构函数被称为C++的big three,这三个函数十分的重要一定要时刻小心。看一个人写的类文件,首先就要看从这三个函数开始,写了也不能代表水平很高,但是不写水平肯定不高。
附录1:
1.汇编代码填充内存结构
; 调用PVertex的构造函数
00983181 lea ecx,[pvertex]
00983184 call object_ctor_dtor_copy_semantic::PVertex::PVertex (09712CBh)
; 设置Point3D域的虚基类表
009767D2 mov eax,dword ptr [this]
009767D5 mov dword ptr [eax+4],98D7D4h
; 设置Vertex域的虚基类表
009767DC mov eax,dword ptr [this]
009767DF mov dword ptr [eax+10h],98D7E4h
; 调整this指针,指向Point域
009767E9 add ecx,1Ch
; 调用Point够着函数
009767EC call object_ctor_dtor_copy_semantic::Point::Point (097193Dh)
; 设置point虚表地址
00976D33 mov eax,dword ptr [this]
00976D36 mov dword ptr [eax],98D68Ch
; 初始化成员变量的值
00976D3C mov eax,dword ptr [this]
00976D3F mov dword ptr [eax+4],1
00976D46 mov eax,dword ptr [this]
00976D49 mov dword ptr [eax+8],2
; 再将this指针调回为pvertex的首地址
00976809 mov ecx,dword ptr [this]
; 调用Vertex3D
0097680C call object_ctor_dtor_copy_semantic::Vertex3D::Vertex3D (09713B1h)
00976F29 mov ecx,dword ptr [this]
00976F2C call object_ctor_dtor_copy_semantic::Point3D::Point3D (0971311h)
; 设置Point3D虚表
00976C5D mov eax,dword ptr [this]
00976C60 mov dword ptr [eax],98D6B8h
; 根据虚基类表找到偏移值
00976C66 mov eax,dword ptr [this]
00976C69 mov ecx,dword ptr [eax+4]
00976C6C mov edx,dword ptr [ecx+4]
00976C6F mov eax,dword ptr [this]
; 设置析构函数的虚表地址
00976C72 mov dword ptr [eax+edx+4],98D6C0h
; 根据上面找到的偏移值,初始化成员变量
00976C7A mov eax,dword ptr [this]
00976C7D mov dword ptr [eax+8],3
; 调整this指针,指向Vertex的首地址
00976F3A mov ecx,dword ptr [this]
00976F3D add ecx,0Ch
00976F40 call object_ctor_dtor_copy_semantic::Vertex::Vertex (0971429h)
; 设置Vertex虚表
0097707D mov eax,dword ptr [this]
00977080 mov dword ptr [eax],98D6F8h
; 根据虚基类表找到偏移值
00977086 mov eax,dword ptr [this]
00977089 mov ecx,dword ptr [eax+4]
0097708C mov edx,dword ptr [ecx+4]
0097708F mov eax,dword ptr [this]
; 设置析构函数的虚表地址
00977092 mov dword ptr [eax+edx+4],98D704h
; 根据上面找到的偏移值,初始化成员变量
0097709A mov eax,dword ptr [this]
0097709D mov dword ptr [eax+8],4
; 设置Vertex3D虚表地址
00976F49 mov eax,dword ptr [this]
00976F4C mov dword ptr [eax],98D73Ch
00976F52 mov eax,dword ptr [this]
; 设置Vertex3D虚基类表地址
00976F55 mov dword ptr [eax+0Ch],98D748h
00976F5C mov eax,dword ptr [this]
00976F5F mov ecx,dword ptr [eax+4]
00976F62 mov edx,dword ptr [ecx+4]
00976F65 mov eax,dword ptr [this]
; 设置Vertex3D析构函数的虚表地址
00976F68 mov dword ptr [eax+edx+4],98D754h
; 设置PVertex继承Point3D的虚表地址
00976818 mov eax,dword ptr [this]
0097681B mov dword ptr [eax],98D7A0h
; 设置PVertex继承的Vertex的虚表地址
00976821 mov eax,dword ptr [this]
00976824 mov dword ptr [eax+0Ch],98D7B0h
; 设置PVertex继承的Point的虚表地址
0097682B mov eax,dword ptr [this]
0097682E mov ecx,dword ptr [eax+4]
00976831 mov edx,dword ptr [ecx+4]
00976834 mov eax,dword ptr [this]
00976837 mov dword ptr [eax+edx+4],98D7C4h
; 成员变量的初始化
0097683F mov eax,dword ptr [this]
00976842 mov dword ptr [eax+18h],5
eax,dword ptr [this]
00976824 mov dword ptr [eax+0Ch],98D7B0h
; 设置PVertex继承的Point的虚表地址
0097682B mov eax,dword ptr [this]
0097682E mov ecx,dword ptr [eax+4]
00976831 mov edx,dword ptr [ecx+4]
00976834 mov eax,dword ptr [this]
00976837 mov dword ptr [eax+edx+4],98D7C4h
; 成员变量的初始化
0097683F mov eax,dword ptr [this]
00976842 mov dword ptr [eax+18h],5