C++结构体和类中的内存布局

通常我们访问结构体或类的成员变量,使用的是比较普通的方法。如定义一个struct

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. struct A  
  2. {  
  3. char a;  
  4. int  b;  
  5. double c;  
  6. void (*func) (A *);  
  7. };  

那么我们访问结构体中的成员有两种方法:1)(结构体对象名) . (成员变量名);2) (结构体指针) -> (成员变量名)。由于任何变量都在内存中对应一个地址,我们是不是可以通过指针地址的方式访问结构体的成员变量呢?

首先我们举一个简单的例子:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. struct example  
  2. {  
  3.     int a;  
  4.     int b;  
  5. };//定义一个结构体  
  6. example  ex; // 创建一个结构体变量  
  7. ex.a =1;  
  8. ex.b = 12;//初始化结构体变量  
  9. /*下面实现对结构体成员变量的指针访问;因为结构体的中的内存布局是有一定规律的,对于本例子中,它的内存布局:先存放int类型变量a,然后是int类型变量b;因此对结构体变量ex取地址,并将其转换为(int*)类型,便可获得成员变量a的地址;同样b的地址是对a的地址加1即可(这里要注明的是:对地址进行加1操作的实质是对地址进行+ 1*(指针所指类型的字节大小))*/  
  10. cout<<showbase<<hex<<&ex<<endl; // 输出结构体变量的地址,结果:0x28ff30;不同机器值//可能不一样  
  11. int *pa = (int*)&ex; // pa的值为0x28ff30  
  12. cout<<*pa<<endl; //输出结果:1;即输出的是ex.a的值  
  13. int *pb = (int*)&ex +1; // pb的值为0x28ff34  
  14. cout<<*pb<<endl;// 输出结果:12;即输出的ex.b的值  
  15. /*以上代码就实现了利用地址访问结构的成员变量*/  

下面就举一个稍微复杂一些的例子,也就是本文开头提到的结构体A,只不过改了下名字。

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. struct Base  
  2. {  
  3. char a;  
  4. int b;  
  5. double c;  
  6. void (*func_base)(Base*);  
  7. };  

该结构体包含了一个字符类型的变量a,整形变量b,双精度浮点型变量c,函数指针func。该结构体的内存大小是24个字节(考虑字节对齐)。那么如何实现利用地址访问结构体的成员变量呢?下面先给出实现访问该结构成员变量的代码,然后再分析该代码。

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. void show_base(Base *base) // 定义函数  
  2. {  
  3.     cout<<base->a<<" "<<base->b<<"  "<<base->c<<endl;  
  4.     }  
  5. Base new_base(char i, int j, double k)//创建结构体的函数,类似于类的构造函数  
  6. {  
  7.     Base base;  
  8.     base.a = i;  
  9.     base.b = j;  
  10.     base.c = k;  
  11.     base.func_base = show_base;  
  12.     return base;  
  13.     }  
  14.   
  15. int main()  
  16. {  
  17.     cout<<sizeof(Base)<<endl;  
  18.     Base b = new_base('a', 15,1.56);// 创建结构体  
  19.     cout<<"结构体赋值后的输出: ";  
  20.     b.func_base(&b);  
  21.     cout<<"结构体初始地址是:"<<showbase<<hex<<&b<<endl;  
  22.     char *pa = (char*)&b;  
  23.     cout<<"成员变量a的地址是:"<<showbase<<hex<<  
  24.     (int*)pa<<"; a的值是:"<<*pa<<endl;  
  25.   
  26.     int *pb = (int*)&b +1;  
  27.     cout<<"成员变量b的地址是:"<<showbase<<hex<<  
  28.     (int*)pb<<"; b的值是:"<<*pb<<endl;  
  29.   
  30.     double *pc = (double*)((int*)&b +2);  
  31.     cout<<"成员变量c的地址是:"<<showbase<<hex<<  
  32.     (int*)pc<<"; c的值是:"<<*pc<<endl;  
  33.   
  34.     typedef void (*func)(Base *);  
  35.     func pfunc_base = (func)*(int*)((double*)&b + 2);  
  36.     cout<<"成员变量func_base的地址是:"<<showbase<<hex<<  
  37.     (int*)pfunc_base<<endl;  
  38.     cout<<"成员变量func_base的结果:";  
  39.     pfunc_base(&b);  
  40.     return 0;  
  41. }  


从输出可以看出来以上程序的结果是正确的,而且对每个成员变量的输出地址也能看出结构的内存布局。程序中主要注意两点:

1)      对指针地址进行加减运算时,实际的运算是加减该指针指向的变量类型的字节数*加减的值。

2)      提取函数指针变量的地址是要注意,结构的地址(&b)加上一定的偏移量得到的地址是

指向函数指针的指针,因此要对它做解地址操作,然后进行指针类型转换,即func pfunc_base = (func)*(int*)((double*)&b + 2);这个语句。

 

