我们来写个小程序看看对象内部的内存布局。
A有三个成员a,b,c,一个函数A_func
B自己有一个成员d,和一个函数B_func
-
#include "stdio.h"
-
#include "string.h"
-
class A
-
{
-
private:
-
int a;
-
char b;
-
short c;
-
public:
-
A(int m,char n,short t)
-
{
-
a = m;b = n;c = t;
-
}
-
void A_func();
-
};
-
class B:public A
-
{
-
private:
-
char d;
-
public:
-
B(int m,char n,short t,char q):A(m,n,t)
-
{
-
d = q;
-
}
-
void B_func();
-
};
-
int main()
-
{
-
A x(1,2,3);
-
B y(1,2,3,4);
-
return 0;
-
}
然后我们设置断点,让程序在x和y初始化之后暂停运行,我们先来看一下基类A的实例x里面装了什么东西:
x占用8个字节,而且只放了成员变量的值,成员函数A_func不在它的内存布局里面。当我们打印x内存里面的内容时,我们可以看到,成员变量的排列和结构体一模一样,遵循一样的字节对齐规则(详看前一篇的讨论“从gdb看C/C++(三)——结构体和联合体”)。
再看看y的内存布局:
B在继承了A的成员的同时增加了一个成员char d。由于字节对齐的规则,B比A不止增加了1个字节的内存布局,而是4个字节。所以sizeof(y)=12.
此时A和B大致的内存布局是这样的。
字节对齐造成的内存空隙用随机数填充,所以对两个对象进行memcmp也是不可靠的。
内存布局决定了一个指针可以访问的范围,所以我们定义一个A类指针的时候,这个指针可以访问到8个字节,当我们定义一个B类指针的时候,这个指针可以访问到12个字节。
如果我们这样定义
A* x = &y;
那么x可以访问y体内的8个字节内容,没有问题,因为y体内有12个字节。
如果我们这样定义
B* y = &x;
那么y可以访问12个字节内容,但x体内只有8个字节,访问另外的4个字节,会产生不可预知的结果,所以子类指针指向父类是非法的,因为子类的内存布局总是大于或等于父类。
再看看有虚函数的类内存布局是怎么样的。
把A类里面的A_func改成虚函数
再次运行程序,然后暂停观察。
x在地址开始的地方增加了4个字节,用来存放_vptr指针,这个指针指向了x的虚函数表,C++就是用这个来实现多态的。
由上图可以看到,虽然在程序中B没有没有实现继承的虚函数,甚至没有声明它,但y内存中仍然有虚指针,是从A那里继承过来的。
此时A和B的内存布局大致如下
再试一下从多个基类继承的结果,先把程序改一下,增加一个基类C,里面包含一个虚函数。让B同时继承A和C
-
#include "stdio.h"
-
#include "string.h"
-
class A
-
{
-
private:
-
int a;
-
char b;
-
short c;
-
public:
-
A(int m,char n,short t)
-
{
-
a = m;b = n;c = t;
-
}
-
virtual void A_func()
-
{}
-
};
-
class C
-
{
-
private:
-
char e;
-
public:
-
C(char r)
-
{
-
e = r;
-
}
-
virtual void C_func(){}
-
};
-
class B:public A,public C
-
{
-
private:
-
char d;
-
public:
-
B(int m,char n,short t,char q,char r):A(m,n,t),C(r)
-
{
-
d = q;
-
}
-
void B_func();
-
};
-
int main()
-
{
-
A x(1,2,3);
-
B y(1,2,3,4,5);
-
return 0;
-
}
再看看B的实例y里面的内容
由上图打印出来的内存内容来看,B先安置从A继承过来的虚函数指针和成员,然后再安置从C身上得到的虚函数指针和成员,最后再放自己的成员d,先人后己。程序中定义了d=4,e=5,在上图可以看到e在地位,d在高位。
内存布局大概是这样的
注意,e在C类中占了4个字节,但B类中只给它分配了1个字节,这是因为要e本身只有1个字节,加上字节对齐原则,就跟d挤一块了。
关于这点还是有点疑惑的,在《深度探索C++对象模型》一书中说到,父类内存边界上的空隙是不给子类填充的,子类会另辟内存给自己新增的member,而不是把它放在父类留下的空隙中。而我在实际中发现子类的数据是可以填充到父类留下的内存空隙中的,就像上图所示,使用的编译器是GCC 5.4 。还是以实践为准吧。
阅读(5234) | 评论(0) | 转发(0) |