用汇编的眼光看C++(之class构造、析构) ,(之拷贝、赋值函数)

来源:互联网 发布:河南人 知乎 编辑:程序博客网 时间:2024/04/30 02:17

用汇编的眼光看C++(之class构造、析构)

【 声明:版权所有,欢迎转载,请勿用于商业用途。  联系信箱:feixiaoxing @163.com】  


    前面我们讨论基本上都是C语言的内容,还没有真正触及到C++的相关知识。从这篇博客之后,我们将会更多触及类的内容。类的属性很多,今天我们讨论主要就是构造函数、析构函数。

    

    (1)如果没有构造函数、析构函数呢?

[cpp] view plaincopyprint?
  1. class apple  
  2. {  
  3. public:  
  4.     void print() const {return;}  
  5. };  

    虽然这个类没有什么意义,但是如果用sizeof计算一下大小的话,我们发现它还是占了一个字节。那么如果有一个apple的变量呢?为了让apple的变量有意义,我们尝试做一个改变:

[cpp] view plaincopyprint?
  1. 66:       apple a;  
  2. 67:       a.print();  
  3. 00401248   lea         ecx,[ebp-4]  
  4. 0040124B   call        @ILT+0(apple::print) (00401005)  
  5. 68:       return;  
  6. 69:   }  

    我们看到,堆栈分配了四个字节空间给a,就是ebp下面的一个字节。


    (2) 析构函数什么时候调用?

[cpp] view plaincopyprint?
  1. class apple  
  2. {  
  3. public:  
  4.     apple() {printf("apple()!\n");}  
  5.     ~apple() {printf("~apple()!\n");}  
  6.     void print() const {return;}  
  7. };  

    如果调用呢,我们可以做一个测试环境,如下所示:

[cpp] view plaincopyprint?
  1. 68:       apple a;  
  2. 0040126D   lea         ecx,[ebp-10h]  
  3. 00401270   call        @ILT+65(apple::apple) (00401046)  
  4. 00401275   mov         dword ptr [ebp-4],0  
  5. 69:       {  
  6. 70:           apple b;  
  7. 0040127C   lea         ecx,[b]  
  8. 0040127F   call        @ILT+65(apple::apple) (00401046)  
  9. 71:       }  
  10. 00401284   lea         ecx,[b]  
  11. 00401287   call        @ILT+0(apple::~apple) (00401005)  
  12. 0040128C   mov         dword ptr [ebp-4],0FFFFFFFFh  
  13. 72:   }  
  14. 00401293   lea         ecx,[ebp-10h]  
  15. 00401296   call        @ILT+0(apple::~apple) (00401005)  
  16. 0040129B   mov         ecx,dword ptr [ebp-0Ch]  
  17. 0040129E   mov         dword ptr fs:[0],ecx  

    我们看到,只要出了作用域,析构函数就会自动会被调用。


    (3)如果是new调用类,析构函数会自动调用吗?

    不会。


    (4)构造函数、析构函数的本质?

    我们知道在函数中的临时变量在堆栈里面应用的时候都需要初始化处理的,在堆栈返回的时候会被自动收回。那么构造函数和析构函数?其实是一样的,在函数调用的时候,堆栈也会为这样一个类准备大小合适的堆栈,然后调用构造函数对这样的一片内存进行初始化处理,在函数return的时候,调用另外一个函数对可能涉及到的资源进行一次清理。这里指的资源不是指内存空间,而是指广义意义上的系统资源、比如说IO、socket、锁、画笔、对话框句柄等等。所以,通常而言,如果你在析构函数里面没有及时对资源进行分配,那么就会造成资源的泄露。这一切只有等到程序结束的时候才会重新回到系统的手中,不过如果一个程序占有太多的资源,那么势必会对别的程序造成影响。

 

用汇编的眼光看C++(之拷贝、赋值函数)

【 声明:版权所有,欢迎转载,请勿用于商业用途。  联系信箱:feixiaoxing @163.com】


    拷贝构造函数和复制函数是类里面比较重要的两个函数。两者有什么区别呢?其实也很简单,我们可以举个例子,加入有这样一个类的定义:

[cpp] view plaincopyprint?
  1. class apple  
  2. {  
  3. public:  
  4.     apple() {  printf("apple()!\n");}  
  5.     apple(apple& a) {  printf("copy apple()!\n");}  
  6.     apple& operator=(apple& a) {  printf("= apple()\n"); return *this;}  
  7.     ~apple() {  printf("~apple()!\n");}  
  8.     void print() const {  return;}  
  9. };  

    那么我们在如下的函数里面进行调用的时候,调用的函数分别是哪些呢?