结构体的内存布局大致就是这样的,下面我们就讨论下更有意思的类的内存布局。由于类牵扯到继承,虚函数,多重继承,虚继承,因此类的内存布局比结构体复杂了很多。这里我们就一步一步的分析这些情况。

1)      首先分析单一继承的问题,并且先不考虑虚函数的情况,即类中只有成员变量,因为除了虚函数的成员函数都不会占用类的内存空间,所以我们可以忽略他们。下面就是一个简单的例子。

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class A  
  2. {  
  3.     public:   
  4.     A(int i):a(i){}  
  5.     int a; //之所以声明为public,是方便我们以后调用成员变量  
  6.     };  
  7. class B:public A  
  8. {  
  9.     public:  
  10.     B(int i, int j):A(i),b(j){};  
  11.     int b; //之所以声明为public,是方便我们以后调用成员变量  
  12.     };  
  13. int main()  
  14. {  
  15.     B b(12,15);  
  16.     cout<<"类对象b的地址为: "<<&b<<endl;  
  17.     int *pa = (int*)(&b);  
  18.     cout<<"类对象b中继承自类A的成员变量a的地址:"  
  19.     <<showbase<<hex<<(int*)pa<<"其值为:"<<*pa<<endl;  
  20.   
  21.     int *pb = (int*)((int*)(&b)+1);  
  22.     cout<<"类对象b中的成员变量b的地址:"  
  23.     <<showbase<<hex<<(int*)pb<<"其值为:"<<*pb<<endl;  
  24.     return 0;  
  25. }  


从上面的输出可以看出,在类的继承时,派生类的首地址指向了其继承自基类的成员变量;换句话说就是派生类中的内存布局是:先基类对象,再是派生类的成员变量。如果类内的成员变量的类型比较复杂,那么就应该考虑字节对齐的原则了。为了更好的表明类中的内存布局,在举一个例子说明下,在这里引入了虚函数,并且增加了类中的成员变量的类型:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class A  
  2. {  
  3.     public:  
  4.     A(int i,double da):a(i),da(da){}  
  5.     virtual void show()  
  6.     {  
  7.         cout<<"this is A::show!"<<endl;  
  8.         }  
  9.     int a;  
  10.     double da;  
  11.     };  
  12. class B:public A  
  13. {  
  14.     public:  
  15.     B(int i,double da, int j,double db)  
  16.     :A(i,da),b(j),db(db){};  
  17.     void show()  
  18.     {  
  19.         cout<<"this is B::show!"<<endl;  
  20.         }  
  21.     int b;  
  22.     double db;  
  23.     };  
  24.   
  25. int main()  
  26. {  
  27.     B b(12,3.15,15,6.89);  
  28.     cout<<"类对象b的地址为: "<<&b<<endl;  
  29.     int *vptr = (int*)*(int*)(&b);  
  30.     cout<<"类对象b中指向虚函数表的指针地址是:"  
  31.     <<showbase<<hex<<(int*)vptr<<"其值为:"<<*vptr<<endl;  
  32.     cout<<"虚函数表的第一个函数的地址是: "<<*vptr<<endl;  
  33.     typedef void (*func)(void);  
  34.     func func_show;  
  35.     func_show = (func)*(int*)*(int*)&b;  
  36.     cout<<"转换后的虚函数表的第一个函数的地址是:"<<showbase<<hex<<(int*)func_show<<endl;  
  37.     func_show();  
  38.     int *pa = (int*)((int*)(&b)+1);  
  39.     cout<<"类对象b中继承自类A的成员变量a的地址:"  
  40.     <<showbase<<hex<<(int*)pa<<"其值为:"<<*pa<<endl;  
  41.   
  42.     double *pda = (double*)((int*)(&b)+2);  
  43.     cout<<"类对象b中继承自类A的成员变量da的地址:"  
  44.     <<showbase<<hex<<(int*)pda<<"其值为:"<<*pda<<endl;  
  45.   
  46.      int *pb = (int*)((int*)(&b)+4);  
  47.     cout<<"类对象b中的成员变量b的地址:"  
  48.     <<showbase<<hex<<(int*)pb<<"其值为:"<<*pb<<endl;  
  49.   
  50.     double *pdb = (double*)((int*)(&b)+6);  
  51.     cout<<"类对象b中的成员变量db的地址:"  
  52.     <<showbase<<hex<<(int*)pdb<<"其值为:"<<*pdb<<endl;  
  53.     return 0;  
  54. }  

其输出结果是:



