在类的对象创建的时候,构造函数起到了对对象的初始化的作用。本节将对构造函数的机理进行探讨。可以思考下面的几个问题:1、构造函数的用到是什么机制?2、构造函数在汇编形式下有什么特点,也即是怎样在汇编代码中识别构造函数?由于存在不同生命周期、不同作用域的对象,那么这些构造函数的实现机制都是相同的么?
带着这样的疑问,我们来使用实际的例子探究一番。
本节实验环境:VC++ 2013
例子1:
#include <iostream>
using namespace std;
class Rectangle{
private:
int width ;
int height ;
public:
Rectangle();
};
Rectangle::Rectangle (int width , int height)
{
this.width = width ;
this.height = height ;
}
void main()
{
Rectangle t (3, 6);
}
在debug模式下查看其main函数的反汇编代码:
void main()
{
00D75300 push ebp
00D75301 mov ebp,esp
00D75303 sub esp,0D4h
00D75309 push ebx
00D7530A push esi
00D7530B push edi
00D7530C lea edi,[ebp+FFFFFF2Ch]
00D75312 mov ecx,35h
00D75317 mov eax,0CCCCCCCCh
00D7531C rep stos dword ptr es:[edi]
00D7531E mov eax,dword ptr ds:[00D7F000h];该行以及下面两行是检测栈是否被修改策略用到的方法,先不用追究。
00D75323 xor eax,ebp
00D75325 mov dword ptr [ebp -4], eax
Rectangle t (3, 6 );
00D75328 push 6
00D7532A push 3
00D7532C lea ecx,[ebp-10h];ebp-10h是变量t的首地址,即是该对象的this指针
00D7532F call 00D712B2
}
.....
需要注意的是,在栈帧的建立过程中,需要遵守x86的函数调用约定,将callee保存的寄存器压入栈内。
callee保存的寄存器值:ebx、esi、edi.可以发现这个函数调用将callee需要保存的寄存器全部保存起来。
在创建对象的时候,下面的代码即是调用构造函数的代码:
00D75328 push 6
00D7532A push 3
00D7532C lea ecx,[ebp-10h]
00D7532F call 00D712B2
其中lea ecx,[ebp-10h]值得注意,ecx保存的值相当于就是this指针,也就是说调用构造函数也要传递this指针了。其实,识别C++类的比较好的方法是,C++的成员函数的this指针都是通过ecx寄存器传递的。
回到代码中,可以发现在构造函数调用前,会腾出一段栈空间(ebp-10到ebp之间的空间)提供给对象使用。
在执行到call函数之后,进行单步调试,会进入到一个jmp指令:
00D712B2 jmp 00D73030
之后进入构造函数进行执行:
Rectangle::Rectangle (int width, int height)
{
00D73030 push ebp
00D73031 mov ebp,esp
00D73033 sub esp,0CCh
00D73039 push ebx
00D7303A push esi
00D7303B push edi
00D7303C push ecx
00D7303D lea edi,[ebp+FFFFFF34h]
00D73043 mov ecx,33h
00D73048 mov eax,0CCCCCCCCh
00D7304D rep stos dword ptr es:[edi]
00D7304F pop ecx
00D73050 mov dword ptr [ebp -8], ecx;将this指针保存到地址为ebp-8的内存呢
this-> width = width ;
00D73053 mov eax,dword ptr [ ebp-8 ];将this指针传递到eax
00D73056 mov ecx,dword ptr [ ebp+8 ];将参数width传递给ecx
00D73059 mov dword ptr [eax ],ecx;对this指针所指对象的第一个变量进行赋值
this-> height = height ;
00D7305B mov eax,dword ptr [ ebp-8 ]
this-> height = height ;
00D7305E mov ecx,dword ptr [ ebp+0Ch ];获取参数height
00D73061 mov dword ptr [eax +4], ecx ;对this指针所指对象的第二个变量进行赋值
}
00D73064 mov eax,dword ptr [ ebp-8 ];eax保存了this指针,
00D73067 pop edi
00D73068 pop esi
00D73069 pop ebx
00D7306A mov esp,ebp
00D7306C pop ebp
00D7306D ret 8
注意此代码中ecx在函数最前面要使用一堆数据填充栈时需要用到ecx,所有先保存起来,后来再使用,所以有压栈和弹栈的过程。通过上面的代码我们可以知道,对成员变量的赋值,其实都是通过this指针进行索引,间接寻址方式找到成员变量,也可以看出隐含着的this指针作用多么重要。另外,虽然我们写构造函数时没有返回值,但在汇编代码中还体现了构造函数“拥有”返回值这一现象,即通过eax构造函数返回了this指针,这些隐藏于代码之下的细节,也只有通过汇编的形式才能看到。
上面的对象作为局部对象,是在栈空间内存储对象的数据成员的。
那么,作为在main函数执行之前就已经存在的全局对象和静态对象,在什么时候调用构造函数的呢?
例2:
Rectangle my(5 , 7 );
void main()
{
}
注:Rectangle类与例1中的定义相同。
直接在Rectangle my (5 , 7 )处下断点,将会到下面的地方执行:
00AC19B0 push ebp
00AC19B1 mov ebp,esp
00AC19B3 sub esp,0C0h
00AC19B9 push ebx
00AC19BA push esi
00AC19BB push edi
00AC19BC lea edi,[ebp-0C0h]
00AC19C2 mov ecx,30h
00AC19C7 mov eax,0CCCCCCCCh
00AC19CC rep stos dword ptr es:[edi]
00AC19CE push 7
00AC19D0 push 5
00AC19D2 mov ecx,0ACF324h
00AC19D7 call Rectangle ::Rectangle (0AC12B2h)
00AC19DC pop edi
00AC19DD pop esi
00AC19DE pop ebx
00AC19DF add esp,0C0h
00AC19E5 cmp ebp,esp
00AC19E7 call __RTC_CheckEsp (0AC12DAh)
00AC19EC mov esp,ebp
00AC19EE pop ebp
00AC19EF ret
前面的过程依然是栈帧的建立,着重看后面调用构造函数的代码:
00AC19CE push 7
00AC19D0 push 5
00AC19D2 mov ecx ,0ACF324h ;传递this指针
00AC19D7 call Rectangle :: Rectangle (0AC12B2h )
注意到此时传递的this指针并不是在栈上的,而是编译器分配的一个地址,而且这个地址处在虚拟内存中的低地址部分。那么是谁调用了该构造函数呢?我们继续执行,直到该段代码的ret。
当创建堆对象时,又会进行怎样的处理呢?
代码:
void main()
{
Rectangle * t = new Rectangle(3 , 4 );
}
汇编代码部分:
Rectangle * t = new Rectangle(3, 4);
00E97E7D push 8
00E97E7F call operator new (0E9137Ah);在堆上申请8个字节的空间
00E97E84 add esp,4
00E97E87 mov dword ptr [ebp -0E0h], eax;将申请到的空间首地址拿到栈上
00E97E8D mov dword ptr [ebp -4], 0
00E97E94 cmp dword ptr [ebp -0E0h], 0;检查返回的地址是不是NULL.即有没有申请成功
00E97E9B je main +74h (0E97EB4h );申请空间失败,则不调用构造函数
00E97E9D push 4
00E97E9F push 3
00E97EA1 mov ecx,dword ptr [ ebp-0E0h ];this指针传入
00E97EA7 call Rectangle ::Rectangle (0E912B2h) ;调用构造函数
00E97EAC mov dword ptr [ebp -0F4h], eax;返回的this指针保存
00E97EB2 jmp main +7Eh (0E97EBEh )
00E97EB4 mov dword ptr [ebp -0F4h], 0;申请空间失败,指针赋值为空
00E97EBE mov eax,dword ptr [ ebp-0F4h ];得到该对象的地址
00E97EC4 mov dword ptr [ebp -0ECh], eax ;保存
00E97ECA mov dword ptr [ebp -4], 0FFFFFFFFh
00E97ED1 mov ecx,dword ptr [ ebp-0ECh ];再将对象的首地址移到ecx中
00E97ED7 mov dword ptr [t],ecx ;获取到对象首地址。
通过上面的代码,可以观察到:堆对象创建时,会首先使用new函数申请堆空间,如果申请成功,则执行构造函数,返回首地址,如果不成功,则不执行构造函数,返回为空。既然该指针为空,那么可以想象得到,如果此时引用成员函数,并且访问了该类的动态成员变量,就会引发段错误。
C++拷贝构造函数(浅拷贝)可能带来的问题:
代码:
#include <string.h>
#include <stdlib.h>
class CStudent
{
public :
int id;
char* name;
public:
CStudent(int m_id, char* m_name );
CStudent(CStudent &stu);
~CStudent ();
};
CStudent::CStudent (int m_id, char* m_name )
{
this->id = m_id;
int length = strlen(m_name);
this->name = new char [length + sizeof(char)];
strncpy_s(this ->name, length+sizeof(char), m_name, length );
}
CStudent::~CStudent ()
{
delete this ->name;
this->name = NULL ;
}
CStudent::CStudent (CStudent &stu)
{
this->id = stu. id;
this->name = stu. name;
}
void main()
{
CStudent stu(1, "bingxian" );
CStudent stu2(stu);
}
执行上面的程序,就会出现错误。
其原因是析构函数会释放掉字符串资源,而st1和st2两个对象共享了name资源,析构的时候就会两次释放该资源,于是导致了错误。所以,在编写拷贝构造函数时,为了避免出现这样的错误发生,应使用深度拷贝构造函数。
上面的例子稍加改动拷贝构造函数即可:
CStudent::CStudent(CStudent &stu)
{
this->id = stu .id;
int length = strlen(stu .name);
this->name =new char [length + sizeof(char )];/*在堆上开辟一段缓冲区*/
strncpy_s( this->name, length+sizeof (char), stu.name, length);/*将字符串copy到申请的缓冲区中*/
/*this->name = stu.name;*/
}
我们需要分析的参数对象产生时的汇编形式,看看拷贝构造函数被调用时发生了什么:
CStudent stu2 (stu) ;
00E71680 push 8
00E71682 lea ecx,[stu2 ]
00E71685 call CStudent ::__autoclassinit2 (0E711E0h)
00E7168A lea eax,[stu ]
00E7168D push eax
00E7168E lea ecx,[stu2 ]
00E71691 call CStudent ::CStudent (0E710FAh)
可以看到,编译器会首先为stu2分配8字节的栈空间,并且做初始化工作,接着使用stu对象作为参数传递到构造函数中。
我们为上面的例子中加一个函数:
void InforStu(CStudent stu)
{
cout<< "id = " << stu .id << endl;
cout << "name:" << stu .name << endl;
}
这个函数将CStudent对象作为参数,那么在调用的时候自然会生成对象的拷贝,自然会调用拷贝构造函数,来看一下反汇编代码:
InforStu(stu2 );
003262CA sub esp,8
003262CD mov ecx,esp;新对象的this指针
003262CF mov dword ptr [ebp -0F8h], esp
003262D5 lea eax,[stu2 ];stu2对象
003262D8 push eax
003262D9 call CStudent ::CStudent (032123Fh);调用拷贝构造函数
003262DE mov dword ptr [ebp -100h], eax ;返回this指针
003262E4 call InforStu (0321005h)
从反汇编代码中可以看到,编译器会在栈底弄出一部分空间,使用拷贝构造函数新建一个对象,函数会使用这个新建的对象作为参数。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!