了解C++就必然要了解类,所以类和对象之间的关系这里就不再多说,就举几个简单的小例子:
1.1CNumber类
class CNumber
{
public:
CNumber()
{
m_nOne = 1;
m_nTwo = 2;
}
intGetNumberOne()
{
returnm_nOne;
}
intGetNumberTwo()
{
returnm_nTwo;
}
private:
int m_nOne;
int m_nTwo;
};
void main(int argc,char* argv[])
{
170: CNumber Number;
0042845E lea ecx,[Number]
00428461 call CNumber::CNumber (427117h) ;调用构造函数对其进行初始化
}
CNumber这个类有两个整形成员变量m_nOne和m_nTwo。现在来看一下构造函数的内容。
13: m_nOne= 1;
004284D3 mov eax,dword ptr [this]
004284D6 mov dword ptr [eax],1
14: m_nTwo= 2;
004284DC mov eax,dword ptr [this]
004284DF mov dword ptr [eax+4],2
其中使用了this指针,this指针指向对象的首地址,相当于&Number。再看内存:
0x0012FF60 0100 00 00 02 00 00 00
可以看到this指针后面的8个字节已经变成了1和2,这正是两个成员变量所在的内存。所以Number对象在内存中占据8字节。
是不是所有的类的大小都是所有的成员变量的大小之和呢?这显然是不对的,有以下几种情况:
A.空类:空类中没有任何数据成员,按常理它的大小应该是0才对,但是事实并非如此。没有数据成员但是可能有成员函数,所以空类必须可以实例化,如果大小为0则无法实例化,因此会非配给它1字节用于实例化,但是这1字节并没有被使用。将上面的CNumber类中的数据成员和函数内的代码全部注释掉成为一个空类,但保留函数名。此时可以看到&Number的地址:,说明它确实存在于内存,但是可以看到里面并没有内容。
B.内存对齐
类或结构体中的数据成员会按照他们的出现顺序依次申请内存空间。由于内存对齐的原因,数据成员并不会像数组那样连续的排列,由于数据类型可能不同,所以大小也有可能不同。还是来看例子:
struct tagTEST
{
shortsShort; //占2字节
int nInt; // 占4字节
};
tagTEST test;
test.nInt = 5;
test.sShort = 2;
printf("%d\n", sizeof( test ) );
上面是C++源代码,再来看一下内存:
0x0012FF60 0200 cc cc 05 00 00 00
可以看到在0x0002后面有两个字节是没有使用的,这就是为了内存对齐,这里int是4字节,默认是8字节,所以对齐值为4字节。本来如果不对齐则结构体只要2+4=6字节,但是对齐之后要8字节。
还有一种现象就是结构体中的数据定义顺序不同,结构体的大小也可能会不同。
当结构体如此定义时:
struct tagTEST
{
char cChar; //占1字节
int nInt; // 占4字节
shortsShort; //占2字节
};
初始化部分:
tagTEST test;
test.cChar = 'a';
test.nInt = 5;
test.sShort = 2;
printf("%d\n", sizeof( test ) );
此时查看内存:0x0012FF58 61 cc cc cc 05 00 00 00 02 00 cc cc,输出结构体大小为12。但是若是将结构体定义改成如下:
struct tagTEST
{
char cChar; //占1字节
shortsShort; //占2字节
int nInt; // 占4字节
};
则内存变成了0x0012FF60 61 cc 02 00 05 00 00 00,显示的大小也变成了8。
当然,也可以自己调整内存的对齐值,如下面的例子:
#pragma pack(1)
struct tagTEST
{
char cChar; //占1字节
shortsShort; //占2字节
int nInt; // 占4字节
};
内存变成了0x0012FF60 61 02 00 05 00 00 00,显示大小变成了7。当然修改对齐值也并非一定生效,当它大于结构体内的最大的数据类型的长度时就会无效。
当结构体的成员中有数组时,是根据数组元素的类型计算对齐值,而不是根据数组的整体大小计算。下面有个小例子:
struct cArray
{
char cChar;
char Array[4 ];
shortsShort;
};
cArray carray;
carray.cChar = 'a';
for( int i = 0; i < 4; i++ )
carray.Array[ i ] = 'b';
carray.sShort = 3;
结构体cArray中存在一个大小为4的数组,将其赋值之后查看内存:
0x0012FF5C 61 62 62 62 62 cc 03 00
可以看到这里是以2字节来作为内存对齐值得,所以只在数组后面填充了1字节。若是采用编译器默认的8字节则需要在数组后面填入3字节然后再sShort变量后再填入6字节方可,显然编译器为了节约内存资源并不会这样做。
当结构体嵌套时编译器又是如何分配内存资源的呢?下面还是来看例子:
struct tagTEST
{
char cChar;
shortsShort;
int nInt;
};
struct Nest
{
char cChar;
tagTEST test;
};
结构体定义:
Nest nest;
nest.cChar = 'a';
nest.test.cChar = 'b';
nest.test.nInt = 5;
nest.test.sShort = 2;
定义之后再查看内存:0x0012FF58 61 cc cc cc 62 cc02 00 05 00 00 00,输出nest对象的大小是12。这说明了嵌套结构体的对齐值并非是按照被嵌套的结构体的大小来作为对齐值,而是是以被嵌套的对齐值来作为依据。这里tagTEST结构体占8字节,若是按8字节对齐,则nest对象大小为16字节,但是这里是12字节。tagTEST的对齐值为4字节,cChar为1字节,所以Nest的对齐值自然就是4字节而并非8字节。
至于结构体中有静态数据成员以及对象为全局对象时的情况在后面会依次介绍。
this指针就是保存当前对象首地址的指针。首先,从结构体的寻址方式:
tagTEST test;
tagTEST *pTest = &test;
test.cChar = 'a';
test.nInt = 5;
test.sShort = 2;
可以看到 和,由此可见结构体和类中的数据成员的地址是结构体或类的首地址加上数据成员在其中的偏移量。
下面来看一个this指针的事例:
CTest类:
class CTest
{
public:
voidSetNumber(intnNumber)
{
m_nInt= nNumber;
}
public:
int m_nInt;
};
;对main函数进行分析
217: CTest Test;
218: Test.SetNumber(5);
0042B12E push 5
0042B130 lea ecx,[Test] ;取出对象的首地址存入ecx中
0042B134 call CTest::SetNumber (427117h)
219: printf("CTest: %d\r\n", Test.m_nInt);
0042B139 mov eax,dword ptr [Test] ;取出对象首地址处的4字节
0042B13C push eax
0042B13D push offset string "CTest : %d\r\n" (472F78h)
0042B142 call @ILT+3895(_printf) (426F3Ch)
0042B147 add esp,8
;对Test类进行分析
52: class CTest
53: {
54: public:
55: void SetNumber(intnNumber)
56: {
00428300 push ebp
00428301 mov ebp,esp
00428303 sub esp,0CCh
00428309 push ebx
0042830A push esi
0042830B push edi
0042830C push ecx ;由于后面要用ecx做循环计数器,所以先保存它的值
0042830D lea edi,[ebp-0CCh]
00428313 mov ecx,33h
00428318 mov eax,0CCCCCCCCh
0042831D rep stos dword ptr es:[edi]
0042831F pop ecx ;将类的首地址重新存入ecx中
00428320 mov dword ptr [ebp-8],ecx ;ebp-8存储着this指针的值,将类的首地址存入这里
57: m_nInt = nNumber;
00428323 mov eax,dword ptr [this] ;将类的首地址存入eax,相当于mov eax, [ebp-8]
00428326 mov ecx,dword ptr [nNumber] ;相当于mov ecx,[ebp+8],ebp+8里存着参数值
00428329 mov dword ptr [eax],ecx ;对nInt进行赋值
58: };后续代码略
以上代码便是对象调用成员函数,这里通过寄存器ecx传递对象的首地址的方法便是this指针的由来,传递给ecx之后,编译器会将首地址复制到栈的指定位置作为this指针的值。所有的成员函数都有一个隐藏的参数,就是自身类型的对象的指针this,这样的默认调用约定就是thiscall。同样,在成员函数中访问数据成员也是需要通过this指针的。
当成员函数的调用方式为__stdcall时,this指针将不会使用寄存器ecx传递,而是使用栈传递,下面将SetNumber函数改为使用__stdcall调用方式再来看this指针的传递方式:
218: Test.SetNumber(5);
0042B12E push 5
0042B130 lea eax,[Test] ;相当于lea eax,[ebp-8],利用栈进行传递
0042B133 push eax
0042B134 call CTest::SetNumber (427121h)
57: m_nInt = nNumber;
0042831E mov eax, [ebp+8] ;取出this指针存入eax
.text:00428321 mov ecx, [ebp+0Ch] ;取出nNumber的值
.text:00428324 mov [eax], ecx
这里使用__stdcall方式调用函数使得this指针的识别变得困难,__cdecl调用方式类似,只是在栈平衡时有所不同。