析构函数和构造函数的特点(在汇编中如何识别构造和析构)

1. 构造函数

1.1 概念

​ 常用来完成对象生成时的数据初始化工作,支持函数重载,不可定义返回值,返回值为对象首地址,即this指针

拷贝构造函数:参数为对象地址,返回值为this指针

1.2 构造函数的调用时机

1.2.1 局部对象

在汇编里,关于局部对象的构造函数的识别的充分条件和必要条件

  1. 识别的必要条件
    1)该成员函数是这个对象在作用域内调用的第一个成员函数,根据this指针可以区分每个对象
    2)这个函数返回this指针
    3)这个成员函数是通过thiscall方式调用的
  2. 识别的充分条件
    虚表指针初始化

1.2.2 堆对象

  1. 堆对象的申请过程

    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
    
  2. 识别堆对象的构造函数
    分析双分支结构,找到new运算的调用后,立即在下文寻找判定new返回值的代码,在判定成功(new 的返回值非0)的分支处可定位并得到构造函数。

  3. 申请对象数组和调用有参构造函数的区别

    int *pInt = new int (10);//调用有参构造函数
    int *pInt = new int [10];//申请对象数组
    

1.2.3 参数对象

​ 当对象作为函数参数时,调用复制构造函数。在进入函数前使用拷贝构造函数

  1. 复制构造函数传参加&

    如果不加&,直接传对象进去,压参push不会调用构造函数,但是在函数执行完pop后,在复制构造函数调用完后直接将所传的对象销毁,调用一次析构函数

  2. 浅拷贝

    如果在函数调用时传递参数对象,参数会进行复制,形参是实参的副本,相当于拷贝构造了一个全新的对象

    (定义了新对象,会触发拷贝构造,实现对象间数据的复制,在没有定义拷贝构造函数的情况下,编译器会对原对象与拷贝对象的各数据成员直接进行赋值,即默认构造函数)

    如果是浅拷贝,当类中有资源申请,并以数据成员来保存这些资源时,浅拷贝只是将地址复制了过去而没有进行相应的数据处理

  3. 深拷贝

    源对象中的数据成员间接访问到的其他资源并制作副本的拷贝构造函数(自己提供拷贝构造函数,处理源对象的各数据成员还有他们所指向的资源数据)

    对对象中的数据成员所指向的堆空间数据也进行了数据复制,因此当参数对象被销毁时,释放的堆空间数据是拷贝对象所制作的数据副本,对源对象没有任何影响

    深拷贝示例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 返回对象

​ 函数返回时需要对返回对象进行拷贝,调用拷贝构造函数

  1. temp1=ret();,在ret函数中return的时候就已经把ret里的参数对象的值复制给了temp1,即调用了一次拷贝构造函数

  2. 运算符重载

    Point v1;
    Point v2;
    Point v3;
    v3=v1+v2;
    //相当于对象v1调用函数+传参v2,返回值为v3
    

vs编译器:在函数返回之前,利用复制构造函数将函数中局部对象的数据复制到参数指向的对象中,起到了返回对象的作用

gcc和clang编译器:优化了复制构造函数的调用,与直接构造参数对象等价:Person* getObject(Person* p);

返回对象与返回指针类型的区别

  • 返回对象,在函数中使用构造函数
  • 返回值和参数是对象指针类型的函数,不会使用以参数为目标的构造函数,而是直接使用指针保存对象首地址

1.2.5 全局对象及静态对象

​ 程序中所有全局对象会在 _cinit 函数(mainCRTStartup 函数中调用该函数)调用构造函数以初始化数据

  1. 全局对象的初始化地址

    在函数_cinit的_initterm函数调用中,初始化了全局对象

    while ( pfbegin< pfend ){
    	if (*pfbegin != NULL )
    		**pfbegin) ();/调用每一个初始化或构造代理函数
    	++pfbegin;
    }
    

    执行(**pfbegin)();后并不进入全局对象的构造函数,编译器为每个全局对象生成一段传递this指针和参数的代码,然后使用无参的代理函数去调用构造函数

    以上这一块主要是 mainCRTStartup 的内容,关于 mainCRTStartup 函数的解析,之后我会把相关笔记整理到博客上

    对于全局对象和静态对象,能不能取消代理函数,直接在main()函数前调用构造函数?
    因为构造函数可以重载,所以其参数的类型、个数和顺序都无法预知,也就无法预先定义构造函数。编译器为每个全局对象分别生成构造代理函数,由代理函数调用各类参数和约定的构造函数。因为代理函数的类型被统一指定为PVFV:typedef void ( cdecl *_PVFV)(void); 所以能通过数组统一地管理和执行

  2. 全局对象构造函数的识别
    1)直接定位初始化函数

    • 进入mainCRTStartup函数,找到初始化函数_cinit,在_cinit函数的第二个_initterm处设置断点。
    • 运行程序后,进入_initterm的实现代码内,断点在(**it)();执行处,单步进入代理构造,即可得到全局对象的构造函数。

    2)利用栈回溯
    如果出现全局对象,由于全局对象的地址固定(对于有重定位表的执行文件中的全局对象,也可以在执行文件被加载后至执行前计算得到全局对象的地址)

    ​ 可以在对象的数据成员中设置读写断点,调试运行程序,等待构造函数调用的到来。利用栈回溯窗口,找到程序的执行流程,依次向上查询即可找到构造函数调用的起始处。
    3)对atexit设置断点,因为构造代理函数中会注册析构函数,其注册的方式是使用atexit

1.2.6 编译器提供默认构造函数的两种情况

  1. 父类、本类中定义的成员对象或者父类中有虚函数存在

    需要在构造函数中完成虚表的初始化

  2. 父类或本类中定义的成员对象带有构造函数

    派生类构造顺序是先构造父类再构造自身,当父类中带有构造函数时,将会调用父类构造函数,这个调用过程在构造函数内完成,编译器添加默认的构造函数来完成

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. 在汇编中如何区分构造和析构

构造:构造函数出现在析构函数之前,虚表指针没有指向虚表的首地址

析构:析构函数出现在所有成员函数之后,在实现过程中,虚表指针已经指向了某一个虚表的首地址

充分条件

构造函数

  1. 进行虚表指针的初始化

析构函数

  1. 还原虚表指针,让其指向自身的虚表首地址,防止在析构函数中调用虚函数时取到非自身虚表,从而导致函数调用错误

必要条件

构造函数

  1. 函数的调用是这个对象在作用域内的第一次成员函数调用,分析this指针即可区分对象,是哪个对象的this指针就是哪个对象的成员函数。
  2. 使用thiscall调用方式,使用ecx或者rcx传递this指针,返回值为this指针。

析构函数

  1. 函数的调用是这个对象在作用域内的最后一次成员函数调用,分析this指针即可区分对象,是哪个对象的this指针就是哪个对象的成员函数。
  2. 使用thiscall调用方式,使用ecx或者rcx传递this指针,没有返回值。
posted @ 2023-02-05 00:12  修竹Kirakira  阅读(90)  评论(0编辑  收藏  举报