您的位置:首页 > 其它

结构体和类中的内存布局

2013-11-29 10:24 239 查看
通常我们访问结构体或类的成员变量,使用的是比较普通的方法。如定义一个struct

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中的变量进行地址的访问,不过这些可以依照上面提到的进行提取他们的地址。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: