多重继承与菱形继承

📅 2026/7/12 19:27:54
多重继承与菱形继承
多重继承与菱形继承一、多重继承概述1.1 单一继承 与 多重继承继承类型定义特点单一继承一个派生类只有一个直接基类简单、清晰、无二义性多重继承一个派生类有多个直接基类功能强大、但复杂、可能产生二义性// 单一继承一个派生类只有一个直接基类classChair{};classDeckChair:publicChair{};// 只有一个基类// 多重继承一个派生类有多个直接基类classChair{};classBed{};classDeckChair:publicBed,publicChair{};// 两个基类二、多重继承的基本语法classChair{protected:floatmoney;public:Chair(floatft122):money(ft){}floatMoney(){returnmoney;}};classBed{protected:floatmoney;public:Bed(floatft1200):money(ft){}floatMoney(){returnmoney;}};// 多重继承同时继承 Bed 和 ChairclassDeckChair:publicBed,publicChair{public:DeckChair(){}DeckChair(floatbf,floatef):Chair(ef),Bed(bf){}voidtotal(){// 存在二义性有两个 money 和两个 Money()floattotalBed::Money()Chair::Money();}};三、多重继承的构造顺序核心规则构造顺序按照继承声明顺序而不是初始化列表顺序classBase1{public:Base1(intx){coutBase1 构造: xendl;}};classBase2{public:Base2(intx){coutBase2 构造: xendl;}};classDerived:publicBase1,publicBase2{public:// 虽然初始化列表先写 Base2但实际先构造 Base1Derived(inta,intb):Base2(b),Base1(a){coutDerived 构造endl;}};intmain(){Derivedd(10,20);// 输出顺序// Base1 构造: 10 ← 按继承声明顺序先 Base1// Base2 构造: 20 ← 后 Base2// Derived 构造return0;}构造顺序图解class Derived : public Base1, public Base2 ↑ ↑ 声明顺序第1 声明顺序第2 构造顺序 第1步构造 Base1 第2步构造 Base2 第3步构造成员对象按声明顺序 第4步执行 Derived 构造函数体 注意初始化列表的顺序不影响构造顺序四、多重继承的二义性问题4.1 成员访问的二义性classChair{protected:floatmoney;public:floatMoney(){returnmoney;}};classBed{protected:floatmoney;public:floatMoney(){returnmoney;}};classDeckChair:publicBed,publicChair{public:voidtest(){// × 二义性有两个 money两个 Money()// money 100; // 编译错误// Money(); // 编译错误// 解决方案1使用作用域解析符Bed::money100;Chair::money200;floatbBed::Money();floatcChair::Money();// 解决方案2在派生类中重新定义覆盖}// 解决方案2重新定义同名成员floatMoney(){returnBed::Money()Chair::Money();}};4.2 解决方案总结解决方案语法适用场景作用域解析符Bed::money临时访问派生类重新定义定义同名函数需要统一接口虚继承virtual关键字解决菱形继承五、菱形继承问题5.1 菱形继承的定义菱形继承是指一个派生类同时继承自两个中间类而这两个中间类又共同继承自同一个基类形成的继承结构像菱形一样。图解5.2 菱形继承的三大问题5.2.1 问题一数据冗余多份拷贝#includeiostreamusingnamespacestd;classPerson{public:string name;intage;Person():name(),age(0){coutPerson 构造endl;}};classTeacher:publicPerson{public:string course;Teacher():course(){coutTeacher 构造endl;}};classStudent:publicPerson{public:intstuId;Student():stuId(0){coutStudent 构造endl;}};// 菱形继承classTeachingAssistant:publicTeacher,publicStudent{public:TeachingAssistant(){coutTeachingAssistant 构造endl;}};intmain(){TeachingAssistant ta;// 问题1数据冗余 - 存在两份 Person 的成员// ta.name 张三; // 二义性不知道是 Teacher::name 还是 Student::name// ta.age 25; // 二义性// 需要指定路径才能访问ta.Teacher::name张老师;ta.Student::name张同学;coutTeacher::name ta.Teacher::nameendl;coutStudent::name ta.Student::nameendl;// 查看内存大小coutsizeof(Person) sizeof(Person)endl;// 通常 2*指针大小coutsizeof(Teacher) sizeof(Teacher)endl;// Person coursecoutsizeof(Student) sizeof(Student)endl;// Person stuIdcoutsizeof(TeachingAssistant) sizeof(TeachingAssistant)endl;// 两份Personreturn0;}菱形继承的内存布局无虚继承5.2.2 问题二访问二义性classPerson{public:string name;voidspeak(){coutPerson speakendl;}};classTeacher:publicPerson{public:voidteach(){coutTeachingendl;}};classStudent:publicPerson{public:voidstudy(){coutStudyingendl;}};classTeachingAssistant:publicTeacher,publicStudent{public:voidwork(){// × 二义性有两个 name两个 speak()// name 张三; // 编译错误ambiguous// speak(); // 编译错误ambiguous// 需要明确指定路径Teacher::name张老师;Student::name张同学;Teacher::speak();Student::speak();}};intmain(){TeachingAssistant ta;// × 外部访问也有二义性// ta.name 张三; // 错误// ta.speak(); // 错误// 必须指定路径ta.Teacher::name张老师;ta.Teacher::speak();return0;}5.2.3 问题三构造顺序与重复构造#includeiostreamusingnamespacestd;classPerson{public:string name;Person(conststringn):name(n){coutPerson 构造: nameendl;}~Person(){coutPerson 析构: nameendl;}};classTeacher:publicPerson{public:Teacher(conststringn):Person(n){coutTeacher 构造: nendl;}~Teacher(){coutTeacher 析构endl;}};classStudent:publicPerson{public:Student(conststringn):Person(n){coutStudent 构造: nendl;}~Student(){coutStudent 析构endl;}};classTeachingAssistant:publicTeacher,publicStudent{public:TeachingAssistant(conststringn1,conststringn2):Teacher(n1),Student(n2){// 两次调用 Person 构造coutTeachingAssistant 构造endl;}~TeachingAssistant(){coutTeachingAssistant 析构endl;}};intmain(){TeachingAssistantta(张老师,张同学);return0;}Person 构造: 张老师 ← Teacher 的 Person 部分 Teacher 构造: 张老师 Person 构造: 张同学 ← Student 的 Person 部分重复 Student 构造: 张同学 TeachingAssistant 构造 TeachingAssistant 析构 Student 析构 Person 析构: 张同学 ← 析构 Student 的 Person Teacher 析构 Person 析构: 张老师 ← 析构 Teacher 的 Person问题Person 被构造了2次Person 被析构了2次两个 Person 对象有不同的 name 值矛盾同一个人不应该有两个名字六、虚继承6.1 虚继承的语法// 使用 virtual 关键字实现虚继承classTeacher:virtualpublicPerson{// 虚继承public:string course;};classStudent:virtualpublicPerson{// 虚继承public:intstuId;};// 最底层派生类classTeachingAssistant:publicTeacher,publicStudent{public:// 虚基类由最底层派生类直接初始化TeachingAssistant(conststringn,inta0,conststringc,intid0):Person(n,a),// 直接初始化虚基类Teacher(n,a,c),Student(n,a,id){}};6.2 虚继承的构造顺序#includeiostreamusingnamespacestd;classPerson{public:Person(conststringn){coutPerson 构造: nendl;}};classTeacher:virtualpublicPerson{public:Teacher(conststringn):Person(n){coutTeacher 构造endl;}};classStudent:virtualpublicPerson{public:Student(conststringn):Person(n){coutStudent 构造endl;}};classTeachingAssistant:publicTeacher,publicStudent{public:TeachingAssistant(conststringn):Person(n),// 虚基类由最底层直接初始化Teacher(n),Student(n){coutTeachingAssistant 构造endl;}};intmain(){TeachingAssistantta(张三);return0;}输出Person 构造: 张三 ← 虚基类最先构造只构造一次 Teacher 构造 ← 再构造 Teacher Student 构造 ← 再构造 Student TeachingAssistant 构造 ← 最后构造自身 // 构造顺序 // 1. Person虚基类 ← 最先构造只构造一次 // 2. Teacher ← 按继承声明顺序 // 3. Student ← 按继承声明顺序 // 4. TeachingAssistant ← 最后构造自身 // 注意不管初始化列表中的顺序如何虚基类总是最先构造6.3 虚继承的构造顺序规则第1步虚基类的构造函数按继承声明顺序只构造一次 ↓ 第2步非虚基类的构造函数按继承声明顺序 ↓ 第3步成员对象的构造函数按声明顺序 ↓ 第4步派生类自己的构造函数体 注意虚基类由最底层派生类直接初始化七、虚继承的内存布局7.1 内存图其中 Teacher 部分vbptr(虚基类指针)----指向 Teacher 虚基表 course Student 部分vbptr(虚基类指针)----指向 Student 虚基表 stuId7.2 虚基类指针vbptr与虚基表vbtable7.2.1 什么是 vbptr 和 vbtable概念全称含义存储位置vbptrvirtualbaseptr虚基类指针指向虚基表的指针每个虚继承的子对象中vbtablevirtualbasetable虚基表存储虚基类偏移量的表只读数据段.rodata7.2.2 vbptr 与 vbtable 的关系Teacher 部分 Teacher 的 vbtablevbptr(虚基类指针)----offset_to_Person64(从 Teacher 到 Person 的距离)course Student 部分 Student 的 vbtablevbptr(虚基类指针)----offset_to_Person32(从 Student 到 Person 的距离)stuId 通过 vbptr 访问 Person 的过程1.读取 vbptr 的值2.从 vbtable 中读取偏移量3.计算 Person 的实际地址当前地址偏移量4.访问 Person 的成员7.2.3 通过 vbptr 访问虚基类的完整过程// 代码ta.setName(张老师) // 编译器内部转换过程 // 1. ta 是 TeachingAssistant 对象 // 2. 通过 Teacher 路径访问 Person // 3. 获取 Teacher 部分的 vbptr // 4. 从 vbtable 读取偏移量例如 64 字节 // 5. 计算 Person 的实际地址 // 6. 调用 setName // 等价汇编伪代码 // mov rax, [ta] ; 获取 Teacher 部分的 vbptr // mov rdx, [rax] ; 从 vbtable 读取偏移量 // lea rbx, [ta rdx] ; 计算 Person 实际地址 // call Person::setName ; 调用 Person 的方法7.2.4 为什么 Teacher 和 Student 的偏移量不同偏移量不同的原因 TeachingAssistant 对象假设起始地址 0x1000 0x1000 Teacher 部分 ↓ 0x1000 vbptr指向 Teacher 的 vbtable 0x1008 course 0x1020 Student 部分 ↓ 0x1020 vbptr指向 Student 的 vbtable 0x1028 stuId 0x1030 TeachingAssistant 部分 ↓ department 0x1050 Person 部分只有一份 ↓ name, age Teacher 到 Person 的偏移 0x1050 - 0x1000 80 字节 Student 到 Person 的偏移 0x1050 - 0x1020 48 字节 因为 Teacher 和 Student 在对象中的位置不同所以偏移量不同7.2.5 虚继承的性能代价特性普通继承虚继承内存开销较小较大虚表指针访问速度快慢通过虚表间接访问构造复杂度简单复杂适用场景常规继承解决菱形继承八、虚继承 与 普通继承特性普通继承虚继承基类副本每个派生类一份所有派生类共享一份数据冗余有菱形继承时无访问二义性有菱形继承时无内存开销较小较大vbptr vbtable访问速度快直接访问慢通过 vbptr 间接访问构造顺序基类 → 派生类虚基类最先构造初始化责任每个派生类负责自己的基类最底层派生类初始化虚基类使用场景常规继承解决菱形继承特性单一继承多重继承虚继承基类数量1个多个多个解决菱形二义性问题无有解决数据冗余无可能有解决构造复杂度简单中等复杂内存开销小中等较大使用建议优先使用谨慎使用菱形继承时使用九、设计建议9.1 多重继承的使用原则原则说明优先使用组合能用组合就不用多重继承接口继承多重继承用于接口继承纯虚类避免菱形继承菱形继承复杂且容易出错使用虚继承必须使用菱形继承时用虚继承9.2 组合替代多重继承示例// 不推荐多重继承classHybridCar:privateFueEngine,privateElectricEngine{// 问题紧耦合不易维护};// 推荐组合classHybridCar{private:FueEngine fueEngine;// 燃油引擎ElectricEngine eleEngine;// 电动引擎public:voidstart(){fueEngine.start();eleEngine.start();}};9.3 问题与解决方案总结问题原因解决方案数据冗余多份基类副本虚继承访问二义性多个同名的成员虚继承 或 作用域解析重复构造基类被构造多次虚继承只构造一次不一致的状态多份副本独立修改虚继承共享一份