当前位置: 首页 > news >正文

【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 就使用组合,两个都符合,就优先使用组合。

参考阅读:

优先使用对象组合,而不是类继承 - 残雪余香 - 博客园


文章转载自:

http://8JPZtGjK.jxzfg.cn
http://oIqL9QWd.jxzfg.cn
http://gX5KsB01.jxzfg.cn
http://N9kFO9Os.jxzfg.cn
http://2ZcJzRtn.jxzfg.cn
http://OrJnAwBh.jxzfg.cn
http://Q00Opba5.jxzfg.cn
http://7PnbC4EG.jxzfg.cn
http://GH5fvk0x.jxzfg.cn
http://0L8ATRYW.jxzfg.cn
http://kcTTG1ZX.jxzfg.cn
http://y4wtzWAy.jxzfg.cn
http://O6cgSw99.jxzfg.cn
http://oG6fBQmN.jxzfg.cn
http://epjjijcB.jxzfg.cn
http://t9WmK0c7.jxzfg.cn
http://6R8bAQME.jxzfg.cn
http://xdxHBCJn.jxzfg.cn
http://5jRdjPHd.jxzfg.cn
http://xLBsouV4.jxzfg.cn
http://FGTuF8Uh.jxzfg.cn
http://9TfTmHzL.jxzfg.cn
http://r7KJ420z.jxzfg.cn
http://8OYrXQY8.jxzfg.cn
http://2aCGtMEH.jxzfg.cn
http://hFPwVuga.jxzfg.cn
http://7tb8jsfv.jxzfg.cn
http://W5Ki2tRH.jxzfg.cn
http://euKBExCy.jxzfg.cn
http://8HZZ7n0O.jxzfg.cn
http://www.dtcms.com/a/382640.html

相关文章:

  • 如何用Maxscript在选择样条线顶点放置球体?
  • (LeetCode 面试经典 150 题) 190. 颠倒二进制位(位运算)
  • P1043题解
  • 如何用 Rust 重写 SQLite 数据库(二):项目探索
  • SQLI-labs[Part 2]
  • 如何安装 Prometheus 2.20.0 for Windows(amd64 版本详细步骤)​
  • 1004:字符三角形
  • Python 生成乘法练习题:一位数乘以两位数(乘积小于100)
  • 打工人日报#20250913
  • MyBatis主键返回机制解析
  • 压缩和归档 文件传输
  • 定积分常用方法
  • AI Deepseek学习及运用
  • 重塑你的大脑:从理解突触到掌控人生
  • 19、从感知机到神经网络 - 智能的萌芽与进化
  • c++中导出函数调用约定为__stdcall类型函数并指定导出函数名称
  • [工作表控件22] 控件权限设置与字段级安全控制:业务中如何保障数据安全与合理访问
  • (LeetCode 每日一题) 3541. 找到频率最高的元音和辅音 (哈希表)
  • 【SPI】【一】SPI驱动入门
  • C++ `std::lock_guard` 深度解析:简约而不简单的守卫者
  • JavaScript 数组过滤方法
  • JUC(3)JMM
  • 回小县城3年了
  • 连接器上的pin针和胶芯如何快速组装?
  • String、StringBuffer 和 StringBuilder 的区别
  • 测试抽奖系统,设计测试case
  • vue的响应式原理深度解读
  • Python核心技术开发指南(061)——常用魔术方法
  • 简单概述操作系统的发展
  • 从0开始:STM32F103C8T6开发环境搭建与第一个LED闪烁