多重继承

多重继承基本概念

定义

多重继承是C++独有的面向对象特性(Java、C#等语言不原生支持),指一个类同时继承自多个父类,子类会拥有所有父类的属性和方法,对应现实中“一个事物具有多个身份”的场景。

语法格式

class 子类名 : 继承方式 父类1, 继承方式 父类2, ..., 继承方式 父类n {
    // 子类成员
};

示例:水陆两栖装甲车(既是车也是船)

// 父类1:车
class Car {
    int x;
public:
    Car(int x=0):x(x){}
    void show(){cout << x << endl;}
};

// 父类2:船
class Boat {
    float y;
public:
    Boat(float y=0.0):y(y){}
    void show(){cout << y << endl;}
};

// 子类:水陆两栖装甲车(多重继承Car和Boat)
class AmphibiousVehicle : public Car, public Boat {
public:
    AmphibiousVehicle(int x=0, float y=0.0);
};
多重继承关系图

构造函数

多重继承的子类构造时,需在初始化列表中显式调用每个父类的构造函数(若未显式调用,则默认调用父类的无参构造函数)。

实现示例

// 子类构造函数实现
AmphibiousVehicle::AmphibiousVehicle(int x, float y)
    : Car(x), Boat(y)  // 按继承顺序调用父类构造
{
    // 子类自身初始化逻辑
}

二义性问题

当多个父类拥有重名的方法或属性时,子类直接调用该成员会产生“二义性”(编译器无法确定调用哪个父类的版本),导致编译错误。

错误示例

int main() {
    AmphibiousVehicle v(100, 1.23);
    v.show();  // 错误:Car和Boat都有show(),二义性
}

重名成员处理

解决二义性的核心是使用 域解析符:: 明确指定父类来源。

重名成员结构

正确调用示例

int main() {
    AmphibiousVehicle v(100, 1.23);
    v.Car::show();   // 调用Car类的show(),输出100
    v.Boat::show();  // 调用Boat类的show(),输出1.23
    
    // 若有重名属性,同样用域解析符
    // 假设Car和Boat都有name属性
    // v.Car::name = "越野车";
    // v.Boat::name = "快艇";
}

拓展:多重继承的优缺点

优点

  • 直观表达“多身份”场景(如两栖车=车+船)
  • 代码复用更灵活,可整合多个父类的功能

缺点

  • 二义性问题:重名成员需手动处理
  • 逻辑复杂度高:类关系混乱,可读性下降
  • 菱形继承陷阱:多个父类继承自同一基类时产生数据冗余
  • 兼容性差:其他OOP语言(Java、C#)不支持,跨语言移植困难

菱形继承(钻石继承)

定义与问题

当多重继承的多个父类共同继承自同一个基类时,子类会包含该基类的多个副本(数据冗余),类关系呈菱形结构,称为菱形继承。

菱形继承结构

核心问题

  • 数据冗余:子类拥有基类的多份成员(如示例中AmphibiousVehicle有2个Vehicle的trade成员)
  • 逻辑矛盾:同一属性应唯一(如交通工具的生产厂商不应有2个)

典型场景分析

以交通类为例:

// 基类:交通工具
class Vehicle {
public:
    string trade;  // 生产厂商(应唯一)
};

// 父类1:汽车(继承Vehicle)
class Car : public Vehicle {
    // 汽车特有成员
};

// 父类2:船(继承Vehicle)
class Boat : public Vehicle {
    // 船特有成员
};

// 子类:水陆两栖装甲车(继承Car和Boat)
class AmphibiousVehicle : public Car, public Boat {
    // 两栖车特有成员
};
交通类菱形继承关系

问题演示

int main() {
    AmphibiousVehicle v;
    // 二义性:两个trade成员,需指定来源
    v.Car::trade = "中国兵器工业集团";
    v.Boat::trade = "中国船舶工业集团";  // 逻辑矛盾:一个交通工具不应有两个厂商
}

拓展:菱形继承的其他表现形式

菱形继承并非严格的“四层结构”,以下场景也属于菱形继承变体:

  • 多层传递:基类A → B → D,A → C → D(仍是菱形结构)
  • 多中间类:A → B、A → C、A → E → D(D继承B、C、E,仍含多份A)

虚继承(解决菱形继承)

语法规则

C++引入虚继承机制,通过virtual关键字修饰继承关系,使末端子类仅保留基类的一份副本,解决数据冗余和逻辑矛盾。

语法格式

// 中间类(如Car、Boat)虚继承基类(如Vehicle)
class 中间类名 : virtual 继承方式 基类名 {
    // 类成员
};

示例

// 基类:交通工具
class Vehicle {
public:
    string trade;
    Vehicle(string t="null"):trade(t){}
    void company()const{cout << "生产厂商:" << trade << endl;}
};

// 中间类1:汽车(虚继承Vehicle)
class Car : virtual public Vehicle {
    int x;
public:
    Car(int x=0, string t="null"):Vehicle(t), x(x){}
    void show(){cout << x << endl;}
};

// 中间类2:船(虚继承Vehicle)
class Boat : virtual public Vehicle {  // virtual位置可在public前后
    float y;
public:
    Boat(float y=0.0, string t="null"):Vehicle(t), y(y){}
    void show(){cout << y << endl;}
};
虚继承结构

核心原理

虚继承的核心是改变基类成员的归属权

  • 非虚继承:中间类(B、C)各自拥有基类(A)的副本,末端类(D)继承中间类的副本
  • 虚继承:基类(A)的副本直接归属于末端类(D),中间类(B、C)仅保留“引用”,不再拥有独立副本

代码实现

末端子类需显式调用基类的构造函数(虚继承后,中间类不再自动调用基类构造):

// 末端子类:水陆两栖装甲车
class AmphibiousVehicle : public Car, public Boat {
public:
    // 必须显式调用虚基类Vehicle的构造函数(否则调用默认构造)
    AmphibiousVehicle(string t="null", int x=0, float y=0.0)
        : Vehicle(t),  // 直接初始化虚基类
          Car(x),      // 中间类构造,无需再传t(虚基类由末端子类初始化)
          Boat(y) {}
};

// 测试代码
int main(int argc, char const *argv[]) {
    AmphibiousVehicle v("中国兵器工业集团有限公司", 100, 1.23);
    v.company();  // 输出:生产厂商:中国兵器工业集团有限公司(仅一份副本)
    return 0;
}

注意事项

  1. virtual关键字的作用范围:仅对“末端子类”有效,中间类(如Car、Boat)自身构造时仍会正常调用基类构造。
  2. 构造函数调用顺序:虚基类的构造函数优先于非虚基类和中间类执行。
  3. virtual与多态的区别:此处virtual是“虚继承”关键字,与“虚函数”(多态)无任何关系,仅复用关键字。
  4. 多重虚继承:若有多个虚基类,末端子类需在初始化列表中依次调用所有虚基类的构造函数。

拓展:虚继承的底层实现(虚基类表)

C++编译器通过虚基类表(Virtual Base Table, VBT) 实现虚继承:

  • 中间类(如Car、Boat)会增加一个“虚基类表指针(vbptr)”,指向虚基类表
  • 虚基类表中存储中间类到虚基类(Vehicle)的偏移量
  • 末端子类(AmphibiousVehicle)初始化时,通过偏移量找到唯一的虚基类实例,避免多份副本

底层结构简化示意图

AmphibiousVehicle 对象:
├─ vbptr(Car的虚基类表指针)
├─ x(Car的成员)
├─ vbptr(Boat的虚基类表指针)
├─ y(Boat的成员)
└─ Vehicle 实例(唯一副本)
   └─ trade

核心知识点总结

概念 核心要点
多重继承 一个类继承多个父类,需处理重名成员的二义性(域解析符::
菱形继承 多个父类继承自同一基类,导致基类成员多份副本,逻辑矛盾
虚继承 virtual修饰中间类的继承关系,使末端子类仅保留基类一份副本
构造函数 虚继承时,末端子类需显式调用虚基类构造函数,优先级高于中间类
二义性解决 重名成员用子类对象.父类名::成员指定来源

关键结论

  • 多重继承慎用:仅在确有“多身份”场景时使用,避免类关系复杂
  • 菱形继承必用虚继承:否则会产生数据冗余和逻辑错误
  • 虚继承仅修饰中间类:末端子类无需加virtual,仅中间类(如Car、Boat)需虚继承基类

练习:交通类设计实战

要求

编程实现以下类,运用多重继承和虚继承技巧:

类名 关系 要求
Vehicle(交通工具) 基类 1. 虚函数move()(行走功能);2. 实函数getID()(获取牌照);3. 牌照属性ID
Car(汽车) 虚继承Vehicle 汽车特有成员(如wheelNum车轮数)
Boat(船) 虚继承Vehicle 船特有成员(如displacement排水量)
AmphibiousVehicle 多重继承Car和Boat 两栖车特有成员(如armorThickness装甲厚度)

参考实现

#include <iostream>
#include <string>
using namespace std;

// 基类:交通工具
class Vehicle {
protected:
    string ID;  // 牌照
public:
    Vehicle(string id=""):ID(id){}
    // 虚函数:行走功能(多态)
    virtual void move() {
        cout << "交通工具移动" << endl;
    }
    // 实函数:获取牌照
    string getID() {
        return ID;
    }
};

// 汽车类:虚继承Vehicle
class Car : virtual public Vehicle {
protected:
    int wheelNum;  // 车轮数
public:
    Car(string id="", int num=4):Vehicle(id), wheelNum(num){}
    void move() override {
        cout << "汽车用" << wheelNum << "个轮子行驶" << endl;
    }
};

// 船类:虚继承Vehicle
class Boat : virtual public Vehicle {
protected:
    float displacement;  // 排水量(单位:吨)
public:
    Boat(string id="", float dis=100.0):Vehicle(id), displacement(dis){}
    void move() override {
        cout << "船用排水量" << displacement << "吨的船体航行" << endl;
    }
};

// 水陆两栖装甲车:多重继承Car和Boat
class AmphibiousVehicle : public Car, public Boat {
private:
    float armorThickness;  // 装甲厚度(单位:mm)
public:
    // 显式调用虚基类Vehicle的构造函数
    AmphibiousVehicle(string id="", int num=4, float dis=200.0, float thick=10.0)
        : Vehicle(id), Car(id, num), Boat(id, dis), armorThickness(thick){}
    
    // 重写move(),实现两栖功能
    void move() override {
        cout << "两栖装甲车:";
        Car::move();  // 调用汽车的move()
        Boat::move(); // 调用船的move()
        cout << "装甲厚度:" << armorThickness << "mm" << endl;
    }
};

// 测试
int main() {
    AmphibiousVehicle av("军A12345", 6, 300.0, 15.0);
    cout << "牌照:" << av.getID() << endl;  // 唯一牌照,无歧义
    av.move();  // 多态调用,输出两栖功能
    return 0;
}

拓展:相关特性与对比

与Java的对比(不支持多重继承的替代方案)

Java不支持类的多重继承,但提供两种替代方案:

  1. 接口(Interface):一个类可实现多个接口,接口仅含抽象方法和常量,无数据冗余
  2. 单继承+组合:通过“has-a”关系组合多个类的功能,而非“is-a”的继承关系

示例(Java接口替代)

// 接口:可行驶
interface Drivable { void drive(); }
// 接口:可航行
interface Navigable { void navigate(); }
// 类:两栖车实现两个接口
class AmphibiousVehicle implements Drivable, Navigable {
    public void drive() { /* 行驶逻辑 */ }
    public void navigate() { /* 航行逻辑 */ }
}

C++11后的多重继承增强

  • override关键字:明确标记重写父类虚函数,避免意外隐藏(如上述练习中的move() override
  • final关键字:禁止类被继承或函数被重写,限制多重继承的复杂度
    class FinalClass final { /* 此类不能被继承 */ };
    class Parent {
        virtual void func() final; /* 此函数不能被重写 */
    };
    

多重继承的实际应用场景

  • 混合类(Mixin):提供通用功能的类(如日志类、序列化类),通过多重继承混入主类
    class Loggable {  // 日志混合类
    public:
        void log(string msg) { cout << "日志:" << msg << endl; }
    };
    class Data : public Loggable, public Serializable {  // 混入日志和序列化功能
        // ...
    };
    
  • 多角色类:如“学生运动员”(继承Student类和Athlete类)、“管理员用户”(继承User类和Admin类)

常见陷阱与避坑指南

  1. 避免深层多重继承:继承层次超过3层+多重继承,会导致代码可读性极差
  2. 优先组合而非继承:若仅需复用功能,而非“is-a”关系,用组合(如class A { B b; };)替代继承
  3. 虚基类尽量简单:虚基类应仅包含公共属性和方法,避免复杂构造逻辑
  4. 明确重写虚函数:多重继承中虚函数易被隐藏,需用override明确标记
posted @ 2025-12-22 08:45  Jaklin  阅读(7)  评论(0)    收藏  举报