[cpp] view plaincopyprint?
  1. void process()  
  2. {  
  3.     apple a, c;  
  4.     apple b =a;  
  5.     c = b;  
  6. }  

    其实汇编的结果是这样的,大家可以一起看一下,自己尝试读一下。如果一次不是很明白,可以多读几次。

[cpp] view plaincopyprint?
  1. 70:       apple a, c;  
  2. 0040127D   lea         ecx,[ebp-10h]  
  3. 00401280   call        @ILT+70(apple::apple) (0040104b)  
  4. 00401285   mov         dword ptr [ebp-4],0  
  5. 0040128C   lea         ecx,[ebp-14h]  
  6. 0040128F   call        @ILT+70(apple::apple) (0040104b)  
  7. 00401294   mov         byte ptr [ebp-4],1  
  8. 71:       apple b =a;  
  9. 00401298   lea         eax,[ebp-10h]  
  10. 0040129B   push        eax  
  11. 0040129C   lea         ecx,[ebp-18h]  
  12. 0040129F   call        @ILT+50(apple::apple) (00401037)  
  13. 004012A4   mov         byte ptr [ebp-4],2  
  14. 72:       c = b;  
  15. 004012A8   lea         ecx,[ebp-18h]  
  16. 004012AB   push        ecx  
  17. 004012AC   lea         ecx,[ebp-14h]  
  18. 004012AF   call        @ILT+75(apple::operator=) (00401050)  
  19. 73:   }  
  20. 004012B4   mov         byte ptr [ebp-4],1  
  21. 004012B8   lea         ecx,[ebp-18h]  
  22. 004012BB   call        @ILT+0(apple::~apple) (00401005)  
  23. 004012C0   mov         byte ptr [ebp-4],0  
  24. 004012C4   lea         ecx,[ebp-14h]  
  25. 004012C7   call        @ILT+0(apple::~apple) (00401005)  
  26. 004012CC   mov         dword ptr [ebp-4],0FFFFFFFFh  
  27. 004012D3   lea         ecx,[ebp-10h]  
  28. 004012D6   call        @ILT+0(apple::~apple) (00401005)  
  29. 004012DB   mov         ecx,dword ptr [ebp-0Ch]  
  30. 004012DE   mov         dword ptr fs:[0],ecx  
  31. 004012E5   pop         edi  
  32. 004012E6   pop         esi  
  33. 004012E7   pop         ebx  
  34. 004012E8   add         esp,58h  
  35. 004012EB   cmp         ebp,esp  
  36. 004012ED   call        __chkesp (004087c0)  
  37. 004012F2   mov         esp,ebp  
  38. 004012F4   pop         ebp  
  39. 004012F5   ret  

    代码有点长,大家可以一句一句来看,比如说就按照70、71、72、73分别查看对应的汇编代码:

    (1)70句: 我们看到函数做了两次函数调用,恰好就是apple的构造函数调用。这也正好对应着两个临时变量a和c,两个变量的地址分别是【ebp-10】和【ebp-14】,这里也可以看出整个类的大小就是4个字节,就是一块存放数据的普通内存。而构造函数之所以能和对应的内存绑定在一起,主要是因为ecx记录了内存的起始地址,这在C++编译中是十分关键的。我们看到的C++构造函数好像是没有绑定内存,实际上在VC里面已经做好了约定,ecx就是this指针,就是类的内存起始地址。有兴趣的同学看看G++编译的时候,采用的this指针是哪个寄存器保存的?(其实是eax)

    (2)71句:通过对应看到了eax记录了引用变量的地址,而ecx是ebp下面紧挨着四个字节。但是函数调用的地址和前面的缺省构造函数不太一样,所以我们大胆猜测,这里的构造函数这是拷贝构造函数,我们可以在调试的时候查看一下打印消息。

    (3)72句:0x4012AF语句已经清楚地告诉了我们,这里调用的函数就是operator=函数,这一部分是算术符重载的内容,我们在后面的博客会重点介绍。

    (4)73句: 前面我们讲过,析构函数在函数调用结束的时候被被自动调用,那么这里我们看到却是出现了三个调用?这三个变量正好是我们之前说的a、b、c三个变量。那么这三个变量调用的次序是怎样的呢?我们可以查看一下变量的地址,分别是【ebp-18h】、【ebp-14h】、【ebp-10h】,这正好和变量出现的顺序相反。所以我们看到,析构函数和构造函数是严格一一对应的,谁先出现,谁后析构。