构造函数与析构函数是类的重要组成部分,构造函数常用来完成对象生成时的数据初始化工作,而析构函数则常用于在对象销毁时释放对象中所申请的资源;
当对象生成时,编译器会自动产生调用其类构造函数的代码,在编译过程中可以为类中的数据成员赋予恰当的初值。当对象销毁后,编译器调用析构函数;
运行环境:
- 操作系统: Windows 7家庭版
 
- 编译器:VC6 VS2013
 
构造析构
构造函数出现时机
对象生成时会自动调用构造函数,只要找到了定义对象的地方就找了构造函数的调用时机。不同作用域对象声明周期不同。
将对象进行分类:不同类型的对象的构造函数被调用的时机会发生变化,但都会遵循C++语法;定义的同时调用构造函数,那么只要知道了对象的生命周期,便可以推断出构造函数的调用时机;
局部对象
局部对象下的构造函数的出现实际比较容易识别。当对象产生时,便有可能引发构造函数的调用。编译器隐藏了构造函数的调用过程;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
   | 21:   int main(int argc, char* argv[]) 22:   { 00401060   push        ebp 00401061   mov         ebp,esp 00401063   sub         esp,44h 00401066   push        ebx 00401067   push        esi 00401068   push        edi 00401069   lea         edi,[ebp-44h] 0040106C   mov         ecx,11h 00401071   mov         eax,0CCCCCCCCh 00401076   rep stos    dword ptr [edi] 23:       CNumber Number; 00401078   lea         ecx,[ebp-4]			;获得对象首地址,传入ecx中作为参数 0040107B   call        @ILT+0(CNumber::CNumber) (00401005) 24:       return 0; 00401080   xor         eax,eax 25:   }
 
  16:   CNumber::CNumber() 17:   { 00401020   push        ebp 00401021   mov         ebp,esp 00401023   sub         esp,44h 00401026   push        ebx 00401027   push        esi 00401028   push        edi 00401029   push        ecx 0040102A   lea         edi,[ebp-44h] 0040102D   mov         ecx,11h 00401032   mov         eax,0CCCCCCCCh 00401037   rep stos    dword ptr [edi] 00401039   pop         ecx						;还原ecx 0040103A   mov         dword ptr [ebp-4],ecx	;this指针 18:       m_nNumber = 1; 0040103D   mov         eax,dword ptr [ebp-4]	;[ebp-4]为this指针,eax保存了对象的首地址 00401040   mov         dword ptr [eax],1		;偏移量为0,所以eax不需要加 19:   } 00401046   mov         eax,dword ptr [ebp-4]
 
   | 
 
当在进入对象的作用域时,编译器会产生调用构造函数的代码。由于构造函数属于成员函数,因此在调用过程中需要传递this指针。构造函数调用结束后,会将this指针作为返回值;
- 该成员函数是这个对象在作用域内调用的第一个成员函数,根据this指针即可区分每个对象;
 
- 这个函数必须返回this指针
 
- 使用其他调用约定,编译器会提示调用约定无效
 
- 构造函数返回的值不被调用方使用
 
堆对象
堆对象的识别重点在于识别堆空间的申请与释放。在C++中堆申请需要使用malooc、new运算或其他具有相同功能的函数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
   | class CNumber { public: 	int i; 	int j; 	CNumber(); 	~CNumber(); protected: private:
  };
  CNumber::CNumber() { 	; }
  CNumber::~CNumber() { 	; }
  int main(int argc, char* argv[]) { 	CNumber *pNumber = NULL; 	pNumber = new CNumber;
  	pNumber->i = 100; 	printf("%d\n",pNumber->i); 	return 0; }
  30:       CNumber *pNumber = NULL; 004010BD   mov         dword ptr [ebp-10h],0				;给指针赋值为0 31:       pNumber = new CNumber; 004010C4   push        8									;压入类的大小 004010C6   call        operator new (00401280)				;函数重载,后续介绍 004010CB   add         esp,4								; 004010CE   mov         dword ptr [ebp-18h],eax				;使用临时变量保存new的返回值,new返回值为对象首地址 004010D1   mov         dword ptr [ebp-4],0					;[ebp-4]保存堆申请次数 004010D8   cmp         dword ptr [ebp-18h],0				;检测堆空间是否申请成功 004010DC   je          main+5Bh (004010eb)					;失败则跳过构造函数 004010DE   mov         ecx,dword ptr [ebp-18h]				;使用ecx传递this指针 004010E1   call        @ILT+0(CNumber::CNumber) (00401005)	;构造函数 004010E6   mov         dword ptr [ebp-1Ch],eax				;构造函数返回this指针 004010E9   jmp         main+62h (004010f2)					;编译器产生双分支结构,用于检查new是否成功 004010EB   mov         dword ptr [ebp-1Ch],0				;堆申请失败,设置指针值为NULL 004010F2   mov         eax,dword ptr [ebp-1Ch]				; 004010F5   mov         dword ptr [ebp-14h],eax 004010F8   mov         dword ptr [ebp-4],0FFFFFFFFh 004010FF   mov         ecx,dword ptr [ebp-14h] 00401102   mov         dword ptr [ebp-10h],ecx 32: 33:       pNumber->i = 100; 00401105   mov         edx,dword ptr [ebp-10h]				;edx获得this指针 00401108   mov         dword ptr [edx],64h					;对成员函数赋值 34:       printf("%d\n",pNumber->i); 0040110E   mov         eax,dword ptr [ebp-10h] 00401111   mov         ecx,dword ptr [eax] 00401113   push        ecx 00401114   push        offset string "%d\n" (0042501c) 00401119   call        printf (00401200) 0040111E   add         esp,8
 
   | 
 
