最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
一旦提到 多重继承(multiple inheritance,MI),C++社群便会分成两个基本阵营:
本条款的目的是让大家了解 多重继承 的两个观点,而不是比较哪个比较好。
首先需要认清一件事:使用 多重继承 ,程序有可能从一个以上的 基类 继承相同名称(如函数,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 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]
但由于不是所有人都喜欢用方括号,所有两个 虚函数 valueDelimOpen
和 valueDelimClose
允许 派生类 设定它们自己的头尾界限符号。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()
不能返回 “带有起始符号和结尾符号的” 值。
CPerson
和 PersonInfo
的关系是,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:了解隐式接口和编译期多态