多重继承
多重继承是一种面向对象编程的特性,它允许一个类从多个基类继承属性和方法。这种继承方式提供了两种主要好处:
- 共享接口(Shared Interface) :通过继承,可以减少重复代码,统一代码规范。这种方式通常被称为运行时多态(Run-time Polymorphism)或接口继承(Interface Inheritance)。
- 共享实现(Shared Implementation) :减少代码量,统一实现代码的规范,这种方式被称为实现继承(Implementation Inheritance)。
一个类可以同时采用这两种风格,以实现更灵活的代码复用和规范统一。
#include <iostream>
// 共享接口
class Animal {
public:
virtual void speak() = 0;
};
// 共享实现
class Obj {
public:
virtual void weight() {
std::cout << "I am 10kg." << std::endl;
}
};
class Dog : public Animal, protected Obj {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
void weight() override {
Obj::weight();
}
};
int main() {
Dog d;
d.speak();
d.weight();
}
多重继承时,如果对同一个成员函数有重复的定义,会报错。此时,可以通过类名区分。
class Animal {
public:
void speak();
};
class Cat : public Animal {
};
class Dog : public Animal, public Cat {
};
int main() {
Dog d;
d.speak(); // Error: ambiguous call to speak
d.Cat::speak(); // OK
d.Animal::speak(); // OK
}
此外,还有一种解决办法就是在类中定义这个函数。
class Animal {
public:
void speak();
};
class Cat : public Animal {
};
class Dog : public Animal, public Cat {
public:
void speak();
};
int main() {
Dog d;
d.speak(); // OK
}
重复基类
在多重继承中,一个类可以有多个直接基类。这种设计可能导致一个基类在类层次结构中出现多次。
struct Storable {
virtual string get_file() = 0;
virtual void read() = 0;
virtual void write() = 0;
virtual ~Storable() {}
};
class Transmitter : public Storable {
public:
void write() override;
// ...
};
class Receiver : public Storable {
public:
void write() override;
// ...
};
class Radio : public Transmitter, public Receiver {
public:
string get_file() override;
void read() override;
void write() override;
// ...
};
可能出现的两种情况
- 两个
Storable
子对象:一个Radio
对象包含两个Storable
子对象,一个是Transmitter
的,另一个是Receiver
的。 - 一个共享的
Storable
子对象:一个Radio
对象包含一个Storable
子对象,这个对象由Transmitter
和Receiver
共享。
默认情况下,如果没有特殊说明,每个基类都会被拷贝一份。这意味着 Radio
类将有两个 Storable
的实例。如下图所示:
在派生类 Radio
中,可以覆盖 Storable
的虚函数,并且通常这个覆盖的函数会先调用其基类的版本,然后执行派生类自己的操作。
void Radio::write() {
Transmitter::write();
Receiver::write();
// ...写入radio特定的信息...
}
这种设计允许 Radio
类在调用 write()
方法时,先执行 Transmitter
和 Receiver
的 write()
方法,然后添加 Radio
类特有的写入操作。
虚基类
为了避免 Storable
被重复使用,我们将其声明为虚基类。这样,无论 Radio
类通过多少个基类继承 Storable
,都只会有一个 Storable
实例被共享。
class Transmitter : public virtual Storable {
public:
void write() override;
// ...
};
class Receiver : public virtual Storable {
public:
void write() override;
// ...
};
class Radio : public Transmitter, public Receiver {
public:
void write() override;
// ...
};
可表示为下面图形:
同一层次体系中两个类共享数据的方法:
- 非局部作用域:将数据置于类的外部,如全局变量或命名空间变量。这种方法破坏了封装性和局部性,通常不推荐使用。
- 基类中放置数据:将数据放在基类中,这是最简单的做法。但在单继承中,这会导致所有派生类都能访问这些数据,类似于使用非局部数据,存在同样的问题。
- 指针共享数据:在某处分配对象,并将指针分别交给两个类。这种方法需要构造函数为共享对象分配内存、初始化,并提供指向共享对象的指针,这正是虚基类构造函数所做的。
虚基类提供了一个不是根的公共基类,允许在类层次中实现数据共享,避免了非局部数据的问题。如果不需要共享,可以不使用虚基类,代码会更简单。但如果需要共享,使用虚基类是更好的选择,否则需要自己构建变量来实现共享。
构造虚基类
虚基类在多重继承中用于确保基类的构造函数只被调用一次,即使在类层次结构中被多次提及。
虚基类永远被认为是其最终派生类的直接基类。
尽管多个基类可能都初始化同一个虚基类,编译器只会使用最终派生类提供的初始化器。
struct V { V(int i); }; // 虚基类V有一个接受int参数的构造函数
struct A { A(); }; // 基类A有一个默认构造函数
struct B : virtual V, virtual A {
B() : V{ 1 } { /* ... */ } // 默认构造函数,必须初始化基类V
};
class C : virtual V {
public:
C(int i) : V{ i } { /* ... */ } // 必须初始化基类V
};
class D : virtual public B, virtual public C {
// 从B和C隐式地获取虚基类V
// 从B隐式地获取虚基类A
public:
D() { /* ... */ } // 错误:C和V没有默认构造函数
D(int i) : C{ i } { /* ... */ } // 错误:V没有默认构造函数
D(int i, int j) : V{ i }, C{ j } { /* ... */ } // OK
};
在这个例子中,D
类必须为虚基类 V
提供一个初始化器,即使 V
没有被显式声明为 D
的基类。这是因为虚基类被认为是最终派生类的直接基类,所以 D
必须负责其初始化。
重复基类与虚基类
在面向对象编程中,多重继承可以用来实现接口和提供实现细节。以下是两种不同的方法来处理接口和实现的关系,以及它们在设计上的影响和示例。
-
重复接口类:在类层次体系中每次提到接口时都创建一个对象
class BB_ival_slider : public lval_slider, protected BBslider { // ... }; class Popup_ival_slider : public lval_slider { // ... }; class BB_popup_ival_slider : public Popup_ival_slider, protected BB_ival_slider { // ... };
-
虚基类:层次体系中所有提及接口的类共享一个简单的对象
class BB_ival_slider : public virtual lval_slider, protected BBslider { // ... }; class Popup_ival_slider : public virtual lval_slider { // ... }; class BB_popup_ival_slider : public virtual Popup_ival_slider, protected BB_ival_slider { // ... };
覆盖虚基类函数
在面向对象编程中,派生类可以覆盖其直接或间接虚基类的虚函数。尤其是,两个不同的类可能会覆盖虚基类的不同的虚函数。通过这种方式,几个派生类就能共同为一个虚基类表示的接口提供实现了。
class Window {
public:
virtual void set_color(Color) = 0; // 设置背景颜色
virtual void prompt() = 0; // 用户交互
};
class Window_with_border : public virtual Window {
public:
void set_color(Color) override; // 控制背景颜色
};
class Window_with_menu : public virtual Window {
public:
void prompt() override; // 控制用户交互
};
class My_window : public Window_with_menu, public Window_with_border {
public:
void prompt() override; // 改进用户交互
};
如果 My_window
类没有覆盖 prompt()
函数,而 Window_with_menu
和 Window_with_border
都覆盖了 prompt()
,则会导致错误,因为这样会造成二义性。为了避免这种情况,必须在最终派生类中提供一个覆盖函数,以确保虚函数表的一致性。
class Window {
public:
virtual void set_color(Color) = 0; // 设置背景颜色
virtual void prompt() = 0; // 用户交互
};
class Window_with_border : public virtual Window {
public:
void prompt() override; // 控制背景颜色
};
class Window_with_menu : public virtual Window {
public:
void prompt() override; // 控制背景颜色
};
class My_window1 : public Window_with_menu, public Window_with_border { // 正确:在最终派生类中提供一个覆盖函数
public:
void prompt() override; // 控制背景颜色
};
class My_window2 : public Window_with_menu, public Window_with_border { // 报错:prompt 二义性
};
为虚基类提供部分实现(但非全部实现)的类称为混入类(mixin) ,如 Window_with_border
和 Window_with_menu
。