1. 构造函数
1.1 概念
常用来完成对象生成时的数据初始化工作,支持函数重载,不可定义返回值,返回值为对象首地址,即this指针
拷贝构造函数:参数为对象地址,返回值为this指针
1.2 构造函数的调用时机
1.2.1 局部对象
在汇编里,关于局部对象的构造函数的识别的充分条件和必要条件
- 识别的必要条件
1)该成员函数是这个对象在作用域内调用的第一个成员函数,根据this指针可以区分每个对象
2)这个函数返回this指针
3)这个成员函数是通过thiscall方式调用的 - 识别的充分条件
虚表指针初始化
1.2.2 堆对象
-
堆对象的申请过程
CNumber* pNumber = new CNumber;
1)new申请堆空间,需要调用构造函数,完成对象的数据成员初始化过程。
2)VS编译器检查堆空间的申请结果,产生一个双分支结构,以决定是否触发构造函数。(如果堆空间申请失败,则会避开构造函数的调用。如果new运算执行成功,返回值为对象的首地址,否则为NULL。)
注意:在GCC和Clang编译器中并不检查构造函数的返回值,应当注意区别
C++示例代码:
#include <stdio.h> class Person { public: Person() { age = 20; } int age; }; int main(int argc, char* argv[]) { Person *p = new Person; //为了突出本节讨论的问题,这里没有检查new运算的返回值 p->age = 21; printf("%d\n", p->age); return 0; }
vs_x86汇编标识:
//vs_x86 00401006 push 4 ;压入类的大小,用于堆内存申请 00401008 call sub_4010FA ;调用new函数 0040100D add esp, 4 00401010 mov [ebp-4], eax ;使用临时变量保存new返回值 00401013 cmp dword ptr [ebp-4], 0;检测堆内存是否申请成功 00401017 jz short loc_401026; ;申请失败则跳过构造函数调用 00401019 mov ecx, [ebp-4] ;申请成功,将对象首地址传入ecx 0040101C call sub_401060 ;调用构造函数 00401021 mov [ebp-8], eax ;构造函数返回this指针,保存到临时变量ebp-8中 00401024 jmp short loc_40102D 00401026 mov dword ptr [ebp-8], 0;申请堆空间失败,设置指针值为NULL 0040102D mov eax, [ebp-8] 00401030 mov [ebp-0Ch], eax ;当没有打开/02时,对象地址将在几个临时变量中倒 ;换,最终保存到[ebp-0Ch]中,这是指针变量p 00401033 mov ecx, [ebp-0Ch] ;ecx得到this指针 00401036 mov dword ptr [ecx], 15h;为成员变量age赋值21 0040103C mov edx, [ebp-0Ch] 0040103F mov eax, [edx] 00401041 push eax ;参数2,p->age 00401042 push offset aD ;参数1,"%d\n" 00401047 call sub_4010C0 ;调用printf函数 0040104C add esp, 8
gcc_x86汇编标识:
0000000F mov dword ptr [esp], offset loc_4 ;压入类的大小,用于堆内存申请 00000016 call __Znwj ;调用new函数 0000001B mov ebx, eax ;保存new返回值 0000001D mov ecx, ebx ;将对象首地址传入ecx 0000001F call __ZN6PersonC1Ev ;调用构造函数 00000024 mov [esp+1Ch], ebx ;this指针存到[ebp-1Ch]中,这是指针变量p 00000028 mov eax, [esp+1Ch] ;eax得到this指针 0000002C mov dword ptr [eax], 15h ;为成员变量age赋值21 00000032 mov eax, [esp+1Ch] 00000036 mov eax, [eax] 00000038 mov [esp+4], eax ;参数2,p->age 0000003C mov dword ptr [esp], offset aD ;参数1,"%d\n" 00000043 call _printf ;调用printf函数 00000048 mov eax, 0 0000004D mov ebx, [ebp-4] 00000050 leave 00000051 retn
clang_x86汇编标识:
00401014 mov dword ptr [esp], 4 ;压入类的大小,用于堆内存申请 0040101B mov [ebp-10h], eax 0040101E mov [ebp-14h], ecx 00401021 call sub_401170 ;调用new函数 00401026 mov ecx, eax ;将对象首地址传入ecx 00401028 mov [ebp-18h], eax ;this指针保存到临时变量ebp-18h中 0040102B call sub_401070 ;调用构造函数 00401030 mov ecx, [ebp-18h] 00401033 mov [ebp-0Ch], ecx ;this指针存到[ebp-Ch]中,这是指针变量p 00401036 mov edx, [ebp-0Ch] ;edx得到this指针 00401039 mov dword ptr [edx], 15h ;为成员变量age赋值21 0040103F mov edx, [ebp-0Ch] 00401042 mov edx, [edx] 00401044 lea esi, aD 0040104A mov [esp], esi ;参数1,"%d\n" 0040104D mov [esp+4], edx ;参数2,p->age 00401051 mov [ebp-1Ch], eax 00401054 call sub_401090 ;调用printf函数 00401059 xor ecx, ecx 0040105B mov [ebp-20h], eax 0040105E mov eax, ecx 00401060 add esp, 24h
-
识别堆对象的构造函数
分析双分支结构,找到new运算的调用后,立即在下文寻找判定new返回值的代码,在判定成功(new 的返回值非0)的分支处可定位并得到构造函数。 -
申请对象数组和调用有参构造函数的区别
int *pInt = new int (10);//调用有参构造函数 int *pInt = new int [10];//申请对象数组
1.2.3 参数对象
当对象作为函数参数时,调用复制构造函数。在进入函数前使用拷贝构造函数
-
复制构造函数传参加&
如果不加&,直接传对象进去,压参push不会调用构造函数,但是在函数执行完pop后,在复制构造函数调用完后直接将所传的对象销毁,调用一次析构函数
-
浅拷贝
如果在函数调用时传递参数对象,参数会进行复制,形参是实参的副本,相当于拷贝构造了一个全新的对象
(定义了新对象,会触发拷贝构造,实现对象间数据的复制,在没有定义拷贝构造函数的情况下,编译器会对原对象与拷贝对象的各数据成员直接进行赋值,即默认构造函数)
如果是浅拷贝,当类中有资源申请,并以数据成员来保存这些资源时,浅拷贝只是将地址复制了过去而没有进行相应的数据处理
-
深拷贝
源对象中的数据成员间接访问到的其他资源并制作副本的拷贝构造函数(自己提供拷贝构造函数,处理源对象的各数据成员还有他们所指向的资源数据)
对对象中的数据成员所指向的堆空间数据也进行了数据复制,因此当参数对象被销毁时,释放的堆空间数据是拷贝对象所制作的数据副本,对源对象没有任何影响
深拷贝示例c++代码:
#include <stdio.h> #include <string.h> class Person { public: Person() { name = NULL;//无参构造函数,初始化指针 } Person(const Person& obj) { // 注:如果在复制构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,这属于浅拷贝 // this->name = obj.name; // 为实参对象中的指针所指向的堆空间制作一份副本,这就是深拷贝了 int len = strlen(obj.name); this->name = new char[len + sizeof(char)]; // 为便于讲解,这里没有检查指针 strcpy(this->name, obj.name); } void setName(const char* name) { int len = strlen(name); if (this->name != NULL) { delete [] this->name; } this->name = new char[len + sizeof(char)]; // 为便于讲解,这里没有检查指针 strcpy(this->name, name); } public: char * name; }; void show(Person person){ // 参数是对象类型,会触发复制构造函数 printf("name:%s\n", person.name); } int main(int argc, char* argv[]) { Person person; person.setName("Hello"); show(person); return 0; }
vs_x86汇编标识:
00401026 lea ecx, [ebp-4] ;ecx=&person 00401029 call sub_4010D0 ;调用构造函数 0040102E push offset aHello;参数1,"Hello" 00401033 lea ecx, [ebp-4] ;ecx=&person 00401036 call sub_401130 ;调用成员函setName 0040103B push ecx ;等价于“sub esp,4”,但是push ecx的机器码更短,效率更高,Person的类型长度为4字节, ;所以传递参数对象时要在栈顶留下4字节,以作为参数对象的空间,此时esp保存的内容为参数对象的地址 0040103C mov ecx, esp ;获取参数对象的地址,保存到ecx中 0040103E lea eax, [ebp-4] ;获取对象person的地址并保存到eax中 00401041 push eax ;参数1,将person地址作为参数 00401042 call sub_401070 ;调用复制构造函数 00401047 call sub_401000 ;此时栈顶上的参数对象传递完毕,开始调用show函数 0040104C add esp, 4 0040104F mov dword ptr [ebp-8], 0 00401056 lea ecx, [ebp-4] ;ecx=&person 00401059 call sub_4010F0 ;调用对象person的析构函数 0040105E mov eax, [ebp-8] 00401061 mov esp, ebp 00401063 pop ebp 00401064 retn 00401070 push ebp ;复制构造函数 00401071 mov ebp, esp 00401073 sub esp, 0Ch 00401076 mov [ebp-4], ecx ;[ebp-4]保存this指针 00401079 mov eax, [ebp+8] ;eax=&obj 0040107C mov ecx, [eax] ;ecx=obj.name 0040107E push ecx ;参数1 0040107F call sub_404AA0 ;调用strlen函数 00401084 add esp, 4 00401087 mov [ebp-8], eax ;len=strlen(obj.name) 0040108A mov edx, [ebp-8] 0040108D add edx, 1 00401090 push edx ;参数1,len +1 00401091 call sub_40121A ;调用new函数 00401096 add esp, 4 00401099 mov [ebp-0Ch], eax 0040109C mov eax, [ebp-4] 0040109F mov ecx, [ebp-0Ch] 004010A2 mov [eax], ecx ;this->name = new char[len + sizeof(char)]; 004010A4 mov edx, [ebp+8] 004010A7 mov eax, [edx] 004010A9 push eax ;参数2,obj.name 004010AA mov ecx, [ebp-4] 004010AD mov edx, [ecx] 004010AF push edx ;参数1,this->name 004010B0 call sub_4049A0 ;调用strcpy函数 004010B5 add esp, 8 004010B8 mov eax, [ebp-4] ;返回this指针 004010BB mov esp, ebp 004010BD pop ebp 004010BE retn 4 00401000 push ebp ;show函数 00401001 mov ebp, esp 00401003 mov eax, [ebp+8] 00401006 push eax ;参数2,person.name 00401007 push offset aNameS ;参数1,"name:%s\n" 0040100C call sub_4011E0 ;调用printf函数 00401011 add esp, 8 00401014 lea ecx, [ebp+8] ;ecx=&person 00401017 call sub_4010F0 ;调用析构函数
1.2.4 返回对象
函数返回时需要对返回对象进行拷贝,调用拷贝构造函数
-
temp1=ret();,在ret函数中return的时候就已经把ret里的参数对象的值复制给了temp1,即调用了一次拷贝构造函数
-
运算符重载
Point v1; Point v2; Point v3; v3=v1+v2; //相当于对象v1调用函数+传参v2,返回值为v3
vs编译器:在函数返回之前,利用复制构造函数将函数中局部对象的数据复制到参数指向的对象中,起到了返回对象的作用
gcc和clang编译器:优化了复制构造函数的调用,与直接构造参数对象等价:Person* getObject(Person* p);
返回对象与返回指针类型的区别:
- 返回对象,在函数中使用构造函数
- 返回值和参数是对象指针类型的函数,不会使用以参数为目标的构造函数,而是直接使用指针保存对象首地址
1.2.5 全局对象及静态对象
程序中所有全局对象会在 _cinit 函数(mainCRTStartup 函数中调用该函数)调用构造函数以初始化数据
-
全局对象的初始化地址
在函数_cinit的_initterm函数调用中,初始化了全局对象
while ( pfbegin< pfend ){ if (*pfbegin != NULL ) **pfbegin) ();/调用每一个初始化或构造代理函数 ++pfbegin; }
执行(**pfbegin)();后并不进入全局对象的构造函数,编译器为每个全局对象生成一段传递this指针和参数的代码,然后使用无参的代理函数去调用构造函数
以上这一块主要是 mainCRTStartup 的内容,关于 mainCRTStartup 函数的解析,之后我会把相关笔记整理到博客上
对于全局对象和静态对象,能不能取消代理函数,直接在main()函数前调用构造函数?
因为构造函数可以重载,所以其参数的类型、个数和顺序都无法预知,也就无法预先定义构造函数。编译器为每个全局对象分别生成构造代理函数,由代理函数调用各类参数和约定的构造函数。因为代理函数的类型被统一指定为PVFV:typedef void ( cdecl *_PVFV)(void); 所以能通过数组统一地管理和执行 -
全局对象构造函数的识别
1)直接定位初始化函数- 进入mainCRTStartup函数,找到初始化函数_cinit,在_cinit函数的第二个_initterm处设置断点。
- 运行程序后,进入_initterm的实现代码内,断点在(**it)();执行处,单步进入代理构造,即可得到全局对象的构造函数。
2)利用栈回溯
如果出现全局对象,由于全局对象的地址固定(对于有重定位表的执行文件中的全局对象,也可以在执行文件被加载后至执行前计算得到全局对象的地址) 可以在对象的数据成员中设置读写断点,调试运行程序,等待构造函数调用的到来。利用栈回溯窗口,找到程序的执行流程,依次向上查询即可找到构造函数调用的起始处。
3)对atexit设置断点,因为构造代理函数中会注册析构函数,其注册的方式是使用atexit
1.2.6 编译器提供默认构造函数的两种情况
-
父类、本类中定义的成员对象或者父类中有虚函数存在
需要在构造函数中完成虚表的初始化
-
父类或本类中定义的成员对象带有构造函数
派生类构造顺序是先构造父类再构造自身,当父类中带有构造函数时,将会调用父类构造函数,这个调用过程在构造函数内完成,编译器添加默认的构造函数来完成
2. 析构函数
2.1 概念
析构函数则常用于对象销毁时释放对象中所申请的资源,无参函数
2.2 析构函数的调用时机
2.2.1 局部对象
调用时机:作用域结束前调用析构函数
析构函数:不支持函数重载,只有一个参数,即this指针,编译器隐藏了这个参数的传递过程,无返回值
2.2.2 堆对象
调用时机:释放堆空间前调用析构函数
在释放过程中,需要使用析构代理函数间接调用析构函数(如果直接调用析构函数,则无法完成多对象的析构)
单个对象的申请和释放:
1)申请:调用new申请内存空间,编译器判断是否申请成功,如果成功调用构造函数
2)释放:判断之前申请内存空间是否成功,成功即调用构造代理函数,在构造代理函数中调用析构函数,检查析构函数标志,调用delete
注意:
1)只有vs编译器new时有判断申请的内存空间是否成功的过程
2)单个对象的释放delete不可以添加符号“[]”,因为会把delete函数的目标指针减4或者8,释放单个对象的空间时就会发生错误,当执行到delete函数时会产生堆空间释放错误
C++示例代码:
#include <stdio.h>
class Person {
public:
Person() {
age = 20;
}
~Person() {
printf("~Person()\n");
}
int age;
};
int main(int argc, char* argv[]) {
Person *person = new Person();
person->age = 21; //为了便于讲解,这里没检查指针
printf("%d\n", person->age);
delete person;
return 0;
}
x86_vs汇编标识:
00401006 push 4 ;参数1
00401008 call sub_40116A ;调用new函数申请内存空间 ①
0040100D add esp, 4
00401010 mov [ebp-8], eax ;保存申请的内存地址到临时变量
00401013 cmp dword ptr [ebp-8], 0
00401017 jz short loc_401026 ;检查内存空间是否申请成功 ②
00401019 mov ecx, [ebp-8] ;传递this指针
0040101C call sub_401080 ;申请内存成功,调用构造函数 ③
00401021 mov [ebp-0Ch], eax ;保存构造函数返回值到临时变量
00401024 jmp short loc_40102D
00401026 mov dword ptr [ebp-0Ch], 0 ;申请内存失败,赋值临时变量NULL
0040102D mov eax, [ebp-0Ch]
00401030 mov [ebp-4], eax ;保存申请的地址到指针变量person
00401033 mov ecx, [ebp-4] ;ecx=person
00401036 mov dword ptr [ecx], 15h ;person->age=21
0040103C mov edx, [ebp-4]
0040103F mov eax, [edx]
00401041 push eax ;参数2,person->age
00401042 push offset aD ;参数1,"%d\n"
00401047 call sub_401130 ;调用printf函数
0040104C add esp, 8
0040104F mov ecx, [ebp-4]
00401052 mov [ebp-10h], ecx
00401055 cmp dword ptr [ebp-10h], 0
00401059 jz short loc_40106A ;检查内存空间是否申请成功 ④
0040105B push 1 ;标记
0040105D mov ecx, [ebp-10h] ;传递this指针
00401060 call sub_4010C0 ;内存申请成功,调用析构代理函数 ⑤
00401065 mov [ebp-14h], eax
00401068 jmp short loc_401071
0040106A mov dword ptr [ebp-14h], 0
00401071 xor eax, eax
00401073 mov esp, ebp
00401075 pop ebp
00401076 retn
004010C0 push ebp ;析构代理函数
004010C1 mov ebp, esp
004010C3 push ecx
004010C4 mov [ebp-4], ecx
004010C7 mov ecx, [ebp-4] ;传递this指针
004010CA call sub_4010A0 ;调用析构函数 ⑥
004010CF mov eax, [ebp+8]
004010D2 and eax, 1
004010D5 jz short loc_4010E5 ;检查析构函数标记,以后讲多重继承时会详谈
004010D7 push 4
004010D9 mov ecx, [ebp-4]
004010DC push ecx ;参数1,堆空间的首地址
004010DD call sub_40119A ;调用delete函数,释放堆空间 ⑦
004010E2 add esp, 8
004010E5 mov eax, [ebp-4]
004010E8 mov esp, ebp
004010EA pop ebp
004010EB retn 4
多个对象的申请和释放:
1)申请对象数组
对象都在同一个堆空间中,32位程序编译器使用了堆空间的前4字节数据保存对象的总个数,64位程序编译器使用了堆空间的前8字节数据保存对象的总个数
2)对象数组的delete
使用delete,当数组元素为基本数据类型时不会出错
使用delete[],当数组元素为对象时不会出错
3)构造代理函数的过程
①类对象产生时,调用构造函数来初始化对象中的数据,用到了代理函数,根据对象数组的元素逐个调用他们的构造函数,完成初始化过程
②根据数组中对象总个数,从堆数组中的第一个对象的首地址开始,依次向后遍历数组中每个对象
③将数组中每个对象的首地址作为this指针逐个调用构造函数
编译器的区别:
①vs_x86 Debug while循环
②gcc_x86 Debug while循环
③clang_x86 Debug do while循环
4)堆对象释放函数的过程
堆对象在析构过程中没有直接调用代理函数,而是插入了中间的检测,检查参数是否为对象数组。
①释放单个堆对象时,向中间处理函数传入参数1作为释放标志。堆空间中只有一个堆对象,没有记录对象个数的数据存在,可直接调用对象的析构函数并释放堆空间。
②释放对象数组时,delete[]。在堆空间释放时传入释放标记3。
a.执行到中间的检测时,判断标记为3,调用析构函数代理,完成所有堆对象的析构调用过程
析构代理函数:根据数组中对象总个数,从最后一个对象的首地址开始,依次向前遍历数组中的每个对象,将数组中每个对象的首地址作为this指针逐个调用析构函数
b.根据释放堆空间的标志判断是否释放内存
c.把delete的目标指针减4,修正为堆空间的首地址,调用delete函数释放内存空间
注意:
释放对象类型标志,1为单个对象,3为释放对象数组,0表示仅仅执行析构函数,不释放雉空间
这个标志占2位,使用delete[]时标志为二进制11,直接使用delete时标志为二进制01
c++示例代码:
#include <stdio.h>
class Person {
public:
Person() {
age = 20;
}
~Person() {
printf("~Person()\n");
}
int age;
};
int main(int argc, char* argv[]) {
Person *objs = new Person[3]; //申请对象数组
delete[] objs; //释放对象数组
return 0;
}
vs_x86汇编标识:
00401006 push 10h ;每个对象占4字节,却申请了16字节大小的空间,
;在申请对象数组时,会使用堆空间的首地址处的4字节内容保存对象总个数
00401008 call sub_401238 ;调用new函数 ①
0040100D add esp, 4
00401010 mov [ebp-4], eax ;[ebp-4]保存申请的堆空间的首地址
00401013 cmp dword ptr [ebp-4], 0
00401017 jz short loc_401042 ;检查堆空间的申请是否成功 ②
00401019 mov eax, [ebp-4]
0040101C mov dword ptr [eax], 3 ;设置首地址的4字节数据为对象个数 ③
00401022 push offset sub_401080 ;参数4,构造函数的地址,作为构造代理函数参数
00401027 push 3 ;参数3,对象个数,作为函数参数
00401029 push 4 ;参数2,对象大小,作为函数参数
0040102B mov ecx, [ebp-4]
0040102E add ecx, 4 ;跳过首地址的4字节数据
00401031 push ecx ;参数1,第一个对象地址,作为函数参数
00401032 call sub_401140 ;构造代理函数调用 ④
00401037 mov edx, [ebp-4]
0040103A add edx, 4 ;跳过堆空间首4字节的数据
0040103D mov [ebp-8], edx ;保存堆空间中的第一个对象的首地址
00401040 jmp short loc_401049 ;跳过申请堆空间失败的处理
00401042 mov dword ptr [ebp-8], 0 ;申请堆空间失败,赋值空指针
00401049 mov eax, [ebp-8]
0040104C mov [ebp-10h], eax
0040104F mov ecx, [ebp-10h]
00401052 mov [ebp-0Ch], ecx ;堆空间中的第一个对象的首地址
00401055 cmp dword ptr [ebp-0Ch], 0
00401059 jz short loc_40106A ;检查对象指针是否为NULL
0040105B push 3 ;参数2,释放对象类型标志,1为单个对象,3为释放对象数组,
;0表示仅执行析构函数,不释放堆空间
;这个标志占2位,使用delete[]时标志为二进制11,直接用delete为二进制01
0040105D mov ecx, [ebp-0Ch] ;参数1,释放堆对象首地址
00401060 call sub_4010C0 ;释放堆对象函数 ⑤
00401065 mov [ebp-14h], eax
00401068 jmp short loc_401071
0040106A mov dword ptr [ebp-14h], 0
00401071 xor eax, eax
00401073 mov esp, ebp
00401075 pop ebp
00401076 retn
vs_x86构造代理函数:
;调用此函数时,共压入5个参数,还原参数原型为:
sub_401140(void * objs, //第一个对象所在堆空间的首地址
int size, //对象占用内存空间的大小
int count, //对象个数
void (*pfn)(void)) //通过thiscall方式构造函数指针
00401140 push ebp
00401141 mov ebp, esp
00401143 push ecx
00401144 mov eax, [ebp+10h]
00401147 mov [ebp-4], eax
0040114A mov ecx, [ebp+10h] ;获得对象个数
0040114D sub ecx, 1
00401150 mov [ebp+10h], ecx ;count--
00401153 cmp dword ptr [ebp-4], 0
00401157 jbe short loc_40116A ;循环判断部分,如果count不为0,继续循环
00401159 mov ecx, [ebp+8] ;获取对象所在堆空间的首地址,使用ecx传递this指针
0040115C call dword ptr [ebp+14h] ;调用构造函数
0040115F mov edx, [ebp+8] ;edx作为对象数组元素的指针,edx=objs
00401162 add edx, [ebp+0Ch] ;edx=edx+size
00401165 mov [ebp+8], edx ;修改指针,使其指向下一对象的首地址
00401168 jmp short loc_401144
0040116A mov esp, ebp ;结束循环结构,完成构造函数的调用过程
0040116C pop ebp
0040116D retn 10h
2.2.3 参数对象和返回对象
参数对象的析构函数调用时机:退出函数前,调用参数对象的析构函数
返回对象的析构函数调用时机:如无对象引用定义,退出函数后,调用返回对象的析构函数,否则与对象引用的作用域一致
1)函数的参数为对象,函数调用结束后会调用它的析构函数,然后释放掉参数对象所占的内存空间。
2)当返回值为对象
①CMystring MyString =GetMystring();
把MyString的地址作为隐含参数传给GetMyString(),在GetMyString()内部完成拷贝构造。函数执行完毕后,MyString构造完成,所以析构函数由MyString的作用域来决定
②Mystring = GetMystring();
不会触发MyString的拷贝构造函数,产生临时对象作为GetMyString()隐含参数,临时对象会在GetMyString()内部完成拷贝构造函数。
函数执行完毕后,如果MyString的类中定义“=”运算符重载,则调用﹔否则根据对象成员逐个赋值。如果对象内数据量过大,调用rep movs串操作指令批量赋值,属于浅拷贝。
注意:
①一旦分号出现,就会触发临时对象的析构函数
②特殊情况:当引用这个临时对象时,它的生命期会和引用一致,如:Number = getNumber(), printf("Hello\n");
逗号运算符后是printf调用,于是临时对象的析构在printf函数执行完毕后才会触发
2.2.4 全局对象与静态对象
调用时机:在main函数执行完毕之后,出现在程序结束处
exit终止程序
mainCRTStartup 函数在调用main函数结束后使用了exit用来终止程序,全局对象的析构函数的调用也在其中,由exit函数内的_execute_onexit_table实现:
_PVFV* saved_first = first;
_PVFV* saved_last = last;
for (;;)
{
//从后向前依次释放全局对象
_PVFV const function = __crt_fast_decode_pointer(*last);
*last = encoded_nullptr;
//调用保存的函数指着
function();
}
//调用__crt_fast_decode_pointer函数可以获取保存各类资源释放函数的首地址
2.2.5 析构代理函数的注册地点
在执行每个全局对象构造代理函数时会先执行对象的构造函数,使用atexit注册析构代理函数
2.2.6 析构代理函数
编译器需要为每个全局和静态对象建立中间代理的析构函数,传入全局对象的this指针。因为在数组中保存的析构代理函数为无参函数,在调用析构函数时无法传递this指针。
3. 在汇编中如何区分构造和析构
构造:构造函数出现在析构函数之前,虚表指针没有指向虚表的首地址
析构:析构函数出现在所有成员函数之后,在实现过程中,虚表指针已经指向了某一个虚表的首地址
充分条件:
构造函数:
- 进行虚表指针的初始化
析构函数:
- 还原虚表指针,让其指向自身的虚表首地址,防止在析构函数中调用虚函数时取到非自身虚表,从而导致函数调用错误
必要条件:
构造函数:
- 函数的调用是这个对象在作用域内的第一次成员函数调用,分析this指针即可区分对象,是哪个对象的this指针就是哪个对象的成员函数。
- 使用thiscall调用方式,使用ecx或者rcx传递this指针,返回值为this指针。
析构函数:
- 函数的调用是这个对象在作用域内的最后一次成员函数调用,分析this指针即可区分对象,是哪个对象的this指针就是哪个对象的成员函数。
- 使用thiscall调用方式,使用ecx或者rcx传递this指针,没有返回值。