1.虚继承
虚继承是建立在多重继承之上的,当多重继承时,有可能存在重复继承的情况,如下图所示
在a中,B1,B2虚继承了A,所以当D多重继承了B1和B2时,最后只是间接的继承了一个A类
在b中,因为没有使用虚继承,所以最后D间接继承了两个A类,显然这样是不行的。
虚继承类并不是存放在类对象空间中的,而是会在类对象空间中存放一个指针,来指向虚继承的存储空间。
用此例来探究数据成员的存储结构
class X{};
class Y :public virtual X{};
class Z :public virtual X{};
class A :public Y, public Z{};
int main()
{
cout << sizeof(X) << endl;
cout << sizeof(Y) << endl;
cout << sizeof(Z) << endl;
cout << sizeof(A) << endl;
system("pause");
}
继承关系如图所示:
从这个例子中有以下几点可以总结:
①X是空类,也是会占一个字节的内存,这是编译器安插的一个char类型的字符,并且这一个字节是不会被继承的。
②Y和Z中都只有一个四字节的指针,这就是指向虚继承类的指针。(这只是针对VS,VC等编译器,有些编译器会是8字节)
③A是对Y和Z的自然继承,就是将这两个类中内容直接拷贝过来,而且自己又是空类,所以是8字节
class X{};
class Y :public X{};
class Z :public virtual X{};
class A :public Y, public Z{};
int main()
{
cout << sizeof(X) << endl;
cout << sizeof(Y) << endl;
cout << sizeof(Z) << endl;
cout << sizeof(A) << endl;
system("pause");
}
如果去掉其中一个虚继承,占内存就是这样,但是这样在实际中使用肯定是有问题的。
2.Data Member在对象存储空间中的布局(存储顺序)
只有非静态数据会存储在类对象的存储空间中,所以只需要考虑非静态数据在对象中存储空间的分布就行了。
一般编译器中都是按照声明声明顺序在存储空间中依次排列的,而vptr指针有的编译器放在对象开头,有的编译器放在对象结尾。
并且看一下下面这个类
class test
{
public:
int a;
double b;
private:
char c;
long d;
public:
string e;
protected:
short f;
}
这样的类在创建了一个类对象以后,对象空间中,还是依次按照a,b,c,d,e,f的顺序来存储非静态数据,并且这样写并不会造成额外的负担,MFC中这样的用法就很常见。
在这里就有一个奇怪的事,就是public,private,protected这些关键词去哪了,其实这些关键词都是编译器在起作用。以private为例,在编译阶段,编译器会检查private成员函数是否在类空间以外地方被使用,如果被使用了,编译是通不过的。
所以其实类对象从底层存储空间的角度来说,就是一个结构体,因为其他工作都是编译器来完成的。
3.模板实例化的两种方法
template <class T> T add(T a, T b)
{
return a + b;
}
int main()
{
cout << add<int>(3, 3) << endl;//方法一:最正规的方式使用模板实例化
cout << add(1, 2) << endl;//方法二:这样也可以
system("pause");
}
模板并不一定需要提前声明,会在实例中使用第一个被声明的形式。但是方法一更好,因为在模板类中前后不一时,就会有影响
cout << add<int>(3, 3.1) << endl;//可以编译通过
cout << add(1, 2.1) << endl;//编译不通过
本来应该输入同一种类型的入口参数,但是现在输入一个int,一个double,这样方法一会把3.1强转成int类型的3,而方法二则认为模板不统一,会报错。
4.指向data member的指针
如果不定义变量,取类中非静态成员的地址,得到的是该非静态成员相对于该类首地址的偏移量,而静态数据成员则是实际地址(其实当这个类被定义并且静态数据成员被初始化以后,静态数据成员已经被创建并且分配内存了)
class point3d{
public:
virtual void add();
//static point3d origin;
float x, y, z;
static int a;
};
int point3d::a = 10;
int main()
{
printf("%p\n", &point3d::x);
printf("%p\n", &point3d::y);
printf("%p\n", &point3d::z);
printf("%p\n", &point3d::a);
/*这样是错误的,得到的结果全是1
cout << & point3d::x << endl;
cout << &point3d::y << endl;
cout << &point3d::z << endl;
*/
system("pause");
}
所以通过类对象使用非静态成员时,可以有如下等价
point3d origin;
origin.y=0.0;
//origin.y经过编译器的处理以后为:*(&origin+(&Point3d::y));
5.子类中data member在存储空间中的布局
除了子类自己的非静态成员变量会存储在对象空间中以外,还有继承的父类所有的的内容都会拷贝过来,包括字节对齐,如下
继承关系如下:
之所以要把父类包括对齐的字节都复制过来,是当父类指针指向子类对象时,父类指针可以使用的内存空间都是父类的,并不会出现子类独有的成员。
注意:vptr指针也是直接继承,如果父类有就直接继承,并且不会创建新的vptr,如果没有,但自类中virtual关键词,就会创建一个vptr。
6.多重继承的data member内存布局
以一个例子来阐述
现在有几个类的继承关系如图所示
其中内存分布如图所示
可以看到最后的子类Vertex3d中,先是复制的point3d的成员,再是复制的Vertex成员,最后是自己的成员。这个顺序也是按照继承的声明顺序来写的。
其中如果Vertex的指针指向这个对象,那么编译器会做出一系列处理来保证这个指针仍然指向Vertex3d对象中的Vertex那部分。
7.类中定义的成员函数都是默认内联的