如果堆申请失败,则会避开调用构造函数,因为在C++语法中,new成功后,返回值为对象的首地址,否则为NULL。因此编译器需要检查堆空间申请结果,产生一个双分支,以此决定是否触发构造函数。
全局对象
- debug版本下在
_cinit函数的第二个_initterm函数中的函数指针,有个$e4开头的标号 
- $e4:构造函数的代理的代理,先执行构造函数的代理然后执行析构函数的代理的代理
- $e1:执行构造函数代理
 
- $e3:注册析构函数的代理的代理
- $e2:析构函数代理,调用atexit内部的函数指针
 
 
 
- $E4(构造函数的代理的代理)->$E1(构造函数的代理)。析构在$E4->$E3(注册析构函数代理的代理)->$E2(析构函数的代理.调用atexit(offset E2)
 

快速识别方法:

全局对象与静态对象
全局对象与静态对象的构造时机相同,它们的构造函数的调用被隐藏起来,但识别很容易。因为程序中所有全局对象将会在同一地点调用构造函数以初始化数据。
构造函数需要传递对象的首地址作为this指针,而且构造函数可以带各类参数。
- 直接定位初始化函数
- 先进入MainCRTStartup函数中,然后找到初始化函数_cinit,在_cinit函数的第二个__initterm处设置断点。
 
 
- 栈回溯
 
对象数组
构造
- new[]指针往上4个字节可以看到对象总个数,增加是正序增加
 
- delete[]会逆序释放
 
- 对象数组构造的五个参数
- 对象数组的首地址
 
- 构造函数的指针
 
- 数组对象的总个数
 
- 对象的大小
 
- 析构函数指针
 
 
析构
- 会无条件执行析构函数
 
- 对象数组析构必要条件
- 对象数组的首地址
 
- 对象的大小
 
- 对象的数量
 
- 析构函数的地址
 
- 当对象数组的时候多了一个位表示是否数组对象
 
 

地址004014D7的位置为对象数组的首地址
地址004014DC的位置为构造函数的指针
地址004014EA的位置为数组对象的个数
地址004014EC的位置为数组对象的大小
地址004014F4的位置为构造函数的指针,也就是this指针

析构的顺序也是一样的;

这里有一个需要注意的地方,就是00401690处的and 2指令,这条指令大概意思是取到处第二位判断,是否为对象数组;
004016B1处的and 1指令为取最低位,如果为1,则释放空间;
参数对象
参数对象属于局部对象中一种特殊的饿情况。当对象作为参数时,调用一个特殊的构造函数-拷贝构造函数。该构造函数只有一个参数,类型为对象的引用;
当对象为参数时,会触发此类对象的拷贝构造函数。如果在函数调用时传递该对象,参数会进行复制,形参是实参的副本,相当于拷贝构造了一个全新的对象。由于定义了新对象,会触发拷贝构造函数,在这个特殊的构造函数中完成对象的数据赋值;
当然我们不对构造函数进行拷贝构造函数的书写,那么编译器会为我们默认提供一个浅拷贝函数;
1 2 3 4 5 6 7 8
   | 125:      CMyString str; 0040155D   lea         ecx,[ebp-14h]			;获取str对象的首地址 00401560   call        @ILT+5(CMyString::CMyString) (0040100a)	;调用构造函数,没有拷贝构造函数 00401565   mov         dword ptr [ebp-4],0		 126:      CMyString Two(str); 0040156C   mov         eax,dword ptr [ebp-14h]		;取出对象str中数据成员信息 0040156F   mov         dword ptr [ebp-1Ch],eax		;赋值对象Two中数据成员的值
 
   | 
 
虽然编译器提供了默认浅拷贝很方便,但在某些情况下会导致程序出错;
拷贝构造函数在函数外构造,函数内析构;
返回对象
非赋值返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
   | CMyString GetString() { 	CMyString str = "Hello,World!"; 	return str; }	 	 CMyString str1 = GetString();  并非赋值,调用拷贝构造	不产生临时对象 133:      CMyString str1 = GetString(); 0040F5DD   lea         eax,[ebp-14h] 0040F5E0   push        eax 0040F5E1   call        @ILT+55(GetString) (0040103c) 0040F5E6   add         esp,4 0040F5E9   mov         dword ptr [ebp-4],0
  ;构造函数 004015F8   lea         eax,[ebp-14h] 004015FB   push        eax 004015FC   mov         ecx,dword ptr [ebp+8] 004015FF   call        @ILT+5(CMyString::CMyString) (0040100a)		;退出之前构造
 
  0040F606   mov         dword ptr [ebp-18h],0 0040F60D   mov         dword ptr [ebp-4],0FFFFFFFFh 0040F614   lea         ecx,[ebp-14h] 0040F617   call        @ILT+40(CMyString::~CMyString) (0040102d) 0040F61C   mov         eax,dword ptr [ebp-18h]
 
 
   | 
 
- 会再函数内构造,函数外析构
 
- 拷贝构造的软条件:
- 是个构造函数
 
- 该构造函数的参数是本类对象的指针
 
- 如果没有拷贝构造,则就是以参数为目标的memcpy的行为
 
 
1 2 3 4 5 6 7 8 9 10 11 12
   | CMyString GetString() {     CMyString str("teste");     return str; }
  void main() {     CMyString str = GetString();            }
   | 
 
识别方式:
- 函数的参数1为返回对象的指针
 
- 返回对象的函数返回值为参数1
 
- 再返回对象的函数内(退出前),会执行以参数1为this的拷贝构造函数,
- 如果没有拷贝构造,则执行浅拷贝(大对象有memcpy的行为,小对象寄存器赋值)
 
 
- 返回对象的析构实际再返回对象函数以外
 
隐含参数
隐藏一个返回对象的指针,做返回值;
1 2 3 4 5 6 7 8 9 10
   | CMyString GetString(&str1) { 	CMyString str = "Hello,World!"; 	return str; }	 	 CMyString str1 = GetString();  并非赋值,调用拷贝构造
 
 
 
   | 
 
拷贝构造
- 识别为构造函数
 
- 该函数的参数为本类对象的指针,有且仅有这一个参数
 
- 在对象传参的时候,调用了这个函数
 
- 在对象作为返回值的时候,调用了这个函数
 
- 拷贝构造函数的识别可以和参数对象以及返回对象相互举证
 
临时对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
   | CMyString GetString() { 	CMyString str = "Hello,World!"; 	return str; }
  int main(int argc, char* argv[]) { 	//CMyString str = "sYstemk1t"; 	//CMyString str = "sYstemk1t"; 	//CMyString str1 = GetString(); 	//printf("%d\n",str1.GetLength()); 	CMyString str1; 	str1 = GetString(); }
  135:      CMyString str1; 0040F5DD   lea         ecx,[ebp-14h]			;传递局部对象首地址 0040F5E0   call        @ILT+10(CMyString::CMyString) (0040100f)		;调用构造函数 0040F5E5   mov         dword ptr [ebp-4],0		;局部变量给0 136:      str1 = GetString(); 0040F5EC   lea         eax,[ebp-1Ch]			;临时变量 0040F5EF   push        eax						;返回局部结果 0040F5F0   call        @ILT+55(GetString) (0040103c)	;构造函数 0040F5F5   add         esp,4 0040F5F8   mov         dword ptr [ebp-24h],eax	;浅拷贝开始 0040F5FB   mov         ecx,dword ptr [ebp-24h] 0040F5FE   mov         edx,dword ptr [ecx] 0040F600   mov         eax,dword ptr [ecx+4] 0040F603   mov         dword ptr [ebp-14h],edx 0040F606   mov         dword ptr [ebp-10h],eax	;浅拷贝结束 0040F609   lea         ecx,[ebp-1Ch]			;获得首地址 0040F60C   call        @ILT+40(CMyString::~CMyString) (0040102d)	;析构局部对象
 
 
   | 
 
临时对象的生命期从函数内执行构造开始,到本条语句结束结束为止(遇到分号则析构)
无名对象
- 无名对象必须由引用去维护
 
- 此处产生无名对象,其特点为使用指针保留并维护这个返回对象的地址
 
- 无名对象的作用域和其引用的作用域定义一致,故此处不会析构这个返回对象
 
- 先识别返回对象,观察对象如何操作,拷贝构造给到第三方后观察有没有析构,如果有析构即可判定为临时对象,如果没有通过拷贝,将返回对象的指针交给第三方维护,即可判定为无名对象
 
1 2 3 4 5 6 7 8 9 10 11
   | CMyString GetString() {     CMyString str("teste");     return str; }
  void main() {     CMyString &str = GetString() ;      }
   |