【C++】继承机制深度解析:多继承与菱形继承
目录
0. 上篇:
1. 继承与友元
2. 继承与静态成员
3. 复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
官方的解决方法:
虚拟继承解决数据冗余和二义性的原理:
4. 继承和组合
概念
实际运用中应该使用继承还是组合呢?
0. 上篇:
继承机制解析与使用示例
1. 继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{// 如果需要使用,就需要再声明友元// friend void Display(const Person& p, const Student& s);
protected:int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl; // 编译失败
}
void main()
{Person p;Student s;Display(p, s);
}
2. 继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
class Person
{
public:Person() { ++_count; }
protected:string _name; // 姓名
public:static int _count; // 统计人的个数。
};int Person::_count = 0;class Student : public Person
{
protected:int _stuNum; // 学号
};int main()
{Person p;Student s;p._count = 1;s._count = 2; // 赋值都是同一个静态变量Person::_count++;// 这里的++也是对同一个静态变量++cout << Person::_count << endl; // 3cout << Student::_count << endl; // 3return 0;
}
3. 复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
(多继承是原罪,是一个大坑,可能导致菱形继承)
菱形继承:菱形继承是多继承的一种特殊情况。
继承是类的复用,此时 Student 里面有一个 Person, Teacher 也有个 Person,此时就导致了数据冗余和二义性。
class Person
{
public:string _name; // 姓名
};
class Student : public Person
{
protected:int _num; //学号
};
class Teacher : public Person
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
void Test()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name = "peter"; // 报错// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
官方的解决方法:
使用 virtual 关键字,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Person
{
public:string _name; // 姓名
};
class Student : virtual public Person // 虚拟继承
{
protected:int _num; //学号
};
class Teacher : virtual public Person // 虚拟继承
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
void Test()
{Assistant a;a._name = "peter"; // 现在就不会报错了
}
虚拟继承->底层的对象模型非常复杂,且有一定效率损失
虚拟继承解决数据冗余和二义性的原理:
为了研究虚拟继承原理,我们写一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
// 内存对象模型(对象在内存中是怎么存的?)
class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};int main()
{D d;cout << sizeof(d) << endl;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
从上图可以看出,03 和 04 上面还存了点东西,03的前一个地址上存的是0x0084ca7c,04的前一个地址上存的是0x0084ca88,这好像是个指针,我们来到指针所指向的空间看一下:
指针指向空间保存的值是一个偏移量,正好就是指针的地址向_a所在地址空间偏移的偏移量:例如,0x005BFA94 - 0x005BFA80 = 0x14
也就是说,这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。
实际中,不到万不得已,不要把类的关系设计成菱形继承。
4. 继承和组合
概念
/* 继承 */
class A{};
class B : public A
{};
/* 组合 */
class C{};
class D
{C c;
};
上述代码中,A和B的关系就是继承,C和D的关系就是复用,他们都完成了类层次的复用。
继承是一种白箱复用,父类对子类基本是透明的,但是它一定程度上破坏了父类的封装。
组合是一种黑箱复用,C对D是不透明的,C保持着他的封装,D只能使用C的公有成员。
实际运用中应该使用继承还是组合呢?
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
// Car和BMW Car和Benz构成is-a的关系,BMW是一辆车,使用继承
class Car {
protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号
};class BMW : public Car {
public:void Drive() { cout << "好开-操控" << endl; }
};class Benz : public Car {
public:void Drive() { cout << "好坐-舒适" << endl; }
};// Tire和Car构成has-a的关系,车有一个轮胎,使用组合
class Tire {
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸};class Car {
protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t; // 轮胎
};
也就是说,实际运用中,符合 is-a 就使用继承,符合 has-a 就使用组合,两个都符合,就优先使用组合。
参考阅读:
优先使用对象组合,而不是类继承 - 残雪余香 - 博客园