31 Aug 2016 本文阅读量

类的深入

面向对象的基础就是类的定义已经对象的创建,C++中尤其重要, C++中,类的默认权限为私有,结构体为公有

类对象在内存中的分布

在类中,只有数据成员占内存空间,成员函数主要分布在代码段中,一般对象所占的内存空间大小为 sizeof(成员1) + sizeof(成员2) + sizeof(成员3) + …,但也有几种特殊情况:
虚函数和继承、空类、内存对齐、静态数据成员。
只要出现虚函数就会多出4个字节的空间,作为虚函数表,继承时需要考虑基类的大小, 出现静态成员时,静态成员存在于数据段中,并不在类对象的空间中。

  • 空类应该不占用内存,但实际却不是这样:
class Test{
public:
    int Print(){
    printf("Hello World\n");
    }
};

int main(){
    Test m_Test;
    
    printf("%d\n",sizeof(m_Test));
}

程序运行会发现输出为1,如上所说,空类中没有数据成员,应该不占内存空间,但我们知道 每个类都有一个this指针指向具体的内存,以便成员函数调用,即使定义一个类什么都不写, 编译器也会提供默认的构造函数初始化这个类,但是类的实例不占内存空间,该如何初始化呢? 所以编译器分配一个1字节的空间初始化this指针,故空类占一个字节

  • 内存对齐
class Test{
public:
    short s;
    int n;
};

在程序中定义这样一个类,通过 sizeof()得到的实例大小为8,又不满足如上了, 我们知道为了程序效率,编译器并不会依次申请内存存储变量,而采取内存对齐的方式,以牺牲一定内存空间的代价来换取程序的效率, 该类的大小为8,就是内存对齐的结果。
通过调试查看类各个成员的内存地址发现(VC++ 6.0),n的地址是 0x0012ff44,s的地址是 0x0012ff40,s 应该占用2个字节,但是n并未出现在 0x0012ff42 的位置。 假设编译器默认采用n个字节对齐方式(VC++ 6.0默认采用8字节对齐方式),而类中某个成员实际占用空间大小为m,那么该成员的内存地址必须是 p的整数倍,p = min(m,n), 所以对 s 来说,采用2字节对齐方式,分配到的首地址0x0012ff40是2的倍数,n采用4字节对齐方式,所以分配给n的内存首地址应该为4的倍数,所以其取0x0012ff44作为首地址,故该类占用8个字节。

class Test{
public:
    short s;
    double d;
    char c;
};

通过 sizeof() 得到类实例大小为24,根据上面分析我们知道,首先为 s 分配内存的时候采用2字节的对齐方式,假设分配的内存地址为 0x0012ff40,为 d 分配内存的时候采用 8 字节对齐方式,应为 0x0012ff48,最后为 c 分配的时候采用 1 字节对齐方式,为 0x0012ff51,总共占空间应为 2 + 6 + 8 + 1 = 17,但结果是 24。
内存对齐时,编译器实际采用方式为:
假设成员变量最大占用n个字节,编译器默认采用m个字节对齐方式,那实际的对齐大小应为 p 的整数倍 p = min(n,m),所以实际采用8字节对齐方式,占用24个字节。
编写程序时,可以使用 #pragma pack(n) 的方式改变编译器默认的对齐方式。

类的成员函数

类的成员函数在调用时直接利用对象调用,实际上类的对象调用类的成员函数时会默认传入第一个参数,是一个指向这个对象地址的指针,即this指针:

class Test{
private:
	int i;
public:
	Test(){
    	i = 0;
    }
    
   int GetNum(){
       i = 10;
       return i;
   }
};

int main(){
    Test m_Test;
    
    m_Test.GetNum();
    return 0;
}

反汇编代码

;主函数
24:       test t;
00401278   lea         ecx,[ebp-4]
0040127B   call        @ILT+20(test::test) (00401019)
25:       t.GetNum();
00401280   lea         ecx,[ebp-4]
00401283   call        @ILT+0(test::GetNum) (00401005)
26:       return 0;
00401288   xor         eax,eax

;GetNum()函数
18:           i = 10;
0040130D   mov         eax,dword ptr [ebp-4]
00401310   mov         dword ptr [eax],0Ah
19:           return i;
00401316   mov         ecx,dword ptr [ebp-4]
00401319   mov         eax,dword ptr [ecx]

main函数中定义类对象时首先会调用其构造函数,在调用函数之前首先通过lea指令获取到对象的首地址并将它保存到ecx寄存器中,在GetNum()函数中,首先在函数栈中定义了一个局部变量,将这个局部变量的值赋值为10,然后将这个局部变量的值赋值到ecx所在地址的内存中,最后再将这块内存中的值放到eax中作为参数返回。通过这部分代码可以看到,this指针并不是通过参数栈的方式传递给成员函数的,而是通过一个寄存器来传递,但是成员函数中若有参数,则仍然通过参数栈的方式传递参数。通过寄存器传递给成员方法作为this指针,然后根据数据成员定义的顺序和类型进行指针偏移找到对应的内存地址,对其进行操作。

类的静态成员函数


Tags:
Status:

Share:

Comments: