结构体和类中的内存布局

通常我们访问结构体或类的成员变量,使用的是比较普通的方法。如定义一个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中的变量进行地址的访问,不过这些可以依照上面提到的进行提取他们的地址。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值