声明: 菜菜的成长总免不了需要施肥,灌溉,除虫等照料,希望能多一点阳光,
这里要感谢下
evikis, 邓韬和pencil的批评和鼓励。
内容都是书籍上的内容,对内容按个人习惯进行整理,以方便后续查阅之用,如果不妥之处还望指教。
阅读书籍信息:
<<高质量C/C++>> 第三版 第13章对象的初始化/拷贝和析构 电子工业出版社
<<深度探索C++对象模型>> 第2章构造函数语义学 华中科技大学出版社
这里主要整理了什么情况下编译器会自动生成默认的构造和拷贝构造的情况,以及初始化列表的顺序。一般情况都会写构造来初始化成员变量,但曾经被这里问题困扰,只是绕过(自己写),却没有很好的了解实现的机制。
编译器生成默认构造和默认拷贝构造的情况如下:
默认构造:
本身没有默认构造,但是却包含有带默认构造的对象(继承的基类有,成员对象有)
包含虚函数需要处理初始化虚表操作(自身的,继承的,成员变量的)和虚继承
默认拷贝构造:
本身没有默认拷贝构造,但是却包含有带默认拷贝构造的对象(继承的基类有,成员对象有)
包含虚函数需要处理初始化虚表操作(自身的,继承的,成员变量的)和虚继承
一:一般不存在默认构造和拷贝构造的情况
如果类可以用memcpy来完成默认构造或拷贝构造,也不会对使用操作影响的(类中没有下面要说的情况),就不需要有构造的过程。
class CAnimal
{
public:
bool isCanSay(){return false;}
static bool isCanFly(){return false;}
public:
char m_szName[16];
static int m_snType;
};
CAnimal TestDefalutConstruct(CAnimal AnimalObj)
{
CAnimal varObj = AnimalObj;
return varObj;
}
int _tmain(int argc, _TCHAR* argv[])
{
CAnimal AnimalObj;
CAnimal AnimalTest(AnimalObj);
TestDefalutConstruct(AnimalTest);
}
CAnimal AnimalObj;
TestDefalutConstruct(AnimalObj);
00182495 sub esp,10h //临时对象局部空间(只有一个数组10h)
00182498 mov eax,esp
0018249A mov ecx,dword ptr [ebp-18h] //AnimalObj的this
0018249D mov dword ptr [eax],ecx
0018249F mov edx,dword ptr [ebp-14h]
001824A2 mov dword ptr [eax+4],edx
001824A5 mov ecx,dword ptr [ebp-10h]
001824A8 mov dword ptr [eax+8],ecx
001824AB mov edx,dword ptr [ebp-0Ch]
001824AE mov dword ptr [eax+0Ch],edx //按位copy
001824B1 lea eax,[ebp-0FCh]
001824B7 push eax //数据块使用引用(指针)的方式操作进行优化
001824B8 call TestDefalutConstruct (1811E5h)
CAnimal TestDefalutConstruct(CAnimal AnimalObj)
{
CAnimal varObj = AnimalObj;
00182378 mov eax,dword ptr [ebp+0Ch]
0018237B mov dword ptr [ebp-18h],eax
0018237E mov ecx,dword ptr [ebp+10h]
00182381 mov dword ptr [ebp-14h],ecx
00182384 mov edx,dword ptr [ebp+14h] //按位copy
00182387 mov dword ptr [ebp-10h],edx
0018238A mov eax,dword ptr [ebp+18h]
0018238D mov dword ptr [ebp-0Ch],eax
return varObj;
001823A8 mov eax,dword ptr [ebp+8] //返回结构体使用引用(指针的方式)
001823AB mov ecx,dword ptr [ebp-18h]
001823AE mov dword ptr [eax],ecx
001823B0 mov edx,dword ptr [ebp-14h] //保存了返回对象的值
001823B3 mov dword ptr [eax+4],edx
001823B6 mov ecx,dword ptr [ebp-10h]
001823B9 mov dword ptr [eax+8],ecx
001823BC mov edx,dword ptr [ebp-0Ch]
001823BF mov dword ptr [eax+0Ch],edx
001823C2 mov eax,dword ptr [ebp+8]
}
//call TestDefalutConstruct (1811E5h)返回之后
001824BD add esp,14h
001824C0 mov ecx,dword ptr [eax] //这里就是返回值,这个eax就是所谓的引用方式
001824C2 mov dword ptr [ebp-114h],ecx
001824C8 mov edx,dword ptr [eax+4]
001824CB mov dword ptr [ebp-110h],edx
001824D1 mov ecx,dword ptr [eax+8]
001824D4 mov dword ptr [ebp-10Ch],ecx
001824DA mov edx,dword ptr [eax+0Ch]
001824DD mov dword ptr [ebp-108h],edx
对于类似结构体做返回值的操作我看的2种情况:
1)这里的有优化的情况,通过传递引用(指针)来完成值的传出;
2)没有优化的情况返回函数的中结构体的首址,外部再做一次copy赋值的操作
实例的内存结构中只存在成员变量,没有需要编译器特别处理的地方,这里就可以把class当成C语言的struct来看待,如果类中只有简单的基本数据类型,也可以拆开看待。
这里成员只有一个基本数据类型int,对类的成员操作可以简单认为为只对这个int变量操作,而类中的成员函数也可以看成是一般函数即可(这里不是全局的,还是存在作用域的)。
013823DB mov eax,dword ptr [AnimalObj]
013823DE push eax //只有一个int成员
013823DF call TestDefalutConstruct (13811E5h)
二:编译器生成默认构造的情况
编译器生成是在其需要的情况下才会有的动作,这个需要只要是:有需要处理构造过程和初始化虚表的操作的情况下。
1)类自身没有默认构造,但有需要调用的默认构造函数
有构造函数时的构造过程为:先基类,再成员,最后自己。自己没写构造函数怎么会完成对基类和成员对象构造的调用呢?其实就是编译器插入的调用的代码完成对基类和成员对象的构造函数的调用,这个插入动作就是编译器需要处理的。
示例代码
class CAnimal
{
public:
CAnimal()
{
strncpy(m_szName, "Test!", sizeof(m_szName));
}
bool isCanSay(){return false;}
static bool isCanFly(){return false;}
public:
char m_szName[16];
static int m_snType;
};
int CAnimal::m_snType = 0x12345678;
class CCat : public CAnimal
{
public:
int m_nCatTest;
int m_nTest;
};
CCat MyCat;
00262348 lea ecx,[ebp-20h]
0026234B call CAnimal::isCanSay (2612C1h)
跳表地址:002612C1 jmp CCat::CCat (261630h)
CCat::CCat:
00261650 mov dword ptr [ebp-8],ecx
00261653 mov ecx,dword ptr [this]
00261656 call CAnimal::CAnimal (2612BCh)
这里需要调用基类的构造,编译器插足,生成了默认构造完成对基类的调用,成员对象有默认构造的情况雷同。
2)类中有虚表(不论虚表是自己的,继承的,还是成员对象的)
因为虚表需要处理化,这个活我们没有做,编译器此时又不得不插足来完成初始化的过程,也就要生成默认的构造函数来完成初始化操作。
实例代码只是增加了CAnimal的虚析构
virtual ~CAnimal()
{
_asm NOP
}
CAnimal AnimalObj;
000D499F lea ecx,[ebp-4Ch]
000D49A2 call CAnimal::CAnimal (0D12BCh)
Debug版中跳表000D12BC jmp CAnimal::CAnimal (0D23D0h)
CAnimal::CAnimal:
000D23F0 mov dword ptr [ebp-8],ecx
000D23F3 mov eax,dword ptr [this]
000D23F6 mov dword ptr [eax],offset CAnimal::`vftable' (0D7840h) //初始化虚表
000D23FC mov eax,dword ptr [this]
000D23FF pop edi
三 编译器生成默认拷贝构造的情况
编译器生成默认拷贝构造涉及的情况和二基本相同,拷贝构造发生的情况如下:
Class X; class Y=X; 相同类型直接赋值,或派生类赋值给基类(有截取)
将对象作为参数或返回一个对象都会存在拷贝构造
1)相同的类型
CAnimal AnimalObj;
010E23B0 lea ecx,[ebp-40h]
010E23B3 call CAnimal::CAnimal (10E10D7h)
CAnimal TestObj = AnimalObj;
010E23B8 lea eax,[ebp-40h]
010E23BB push eax
010E23BC lea ecx,[ebp-5Ch]
010E23BF call CAnimal::CAnimal (10E10FAh)
CAnimal::CAnimal:
010E25A0 mov dword ptr [ebp-8],ecx
010E25A3 mov eax,dword ptr [__that] 类中数据成员copy值
010E25A6 mov ecx,dword ptr [this]
。。。。。。
010E25BC mov dword ptr [ecx+0Ch],eax
010E25BF mov eax,dword ptr [__that]
010E25C2 add eax,10h
010E25C5 push eax
010E25C6 mov ecx,dword ptr [this]
010E25C9 add ecx,10h
010E25CC call CEat::CEat (10E11EAh) 调用成员对象的拷贝构造
跳表010E11EA jmp CEat::CEat (10E2610h)
CEat(const CEat& obj)
010E2610 push ebp
2)派生类赋值给基类,
基类只截取了派生类中包含的部分,类似 char = ((char*)int)[0]的操作
CAnimal AnimalObj = MyCat;
013323A6 lea eax,[MyCat]
013323A9 push eax
013323AA lea ecx,[AnimalObj]
013323AD call CAnimal::CAnimal (13310FAh)
01332120 mov dword ptr [ebp-8],ecx
01332123 mov eax,dword ptr [__that]
01332126 push eax
01332127 mov ecx,dword ptr [this]
0133212A call CEat::CEat (13311EAh)
0133212F mov eax,dword ptr [this]
CCat TestObj = MyCat;
013323B2 lea eax,[MyCat]
013323B5 push eax
013323B6 lea ecx,[TestObj]
013323B9 call CCat::CCat (1331294h)
013325A0 mov dword ptr [ebp-8],ecx
013325A3 mov eax,dword ptr [__that]
013325A6 push eax
013325A7 mov ecx,dword ptr [this]
013325AA call CAnimal::CAnimal (13310FAh)
013325AF mov eax,dword ptr [this]
013325B2 mov ecx,dword ptr [__that]
013325B5 mov edx,dword ptr [ecx+4]
013325B8 mov dword ptr [eax+4],edx
013325BB mov eax,dword ptr [this]
013325BE mov ecx,dword ptr [__that]
013325C1 mov edx,dword ptr [ecx+8]
013325C4 mov dword ptr [eax+8],edx
013325C7 mov eax,dword ptr [this]
四 默认构造和拷贝构造,在虚继承情况下编译器也会插足。
虚继承和非虚继承有何变化
class CAnimal
{
public:
int m_nType;
};
class CCat : public CAnimal
{
public:
int m_nCatTest;
int m_nTest;
};
CCat MyCat;
没有任何汇编指令,其实就是当局部变量了。
class CCat : public virtual CAnimal
CCat MyCat;
00E4239E push 1
00E423A0 lea ecx,[MyCat]
00E423A3 call CEat::CEat (0E412A8h)
CCat::CCat:
00E41680 mov dword ptr [ebp-8],ecx
00E41683 cmp dword ptr [ebp+8],0
00E41687 je CCat::CCat+32h (0E41692h)
00E41689 mov eax,dword ptr [this]
00E4168C mov dword ptr [eax],offset CCat::`vbtable' (0E4783Ch) //原来增加了虚表,这个返回到上面的情况了
00E41692 mov eax,dword ptr [this]
因为虚继承需要初始化虚表,所有这种情况又回到了上面有虚表的情况,编译器会增加默认构造,需要时也会有默认拷贝构造。
五 初始化列表
使用参数列表初始化的情况
1) 初始化引用
2) 初始化const成员
3) 调用基类或成员对象的非默认构造(代码参数的)
class CAnimal
{
public:
CAnimal()
{
strncpy(m_szName, "Test!", sizeof(m_szName));
}
virtual ~CAnimal()
{
_asm NOP
}
virtual bool isCanSay(){return false;}
public:
char m_szName[16];
static int m_snType;
};
int CAnimal::m_snType = 0x12345678;
class CCat : public CAnimal
{
public:
CCat():m_nCatTest(0x123)
{
m_snType = 0x456;
memset(m_szName, 0, sizeof(m_szName));
}
virtual bool isEatFish(){return true;}
int m_nCatTest;
};
CCat MyCat;
01264990 lea ecx,[ebp-2Ch]
01264993 call CCat::CCat (12612B7h)
01264998 mov dword ptr [ebp-4],0
CCat():m_nCatTest(0x123)
{
01263600 mov dword ptr [ebp-8],ecx
01263603 mov ecx,dword ptr [this]
01263606 call CAnimal::CAnimal (1261280h) //调用基类的构造
0126360B mov eax,dword ptr [this]
0126360E mov dword ptr [eax],offset CCat::`vftable' (1267B40h) //保存虚表
01263614 mov eax,dword ptr [this]
01263617 mov dword ptr [eax+14h],123h //初始化列表
m_snType = 0x456;
0126361E mov dword ptr [CAnimal::m_snType (126A00Ch)],456h //构造函数内
memset(m_szName, 0, sizeof(m_szName));
01263628 push 10h
0126362A push 0
0126362C mov eax,dword ptr [this]
0126362F add eax,4
01263632 push eax
01263633 call @ILT+170(_memset) (12610AFh)
01263638 add esp,0Ch
处理类表的顺序不是按列表的写的前后顺序而是按照声明的先后顺序,这里看下
class CCat : public CAnimal
{
public:
CCat():m_nTest(0x123),m_nCatTest(m_nTest)
{
m_snType = 0x456;
memset(m_szName, 0, sizeof(m_szName));
}
virtual bool isEatFish(){return true;}
int m_nCatTest;
int m_nTest;
};
00BA3614 mov eax,dword ptr [this]
00BA3617 mov ecx,dword ptr [this]
00BA361A mov edx,dword ptr [ecx+18h]
00BA361D mov dword ptr [eax+14h],edx //m_nCateTest
00BA3620 mov eax,dword ptr [this]
00BA3623 mov dword ptr [eax+18h],123h //m_nTest
m_snType = 0x456;
看下监视窗口中的值(图一)
有继承关系的构造顺序是先构造继承的最顶端(也就是祖先),也是排资论辈的,析构是相反顺序,初始化列表是按顺明顺序,基类的会更早被初始化。
另外在调用父类构造时如果没有参数则无参构造,否则就要调用带参构造
在实际情况中最好使用显示的构造函数,如果确实不需要,为了避免一些隐患(浅拷贝)可以将构造函数私有.
五边形(2011年6月25日-26日 双休日)
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!