有虚函数的单一继承的派生类中的内存布局:首先是指向虚函数表的指针(放在首位是为了提高效率),其次是基类的成员变量,最后是派生类的成员变量。注:在对虚函数的指针进行转换时一定要注意:类的内存存放的是vptr指针,而vptr是指向虚函数表的指针,对vptr解地址运算,即*vptr,得到的是虚函数表中的第一个指针,但这个指针还不是函数指针,应该在对其做一个解地址运算,并做类型转换,即上述例程中的func_show = (func)*(int*)*(int*)&b;。


 多重继承的情况

对于多重继承,首先要明确多重继承和虚函数的关系。对于单一继承来说,如果class B继承自class A;且A中有虚函数,因此class B中也有一个指向虚函数表的指针。但是对于多重继承,派生类会有多个虚函数表的指针,因为它的每个父类都有可能有虚函数。举个例子来说明下吧:

[cpp]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. class A  
  2. {  
  3.     private:  
  4.     int a;  
  5.     public:  
  6.     A(int i):a(i){}  
  7.     virtual void print_A1()  
  8.     {  
  9.         cout<<"A::print_A1()!"<<endl;  
  10.         }  
  11.     virtual void print_A2()  
  12.     {  
  13.         cout<<"A::print_A2()!"<<endl;  
  14.         }  
  15.     };  
  16. class B  
  17. {  
  18.     private:  
  19.     int b;  
  20.     public:  
  21.     B(int j):b(j){}  
  22.     virtual void print_B1()  
  23.     {  
  24.         cout<<"B::print_B1()!"<<endl;  
  25.         }  
  26.     virtual void print_B2()  
  27.     {  
  28.         cout<<"B::print_B2()!"<<endl;  
  29.         }  
  30.     };  
  31.   
  32. class C  
  33. {  
  34.     private:  
  35.     int c;  
  36.     public:  
  37.     C(int k):c(k){}  
  38.     virtual void print_C1()  
  39.     {  
  40.         cout<<"C::print_C1()!"<<endl;  
  41.         }  
  42.     virtual void print_C2()  
  43.     {  
  44.         cout<<"C::print_C2()!"<<endl;  
  45.         }  
  46.     };  
  47. class D:public A, public B,public C  
  48. {  
  49.     public:  
  50.     D(int i, int j, int k):A(i),B(j),C(k){}  
  51. };  
  52. /*满足上述继承关系的类,最终类D的内存大小应该是多少字节呢?以及类D的对象的内存布局是什么样的呢?下面就通过编写mian函数,将这两个问题表示出来。*/  
  53. typedef void (*func)(void);  
  54.   
  55. int main()  
  56. {  
  57.     cout << sizeof(D) << endl;  
  58.     D d(12,16,25);  
  59.     cout<<showbase<<hex<<"类D对象d的内存地址: "<<(int*)&d<<endl;  
  60.     func func1, func2, func3,func4,func5,func6;  
  61.     func1 = (func)*(int*)*(int*)&d;  
  62.     cout<<"第一个指向虚函数表的指针地址: "<<(int*)&d<<endl;  
  63.     cout<<"基类A的第一个虚函数在虚函数表中的地址:"<<(int*)*(int*)&d<<endl;  
  64.     func1();  
  65.     func2 = (func)*((int*)*(int*)&d +1);  
  66.     cout<<"基类A的第二个虚函数在虚函数表中的地址:"<<(int*)*(int*)&d +1<<endl;  
  67.     func2();  
  68.   
  69.     cout<<"第二个指向虚函数表的指针地址: "<<(int*)&d +2<<endl;  
  70.     func3 = (func)*(int*)*((int*)&d+2);  
  71.     cout<<"基类B的第一个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+2)<<endl;  
  72.     func3();  
  73.     func4 = (func)*((int*)*((int*)&d+2)+1);  
  74.     cout<<"基类B的第二个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+2) + 1<<endl;  
  75.     func4();  
  76.   
  77.   
  78.     cout<<"第三个指向虚函数表的指针地址: "<<(int*)&d +4<<endl;  
  79.     func5 = (func)*(int*)*((int*)&d+4);  
  80.     cout<<"基类C的第一个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+4)<<endl;  
  81.     func5();  
  82.     func6 = (func)*((int*)*((int*)&d+4)+1);  
  83.     cout<<"基类C的第二个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+4) + 1<<endl;  
  84.     func6();  
  85.     return 0;  
  86. }  

上面的输出结果是:



从上面输出结果可以知道:

1.      类D分别继承自类A,类B,类C;它总共有3个虚函数表。因此它的总得内存大小为24个字节。

2.      后面的内容分别用地址访问到了类D中的继承的所有虚函数,从继承的方式上也能看出类D中是存在三个虚函数表的。

3.      上面没有对类D中的变量进行地址的访问,不过这些可以依照上面提到的进行提取他们的地址。

4、 类D的内存布局是:

      类A中的虚函数表指针

      类A中的成员变量

类B中的虚函数表指针

      类B中的成员变量

类C中的虚函数表指针

      类C中的成员变量




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值