明智而审慎地使用多重继承——条款40

本文探讨了C++中多重继承导致的歧义性问题,特别是在函数名冲突时。介绍了如何通过指定基类解决歧义,并讨论了虚拟继承的概念及其在解决钻石型多重继承问题中的作用。虚拟继承虽能避免数据成员的重复,但会增加对象大小和访问速度的开销。

        当多重继承(multiple inheritance;MI)进入设计景框,程序有可能从一个以上的base classes继承相同名称(如函数、typedef等等)。那会导致较多的歧义机会。例如:

class BorrowableItem {             // 图书馆允许你借某些东西
public:
	void checkOut();               // 离开时进行检查
	...
};
class ElectronicGadget {
private:
	bool checkOut() const;        // 指向自我检测,返回是否测试成功
	...
};
class MP3Player:                 // 注意这里的多重继承(某些图书馆愿意借出MP3播放器)
    public BorrowableItem, 
    public ElectronicGadget 
{
	...                          // 这里,class的定义不是我们关心重点 
};
MP3Player mp;
mp.checkOut();                   // 歧义!调用的是哪个checkOut?

        注意此例之中对checkOut的调用是歧义(模棱两可)的,即使两个函数之中只有一个可取用(BorrowableItem内的checkOut是public,ElectronicGadget内的却是private)。这与C++用来解析(resolving)重载函数的调用的规则相符:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配函数后才检验其可取用性。本例的两个checkOuts有相同的匹配程度(译注:因此才造成歧义),没有所谓最佳匹配。因此ElectronicGadget::checkOut的可取用性也就从未被编译器审查。

        为了解决这个歧义,你必须明白指出你要调用哪一个base class内的函数:

mp.BorrowableItem::checkOut();    // 哎呀,原来是这个checkOut...

        你当然也可以尝试明确调用ElectronicGadget::checkOut,但然后你会获得一个“尝试调用private成员函数”的错误。

        多重继承的意思是继承一个以上的base classes,但这些base classes并不常在继承体系中又有更高级的base classes,因为那会导致要命的“钻石型多重继承”:

        任何时候如果你有一个继承体系而其中某个base class和某个derived class之间有一条以上的相通路线(就像上述的File和IOFile之间有两条路径,分别穿越InputFile和OutputFile),你就必须面对这样一个问题:是否打算让base class内的成员变量经由每一条路径被复制?假设File class有个成员变量fileName,那么IOFile内该有多少笔这个名称的数据呢?从某个角度说,IOFile从其每一个base class继承一份,所以其对象内应该有两份fileName成员变量。但从另一个角度说,简单的逻辑告诉我们,IOFile对象只该有一个文件名称,所以它继承自两个base classes而来的fileName不该重复。

        C++其缺省做法是执行复制(也就是上一段所说的第一个做法)。如果那不是你要的,你必须令那个带有此数据的class(也就是File)成为一个virtual base class。为了这样做,你必须令所有直接继承自它的classes采用“virtual继承”:

        C++标准程序库内含一个多重继承体系,其结构就如右图那样,只不过其classes其实是class templates,名称分别是basic_ios,basic_istream,baseic_ostream和basic_iostream,而非这里的File,InputFile,OutputFile和IOFile。

        virtual继承的classes所产生的对象往往比使用non-virtual继承的体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。种种细节因编译器不同而异,但基本重点很清楚:你得为virtual继承付出代价。

        virtual继承的成本还包括其他方面。支配“virtual base classes初始化”的规则比起non-virtual bases的情况远为复杂且不直观。

        我对virtual base classes(亦相当于对virtual继承)的忠告很简单。第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base classes,尽可能避免在其中放置数据。这么一来你就不需担心这些classes身上的初始化(和赋值)所带来的诡异事情了。Java和.NET的Interfaces值得注意,它在许多方面兼容于C++的virtual base classes,而且也不允许含有任何数据。

请记住

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具有实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。
### C++ 继承多重继承与虚继承的概念及用法 #### 一、继承的基本概念 继承是面向对象编程的核心机制之一,允许一个类(子类)基于另一个类(父类或基类)定义。通过继承,子类可以获得父类的属性和方法,并可以选择性地覆盖或扩展它们。 在 C++ 中,单继承是最常见的形式,即一个类只从一个基类派生而来。然而,在更复杂的场景下,可能会涉及到 **多重继承** 和 **虚继承** 的使用。 --- #### 二、多重继承及其特点 多重继承是指一个类可以从多个基类派生而来的特性。这种设计模式使得子类能够同时拥有多个基类的功能,从而提高代码复用率。然而,多重继承也可能带来一些复杂性和潜在问题。 ##### 特点: 1. 子类会继承所有指定基类的成员变量和函数。 2. 如果不同基类中有同名成员,则可能导致命名冲突(二义性),需要显式限定访问范围[^2]。 3. 钻石继承问题是多重继承中最典型的挑战之一,指的是当两个中间层类分别继承自同一顶层基类时,底层类会出现重复继承顶层基类的情况[^4]。 示例代码展示多重继承: ```cpp class A { public: void funcA() { cout << "Function from A\n"; } }; class B { public: void funcB() { cout << "Function from B\n"; } }; // 多重继承:C 同时继承自 A 和 B class C : public A, public B {}; int main() { C obj; obj.funcA(); // 输出 Function from A obj.funcB(); // 输出 Function from B } ``` 上述例子展示了如何利用多重继承让 `C` 类获得来自 `A` 和 `B` 的功能[^1]。 --- #### 三、虚继承的作用与意义 为了应对钻石继承带来的冗余副本问题,C++ 提供了虚继承作为解决方案。通过声明某一层级上的特定继承为虚拟继承,可以确保即使该层级被多次间接引用,最终也只会创建唯一的一份实例。 ##### 关键作用: - 解决因共享祖先而导致的数据复制现象; - 减少内存消耗并增强逻辑一致性[^3]。 以下是改进后的版本,采用虚继承消除重复: ```cpp #include <iostream> using namespace std; class Base { protected: int value; public: Base(int v = 0):value(v){} virtual ~Base(){} void showValue() const {cout<<"Value is "<<value<<endl;} }; // 定义两条分支链路均指向相同的根节点 class DerviedOne:virtual public Base{ private: string name="Dervied One"; public: void displayName()const{cout<<name<<endl;} }; class DerviedTwo:virtual public Base{ private: string name="Dervied Two"; public: void displayName()const{cout<<name<<endl;} }; // 底部汇合处仅保留单一入口至公共基础部分 class FinalClass:public DerviedOne,public DerviedTwo{}; int main(){ FinalClass fc(99); fc.displayName();// 可能调用任一分支的名字显示方法 fc.showValue(); // 正确反映统一的基础数值状态 return 0; } ``` 在此案例里,无论经由哪条线路到达终点,“FinalClass”的构建过程都维持着对于“Base”组件独一无二的存在形态。 --- #### 四、适用场合对比分析 | 功能需求 | 单纯继承 | 多重继承 | 虚继承 | |----------|-----------|------------|---------| | 场景描述 | 基本的对象行为封装 | 结合多种独立能力形成新实体 | 当存在共通源头需防止歧义时应用 | | 实现难度 | 较低 | 中等 | 较高 | | 性能影响 | 少量开销 | 显著增长 | 控制合理则接近单纯水平 | 综上所述,开发者应依据实际项目状况审慎抉择最合适的策略组合。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值