《Effective C++》学习笔记(条款40:谨慎使用 多重继承)

最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!

一旦提到 多重继承(multiple inheritance,MI),C++社群便会分成两个基本阵营:

  • 如果 单一继承(single inheritance,SI)是好的,多重继承 一定更好
  • 单一继承 是好的,但 多重继承 不值得使用

本条款的目的是让大家了解 多重继承 的两个观点,而不是比较哪个比较好。

首先需要认清一件事:使用 多重继承 ,程序有可能从一个以上的 基类 继承相同名称(如函数,typedef等),那会导致较多的歧义。如:

class BorrowableItem {		//图书馆允许你借某些东西
public:
	void checkOut();		//离开时进行检查
	...
};
class ElectronicGadget {
private:
	bool checkOut() const;	//执行自我检测,返回是否测试成功
	...
};
class MP3Player: public BorrowableItem, public ElectronicGadget {	//多重继承
    ...						//类的定义不是我们关心的重点
};

MP3Player mp;
mp.checkOut(); 				// 歧义,此处的 checkOut 是 BorrowableItem 类的还是 ElectronicGadget 类的呢?
							//即使两个函数中只有一个可访问,因为
							//BorrowableItem 的 checkOut 是 public,而 ElectronicGadget 内的却是 private

C++解析重载函数调用的规则:在看到是否有函数可调用之前,C++首先确认这个函数对此调用是否是最佳匹配。找出最佳匹配才去检验可取用性。上述例子中的两个 checkOut 有相同的匹配程度(因此才造成歧义),没有所谓的最佳匹配。因此 ElectronicGadget::checkOut 的可访问性也就从未被编译器审查。

为了解决歧义,必须指明你要调用哪一个 基类 内的函数:

mp.BorrowableItem::checkOut();		//OK
mp.ElectronicGadget::checkOut();	//报错,该类的 checkOut 是 private

多重继承 的意思是继承一个以上的 基类,但这些 基类 并不常在继承体系中又有更高级的 基类,那会导致菱形继承:

class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile, public OutputFile { ... }

假设 File 有个成员变量 fileName ,那么 IOFile 内有多少这个名称的数据呢?

  • 从某个角度说,IOFile 从其每一个 基类 继承一份,所以其对象内应有两份 fileName 成员变量
  • 但从另一个角度说,IOFile 对象只该有一个文件名称,所以它继承自两个 基类而来的 fileName 不该重复

在C++中两种观点都可以成立,虽然默认做法是执行复制(即对象.InputFile::fileName)。如果不是你要的,你必须令那个带有此数据的类(也就是 File)成为一个 virtual base class(虚基类),那么你就得令所有直接继承自它的类采用 虚继承

class File {  ...  };
class InputFile: virtual public File {  ...  };
class OutputFile: virtual public File {  ...  };
class IOFile: public InputFile, public OutputFile {  ...  };

从正确行为的观点看,public继承 应该总是 virtual。但是正确性并不是唯一观点,为避免继承来的成员变量重复,编译器必须提供一些成本:

  • 使用 virtual 继承的那些类所产生的对象往往比使用 non-virtual 继承的兄弟们体积大
  • 访问 virtual base class 的成员变量时,比访问 non-virtual base class 的成员变量速度慢
  • 支配“virtual base class 初始化 ”的规则比 non-virtual base情况复杂且不直观

我对 virtual base class 的忠告很简单:

  • 非必要,不适用 virtual base,平常使用non-virtual继承
  • 如必须使用virtual base class,尽可能避免在内放置数据

现在我们来看一个关于“人”的接口类:

class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

使用 IPerson 就必须用它的 指针 或者 引用,因为 抽象类 无法被实例化创建对象,为了创建一些可被当作 IPerson 使用的对象,就需要用工厂函数将 派生自 IPerson的具体类实体化:

//工厂函数,返回智能指针的原因见条款18
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

DatabaseID askUserForDatabaseID();	//从使用者手上获得一个数据库ID

DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));// 创建一个对象支持Iperson接口,通过成员函数处理*pp
...

假设这个具体类名为 CPerson,那么它必须提供继承自 IPerson 的 纯虚函数 的实现代码。

假设有个数据库相关的类,名为 PersonInfo ,它提供 IPerson 所需要的实质东西:

class PersonInfo {
public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    ...
private:
    virtual const char* valueDelimOpen() const;	//原因下面有解释
    virtual const char* valueDelimClose() const;
    ...
};

PersonInfo 被设计来协助打印各种格式的数据库字段,每个字段值的起点与终点都以特殊字符串为界。默认的是方括号,比如将字符串“Ring-tailed Lemur”格式化为:

[Ring-tailed Lemur]

但由于不是所有人都喜欢用方括号,所有两个 虚函数 valueDelimOpenvalueDelimClose 允许 派生类 设定它们自己的头尾界限符号。PersonInfo 的成员函数将调用这些 虚函数,把适当的界限符号添加到它们的返回值上,如:

const char* PersonInfo::valueDelimOpen() const
{
    return "[";
}
const char* PersonInfo::valueDelimClose() const
{
    return "]";
}
const char* PersonInfo::theName() const
{
    // 保留缓冲区给返回值使用
    static char value[Max_Formatted_Field_Value_Length];
    
    std::strcpy(value, valueDelimOpen());// 写入起始符号
    
    ...    // 将value内字符串添加到此对象的name成员变量中(注意,不要超出缓冲区限制)
    
    std::strcat(value, valueDelimClose());// 写入结尾符号
    return value;
}

身为 CPerson 的设计者,这是个好消息,因为 IPerson 要求 name()birthDate() 不能返回 “带有起始符号和结尾符号的” 值。

CPersonPersonInfo 的关系是,PersonInfo 有若干函数帮助 CPerson 实现,所以是 is-implemented-in-terms-of。
本例中,CPerson 需要重定义 valueDelimOpen()valueDelimClose(),所以 单纯的 组合(见条款38) 无法满足,如果非要用组合,就要用 组合+继承(见条款39) 的形式。

此处 CPerson 以 private 形式继承 PersonInfo,但 也必须实现 接口,那需得以 public继承 才能完成。这时使用 多重继承 就合理多了:将 public 继承自某接口private 继承自某实现

class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

class DatabaseID {  ...  };

class PersonInfo {  ...  };	//上面有它的定义

class CPerson: public IPerson, private PersonInfo {		//注意:多重继承
public:
    explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
    
    virtual std::string name() const {  	// 实现必要的IPerson成员函数
        return PersonInfo::theName();  
    }
    
    virtual std::string birthDate() const {  // 实现必要的IPerson成员函数
        return PersonInfo::theBirthDate();  
    }
private:
    // 重定义 继承而来的virtual界限函数
    const char* valueDelimOpen() const {  return "";  }
    const char* valueDelimClose() const {  return "";  }
};

这个例子告诉我们,多重继承 也有它的合理用途。

Note:

  • 多重继承 比 单一继承 复杂。它可能导致歧义,以及对 虚继承 的需要
  • 虚继承 会增加大小、速度、初始化(或者赋值)复杂度等成本。如果 虚基类 不带任何数据,将是最具实用价值的情况
  • 多重继承的确有正当用途。其中一个情节涉及“public 继承某个接口类” 和 “private继承某个协助实现的类” 的两相组合

条款41:了解隐式接口和编译期多态

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值