前面我们接触的都是 “单继承”,也就是一个派生类只继承一个基类。除此之外,C++ 还支持一个派生类同时继承多个基类,这种写法叫做 “多重继承(Multiple Inheritance)”。
比如一台办公一体机,既具备打印机的打印功能,又具备扫描仪的扫描功能,此时我们就可以让它同时继承 Printer 和 Scanner 两个基类。下面我们就来看看多重继承的基本用法,以及如何解决同名冲突与数据重复问题。
C++ 多重继承的基本用法
在 C++ 中,多重继承的语法非常简单:我们只需要在派生类名后面的英文冒号(:)之后依次列出多个基类,各个基类之间用英文逗号(,)隔开就可以了。
语法:
class 派生类名 : public 基类1, public 基类2 {
// 派生类新增的成员
};说明:
与单个基类的继承一样,派生类会同时拥有列出的所有基类成员。当我们创建派生类对象时,程序会按照继承列表中基类的声明顺序,从左到右依次调用各个基类的构造函数,最后再执行派生类自己的构造函数。销毁对象时,析构函数的执行顺序则刚好相反。
示例 1:同时继承两个基类
#include <iostream>
class Printer {
public:
void print() {
std::cout << "正在打印文件\n";
}
};
class Scanner {
public:
void scan() {
std::cout << "正在扫描文件\n";
}
};
// 同时公有继承 Printer 和 Scanner
class AllInOne : public Printer, public Scanner {};
int main() {
AllInOne device;
device.print();
device.scan();
return 0;
}运行结果如下。
正在打印文件
正在扫描文件分析:
在这个例子中,派生类 AllInOne 同时公有继承了 Printer 和 Scanner 两个基类。创建出对象 device 后,它既可以调用 Printer 的 print() 函数,也可以调用 Scanner 的 scan() 函数。
注意 在多重继承中,每一个基类前面都要显式写上 public。如果漏掉第二个 public 写成 public Printer, Scanner,后面的 Scanner 默认会变成 private 继承,小伙伴们写代码时千万不要漏掉。
多重继承中的同名成员冲突
如果多个基类中的成员名称各不相同,多重继承用起来非常顺畅。可要是两个基类里恰好定义了同名的成员函数,直接调用就会产生冲突。
比如 Printer 和 Scanner 里都有一个 start() 函数,直接写 device.start() 时编译器根本不知道应该执行哪一个。遇到这种情况,我们需要使用作用域限定符 基类名:: 来明确指定目标基类。请看下面例子。
示例 2:解决同名成员冲突
#include <iostream>
class Printer {
public:
void start() {
std::cout << "启动打印模块\n";
}
};
class Scanner {
public:
void start() {
std::cout << "启动扫描模块\n";
}
};
class AllInOne : public Printer, public Scanner {};
int main() {
AllInOne device;
device.Printer::start(); // 调用 Printer 的 start()
device.Scanner::start(); // 调用 Scanner 的 start()
return 0;
}运行结果如下。
启动打印模块
启动扫描模块分析:
device.Printer::start(); 这句代码通过作用域限定符 Printer::,明确指定调用 Printer 基类中的 start() 函数。下一行的 device.Scanner::start(); 则指定调用 Scanner 基类中的版本。
如果不加作用域限定符,写成下面这样就会因为调用存在歧义而直接编译报错:
AllInOne device;
device.start(); // 编译报错:存在二义性,无法确定调用哪个 start()什么是菱形继承?
除了两个无关基类碰巧重名之外,多重继承中还有一种经典情况叫做 “菱形继承”。因为把这 4 个类的关系从上往下画出来:最上面是一个共同基类,中间一左一右分叉出两个子类,最下面又收拢到一个底层派生类,整体形状就像一个菱形。
假设最顶层有一个共同基类 Person(人),包含姓名成员 name。中间层的 Student(学生)和 Worker(职员)都继承自 Person,最底层的 TeachingAssistant(助教)又同时继承它们俩,形成了两条继承路径:
Person -> Student -> TeachingAssistant
Person -> Worker -> TeachingAssistant顺着这两条路径看就会发现:Student 和 Worker 各自从 Person 继承了一份 name。当 TeachingAssistant 同时继承它们俩时,就会把 Person 继承两次,导致同一个助教对象里存了两份独立的 name。我们来看一个简单例子就懂了。
示例 3:菱形继承导致数据重复
#include <iostream>
#include <string>
class Person {
public:
std::string name;
};
class Student : public Person {};
class Worker : public Person {};
class TeachingAssistant : public Student, public Worker {};
int main() {
TeachingAssistant ta;
ta.Student::name = "学生小林";
ta.Worker::name = "职员小林";
std::cout << ta.Student::name << '\n';
std::cout << ta.Worker::name << '\n';
return 0;
}运行结果如下。
学生小林
职员小林分析:
在上面代码中,助教对象 ta 内部同时包含了 Student::name 和 Worker::name 两份独立的成员变量,甚至可以分别存入不同的名字。这种结构会带来两个明显的问题:
- 直接访问会产生歧义:如果我们直接写
ta.name = "小林";,编译器会直接报错,因为它无法确定访问的是Student还是Worker路径下的name,必须加上作用域限定符才能区分。 - 数据重复且浪费内存:同一个助教在现实中明明是同一个人,对象内存里却重复保存了两份
Person成员,既浪费内存空间,又容易出现两边数据不一致的情况。
使用虚继承解决菱形继承
为了让菱形继承中最底层的派生类只保留唯一一份共同基类的成员,C++ 提供了 “虚继承(Virtual Inheritance)” 语法。
语法:
class 中间类1 : virtual public 共同基类 {
// 类成员
};
class 中间类2 : virtual public 共同基类 {
// 类成员
};说明:
使用虚继承时,我们只需要在中间类继承共同基类时加上 virtual 关键字即可。写成 virtual public Person 或 public virtual Person 效果完全相同,平时习惯把 virtual 写在前面。
可能有些小伙伴会问:“为什么 virtual 要加在中间层的 Student 和 Worker 上,而不是加在最底层的 TeachingAssistant 上呢?”
这是因为数据重复的根源在于两个中间类各自复制了一份 Person。在中间层加上 virtual,相当于提前约定好 “以后大家共享同一个 Person 基类”。等底层类同时继承它们俩时,编译器就会自动合并路径,只保留唯一一份 Person。
示例 4:使用虚继承共享基类
#include <iostream>
#include <string>
class Person {
public:
std::string name;
Person() {
std::cout << "Person 基类构造\n";
}
};
// 中间层使用 virtual 虚继承 Person
class Student : virtual public Person {};
// 中间层使用 virtual 虚继承 Person
class Worker : virtual public Person {};
class TeachingAssistant : public Student, public Worker {};
int main() {
TeachingAssistant ta;
ta.name = "小林";
std::cout << ta.name << '\n';
std::cout << ta.Student::name << '\n';
std::cout << ta.Worker::name << '\n';
return 0;
}运行结果如下。
Person 基类构造
小林
小林
小林分析:
从运行结果可以看到,给中间层的 Student 和 Worker 加上 virtual 虚继承后,创建 ta 对象时 Person 的构造函数只执行了一次,说明 ta 内部确实只保留了一份 Person 基类。
这样一来,我们不仅可以直接编写 ta.name = "小林"; 而不会产生歧义,而且不管通过 ta.name、ta.Student::name 还是 ta.Worker::name 去读取,访问到的都是同一个成员变量。
这里顺便补充一个细节:如果共同基类 Person 定义了带参构造函数,假如还靠中间的 Student 和 Worker 各自传参,两边一旦传入不同的值就会产生冲突。因此在虚继承中,必须由最底层的 TeachingAssistant 在自己的初始化列表中直接调用 Person 的构造函数来完成初始化。
另外大家要注意区分,虽然虚函数和虚继承都用到了 virtual 关键字,但两者的位置和作用完全不同。写在函数返回值前面的是虚函数,用于实现多态动态绑定。写在类继承列表里的是虚继承,专门用来解决菱形继承基类重复的问题。
常见问题
1. 虚继承能解决所有多重继承的同名冲突吗?
不能。虚继承只解决菱形继承中 “共同基类重复两份” 的问题。如果两个基类没有共同父类,比如示例 2 中的 Printer 和 Scanner 只是碰巧都有 start(),加 virtual 是无效的,仍需通过 Printer::start() 来区分。
2. 在实际开发中,推荐经常使用多重继承吗?
一般不推荐频繁使用多重继承。它会让类之间的层级关系和构造顺序变得复杂,增加代码的阅读与维护难度。只要使用单继承或对象组合就能解决问题,我们优先选择单继承或对象组合。
在实际项目中,多重继承最常见的合理用法是:同时继承多个只包含纯虚函数的接口类。因为接口类里没有普通成员变量,既能灵活组合多套接口规范,又不会引发菱形继承的数据重复问题。
