结构体和类中的内存布局
2013-11-29 10:24
239 查看
通常我们访问结构体或类的成员变量,使用的是比较普通的方法。如定义一个struct
那么我们访问结构体中的成员有两种方法:1)(结构体对象名) . (成员变量名);2) (结构体指针) -> (成员变量名)。由于任何变量都在内存中对应一个地址,我们是不是可以通过指针地址的方式访问结构体的成员变量呢?
首先我们举一个简单的例子:
下面就举一个稍微复杂一些的例子,也就是本文开头提到的结构体A,只不过改了下名字。
该结构体包含了一个字符类型的变量a,整形变量b,双精度浮点型变量c,函数指针func。该结构体的内存大小是24个字节(考虑字节对齐)。那么如何实现利用地址访问结构体的成员变量呢?下面先给出实现访问该结构成员变量的代码,然后再分析该代码。
从输出可以看出来以上程序的结果是正确的,而且对每个成员变量的输出地址也能看出结构的内存布局。程序中主要注意两点:
1) 对指针地址进行加减运算时,实际的运算是加减该指针指向的变量类型的字节数*加减的值。
2) 提取函数指针变量的地址是要注意,结构的地址(&b)加上一定的偏移量得到的地址是
指向函数指针的指针,因此要对它做解地址操作,然后进行指针类型转换,即func pfunc_base = (func)*(int*)((double*)&b + 2);这个语句。
结构体的内存布局大致就是这样的,下面我们就讨论下更有意思的类的内存布局。由于类牵扯到继承,虚函数,多重继承,虚继承,因此类的内存布局比结构体复杂了很多。这里我们就一步一步的分析这些情况。
1) 首先分析单一继承的问题,并且先不考虑虚函数的情况,即类中只有成员变量,因为除了虚函数的成员函数都不会占用类的内存空间,所以我们可以忽略他们。下面就是一个简单的例子。
从上面的输出可以看出,在类的继承时,派生类的首地址指向了其继承自基类的成员变量;换句话说就是派生类中的内存布局是:先基类对象,再是派生类的成员变量。如果类内的成员变量的类型比较复杂,那么就应该考虑字节对齐的原则了。为了更好的表明类中的内存布局,在举一个例子说明下,在这里引入了虚函数,并且增加了类中的成员变量的类型:
其输出结果是:
有虚函数的单一继承的派生类中的内存布局:首先是指向虚函数表的指针(放在首位是为了提高效率),其次是基类的成员变量,最后是派生类的成员变量。注:在对虚函数的指针进行转换时一定要注意:类的内存存放的是vptr指针,而vptr是指向虚函数表的指针,对vptr解地址运算,即*vptr,得到的是虚函数表中的第一个指针,但这个指针还不是函数指针,应该在对其做一个解地址运算,并做类型转换,即上述例程中的func_show
= (func)*(int*)*(int*)&b;。
多重继承的情况
对于多重继承,首先要明确多重继承和虚函数的关系。对于单一继承来说,如果class B继承自class A;且A中有虚函数,因此class B中也有一个指向虚函数表的指针。但是对于多重继承,派生类会有多个虚函数表的指针,因为它的每个父类都有可能有虚函数。举个例子来说明下吧:
上面的输出结果是:
从上面输出结果可以知道:
1. 类D分别继承自类A,类B,类C;它总共有3个虚函数表。因此它的总得内存大小为24个字节。
2. 后面的内容分别用地址访问到了类D中的继承的所有虚函数,从继承的方式上也能看出类D中是存在三个虚函数表的。
3. 上面没有对类D中的变量进行地址的访问,不过这些可以依照上面提到的进行提取他们的地址。
struct A { char a; int b; double c; void (*func) (A *); };
那么我们访问结构体中的成员有两种方法:1)(结构体对象名) . (成员变量名);2) (结构体指针) -> (成员变量名)。由于任何变量都在内存中对应一个地址,我们是不是可以通过指针地址的方式访问结构体的成员变量呢?
首先我们举一个简单的例子:
struct example { int a; int b; };//定义一个结构体 example ex; // 创建一个结构体变量 ex.a =1; ex.b = 12;//初始化结构体变量 /*下面实现对结构体成员变量的指针访问;因为结构体的中的内存布局是有一定规律的,对于本例子中,它的内存布局:先存放int类型变量a,然后是int类型变量b;因此对结构体变量ex取地址,并将其转换为(int*)类型,便可获得成员变量a的地址;同样b的地址是对a的地址加1即可(这里要注明的是:对地址进行加1操作的实质是对地址进行+ 1*(指针所指类型的字节大小))*/ cout<<showbase<<hex<<&ex<<endl; // 输出结构体变量的地址,结果:0x28ff30;不同机器值//可能不一样 int *pa = (int*)&ex; // pa的值为0x28ff30 cout<<*pa<<endl; //输出结果:1;即输出的是ex.a的值 int *pb = (int*)&ex +1; // pb的值为0x28ff34 cout<<*pb<<endl;// 输出结果:12;即输出的ex.b的值 /*以上代码就实现了利用地址访问结构的成员变量*/
下面就举一个稍微复杂一些的例子,也就是本文开头提到的结构体A,只不过改了下名字。
struct Base { char a; int b; double c; void (*func_base)(Base*); };
该结构体包含了一个字符类型的变量a,整形变量b,双精度浮点型变量c,函数指针func。该结构体的内存大小是24个字节(考虑字节对齐)。那么如何实现利用地址访问结构体的成员变量呢?下面先给出实现访问该结构成员变量的代码,然后再分析该代码。
void show_base(Base *base) // 定义函数 { cout<<base->a<<" "<<base->b<<" "<<base->c<<endl; } Base new_base(char i, int j, double k)//创建结构体的函数,类似于类的构造函数 { Base base; base.a = i; base.b = j; base.c = k; base.func_base = show_base; return base; } int main() { cout<<sizeof(Base)<<endl; Base b = new_base('a', 15,1.56);// 创建结构体 cout<<"结构体赋值后的输出: "; b.func_base(&b); cout<<"结构体初始地址是:"<<showbase<<hex<<&b<<endl; char *pa = (char*)&b; cout<<"成员变量a的地址是:"<<showbase<<hex<< (int*)pa<<"; a的值是:"<<*pa<<endl; int *pb = (int*)&b +1; cout<<"成员变量b的地址是:"<<showbase<<hex<< (int*)pb<<"; b的值是:"<<*pb<<endl; double *pc = (double*)((int*)&b +2); cout<<"成员变量c的地址是:"<<showbase<<hex<< (int*)pc<<"; c的值是:"<<*pc<<endl; typedef void (*func)(Base *); func pfunc_base = (func)*(int*)((double*)&b + 2); cout<<"成员变量func_base的地址是:"<<showbase<<hex<< (int*)pfunc_base<<endl; cout<<"成员变量func_base的结果:"; pfunc_base(&b); return 0; }
从输出可以看出来以上程序的结果是正确的,而且对每个成员变量的输出地址也能看出结构的内存布局。程序中主要注意两点:
1) 对指针地址进行加减运算时,实际的运算是加减该指针指向的变量类型的字节数*加减的值。
2) 提取函数指针变量的地址是要注意,结构的地址(&b)加上一定的偏移量得到的地址是
指向函数指针的指针,因此要对它做解地址操作,然后进行指针类型转换,即func pfunc_base = (func)*(int*)((double*)&b + 2);这个语句。
结构体的内存布局大致就是这样的,下面我们就讨论下更有意思的类的内存布局。由于类牵扯到继承,虚函数,多重继承,虚继承,因此类的内存布局比结构体复杂了很多。这里我们就一步一步的分析这些情况。
1) 首先分析单一继承的问题,并且先不考虑虚函数的情况,即类中只有成员变量,因为除了虚函数的成员函数都不会占用类的内存空间,所以我们可以忽略他们。下面就是一个简单的例子。
class A { public: A(int i):a(i){} int a; //之所以声明为public,是方便我们以后调用成员变量 }; class B:public A { public: B(int i, int j):A(i),b(j){}; int b; //之所以声明为public,是方便我们以后调用成员变量 }; int main() { B b(12,15); cout<<"类对象b的地址为: "<<&b<<endl; int *pa = (int*)(&b); cout<<"类对象b中继承自类A的成员变量a的地址:" <<showbase<<hex<<(int*)pa<<"其值为:"<<*pa<<endl; int *pb = (int*)((int*)(&b)+1); cout<<"类对象b中的成员变量b的地址:" <<showbase<<hex<<(int*)pb<<"其值为:"<<*pb<<endl; return 0; }
从上面的输出可以看出,在类的继承时,派生类的首地址指向了其继承自基类的成员变量;换句话说就是派生类中的内存布局是:先基类对象,再是派生类的成员变量。如果类内的成员变量的类型比较复杂,那么就应该考虑字节对齐的原则了。为了更好的表明类中的内存布局,在举一个例子说明下,在这里引入了虚函数,并且增加了类中的成员变量的类型:
class A { public: A(int i,double da):a(i),da(da){} virtual void show() { cout<<"this is A::show!"<<endl; } int a; double da; }; class B:public A { public: B(int i,double da, int j,double db) :A(i,da),b(j),db(db){}; void show() { cout<<"this is B::show!"<<endl; } int b; double db; }; int main() { B b(12,3.15,15,6.89); cout<<"类对象b的地址为: "<<&b<<endl; int *vptr = (int*)*(int*)(&b); cout<<"类对象b中指向虚函数表的指针地址是:" <<showbase<<hex<<(int*)vptr<<"其值为:"<<*vptr<<endl; cout<<"虚函数表的第一个函数的地址是: "<<*vptr<<endl; typedef void (*func)(void); func func_show; func_show = (func)*(int*)*(int*)&b; cout<<"转换后的虚函数表的第一个函数的地址是:"<<showbase<<hex<<(int*)func_show<<endl; func_show(); int *pa = (int*)((int*)(&b)+1); cout<<"类对象b中继承自类A的成员变量a的地址:" <<showbase<<hex<<(int*)pa<<"其值为:"<<*pa<<endl; double *pda = (double*)((int*)(&b)+2); cout<<"类对象b中继承自类A的成员变量da的地址:" <<showbase<<hex<<(int*)pda<<"其值为:"<<*pda<<endl; int *pb = (int*)((int*)(&b)+4); cout<<"类对象b中的成员变量b的地址:" <<showbase<<hex<<(int*)pb<<"其值为:"<<*pb<<endl; double *pdb = (double*)((int*)(&b)+6); cout<<"类对象b中的成员变量db的地址:" <<showbase<<hex<<(int*)pdb<<"其值为:"<<*pdb<<endl; return 0; }
其输出结果是:
有虚函数的单一继承的派生类中的内存布局:首先是指向虚函数表的指针(放在首位是为了提高效率),其次是基类的成员变量,最后是派生类的成员变量。注:在对虚函数的指针进行转换时一定要注意:类的内存存放的是vptr指针,而vptr是指向虚函数表的指针,对vptr解地址运算,即*vptr,得到的是虚函数表中的第一个指针,但这个指针还不是函数指针,应该在对其做一个解地址运算,并做类型转换,即上述例程中的func_show
= (func)*(int*)*(int*)&b;。
多重继承的情况
对于多重继承,首先要明确多重继承和虚函数的关系。对于单一继承来说,如果class B继承自class A;且A中有虚函数,因此class B中也有一个指向虚函数表的指针。但是对于多重继承,派生类会有多个虚函数表的指针,因为它的每个父类都有可能有虚函数。举个例子来说明下吧:
class A { private: int a; public: A(int i):a(i){} virtual void print_A1() { cout<<"A::print_A1()!"<<endl; } virtual void print_A2() { cout<<"A::print_A2()!"<<endl; } }; class B { private: int b; public: B(int j):b(j){} virtual void print_B1() { cout<<"B::print_B1()!"<<endl; } virtual void print_B2() { cout<<"B::print_B2()!"<<endl; } }; class C { private: int c; public: C(int k):c(k){} virtual void print_C1() { cout<<"C::print_C1()!"<<endl; } virtual void print_C2() { cout<<"C::print_C2()!"<<endl; } }; class D:public A, public B,public C { public: D(int i, int j, int k):A(i),B(j),C(k){} }; /*满足上述继承关系的类,最终类D的内存大小应该是多少字节呢?以及类D的对象的内存布局是什么样的呢?下面就通过编写mian函数,将这两个问题表示出来。*/ typedef void (*func)(void); int main() { cout << sizeof(D) << endl; D d(12,16,25); cout<<showbase<<hex<<"类D对象d的内存地址: "<<(int*)&d<<endl; func func1, func2, func3,func4,func5,func6; func1 = (func)*(int*)*(int*)&d; cout<<"第一个指向虚函数表的指针地址: "<<(int*)&d<<endl; cout<<"基类A的第一个虚函数在虚函数表中的地址:"<<(int*)*(int*)&d<<endl; func1(); func2 = (func)*((int*)*(int*)&d +1); cout<<"基类A的第二个虚函数在虚函数表中的地址:"<<(int*)*(int*)&d +1<<endl; func2(); cout<<"第二个指向虚函数表的指针地址: "<<(int*)&d +2<<endl; func3 = (func)*(int*)*((int*)&d+2); cout<<"基类B的第一个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+2)<<endl; func3(); func4 = (func)*((int*)*((int*)&d+2)+1); cout<<"基类B的第二个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+2) + 1<<endl; func4(); cout<<"第三个指向虚函数表的指针地址: "<<(int*)&d +4<<endl; func5 = (func)*(int*)*((int*)&d+4); cout<<"基类C的第一个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+4)<<endl; func5(); func6 = (func)*((int*)*((int*)&d+4)+1); cout<<"基类C的第二个虚函数在虚函数表中的地址:"<<(int*)*((int*)&d+4) + 1<<endl; func6(); return 0; }
上面的输出结果是:
从上面输出结果可以知道:
1. 类D分别继承自类A,类B,类C;它总共有3个虚函数表。因此它的总得内存大小为24个字节。
2. 后面的内容分别用地址访问到了类D中的继承的所有虚函数,从继承的方式上也能看出类D中是存在三个虚函数表的。
3. 上面没有对类D中的变量进行地址的访问,不过这些可以依照上面提到的进行提取他们的地址。
相关文章推荐
- VC中结构体的内存布局
- 《coredump问题原理探究》Linux x86版5.8节C风格数据结构内存布局之结构体数组结构体coredump
- 一个结构体的内存布局
- 《coredump问题原理探究》Linux x86版5.6节C风格数据结构内存布局之复合类型构成的结构体
- c#编程指南(十二) 平台调用P-INVOKE完全掌握, 结构体边界对齐和内存布局
- .net 互操作之p/invoke- 数据封送(结构体中的字符串,其他字段,内存布局)(4)
- 结构体的内存布局
- 平台调用P-INVOKE完全掌握, 结构体边界对齐和内存布局
- 结构体内存布局
- 一个结构体的内存布局
- 分析编译条件产生结构体内存字节布局
- 继承 虚继承 结构体 为内存布局抛砖引玉
- 《coredump问题原理探究》Linux x86版5.5节C风格数据结构内存布局之基本数据类型构成的结构体
- .net 互操作之p/invoke- 数据封送(结构体中的字符串,其他字段,内存布局)(4)
- C语言结构体在内存中的布局(直接对齐)
- 结构体对齐——结构体内存布局
- 对象内存布局 (3)
- c语言内存布局详解
- 结构体所占内存
- 对象内存布局 (12)