1 继承和友元
友元关系不能继承,因为友元不是类的成员,也就是说基类友元不能访问子类私有和保护成员。
2 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
3 复杂的菱形继承即菱形虚拟继承
3.1 单继承
单继承中子类只有一个基类。单继承子类对象模型:
3.2 多继承
多继承中子类至少有两个基类。
class B1
{
public:
int _b1;
};
class B2
{
public:
int _b2;
};
class D : public B2, public B1
{
public:
int _d;
};
int main()
{
cout << sizeof(D) << endl;
D d;
d._b1 = 1;
d._b2 = 2;
d._d = 3;
return 0;
}
多继承子类对象模型:
注意:如果是多继承,基类中成员在子类中的排列次序与继承列表中基类的先后次序一致。例如上述的B1,B2。
3.3 菱形继承(钻石继承)
class B
{
public:
void TestFunc()
{}
int _b;
};
// 单继承
class C1 : public B
{
public:
int _c1;
};
// 单继承
class C2 : public B
{
public:
int _c2;
};
// 多继承
class D : public C1, public C2
{
public:
int _d;
};
继承模型图:
上图中C1继承B,C2也继承B,这两种继承方式都是单继承。D继承C1,C2,D为多继承。整体的继承方式为菱形继承。派生类D中包含有两个_b,一个是C1继承基类的,另一个是C2继承基类。
菱形继承存在二义性问题:如果用户在派生类中需要改变_b 的值(d._b = 1),编译器就不知道从哪个基类中继承下来的值,因此产生了二义性。
二义性的解决办法:
- 让访问明确化–>对于存在二义性的成员在访问时加基类名称::
例如,d.C1::_b = 2; d.C2::f();该种解决方式可以让代码通过编译,但是最顶层基类中的成员在底层派生类仍然存储了两份。 - 本质上解决是让最顶层基类中成员在最底层中只存储了一份—>虚拟继承。
虚拟继承:在继承权限前面加上virtual关键字。
对象模型:
重要解释:
- 虚基表指针是指向偏移量表格的指针。上述虚基表的第一部分是派生类对象相对于自己的偏移量,因此为0;第二部分是基类部分相对于对象的偏移量。
- 派生类的大小=基类成员大小 + 子类新增成员大小 + 4;
- 从对象前4个字节中取出地址,在该地址上向后偏移4个字节,取出该空间的内容(也就是偏移量offset),让对象地址向后偏移offset个字节,就会拿到子类对象从基类中继承的成员变量。
- 例如上图中在子类对象模型中要访问基类成员变量_b,先在子类对象的前四个字节取出虚基表,再向后偏移4个字节就在虚基表中得到数字8,最后在子类对象的起始位置向后偏移8字节,得到基类成员变量_b。
虚拟继承唯一的作用就是在菱形继承中,解决菱形继承中二义性的问题。
菱形虚拟继承:
class B
{
public:
void f()
{}
int _b;
};
class C1 : virtual public B
{
public:
int _c1;
};
class C2 : virtual public B
{
public:
int _c2;
};
class D : public C1, public C2
{
public:
int _d;
};
};
菱形虚拟继承各类分布如上图所示,按照继承的先后顺序,C1类在最上面,下来是C2类,然后是子类新增的成员变量,最后是基类的成员变量。在C1类中虚基表指针0代表相对于自己的偏移量,20字节代表基类成员变量相对于C1类的偏移量,同理C2。注意:虚基表存储在常量区,不能被修改。
C1& c1 = d;
c1._c1 = 1;//直接访问
c1._b = 2; //通过虚基表的偏移量访问
C2& c2 = d;
c2._c2 = 1; //直接访问
c2._b = 2; //通过虚基表的偏移量访问