六、继承与面向对象
条款40、明智而审慎的使用多重继承
两个阵营
一旦提到多重继承,C++社群便会分成两个基本阵营:
- 如果单一继承(SI - single inheritance)是好的,那么多重继承(MI - multiple inheritance)一定更好
- 单一继承是好的,但多重继承不值得使用
本条款的目的并不是讨论两者好坏,支持or反对哪个阵营,而是来了解这两个阵营。
两个观点
多重继承 可能导致歧义
尤其是当程序可能从多个基类继承相同名称(函数、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类的呢?
注释①
为了解决这种歧义,首先要明确调用哪个基类的函数,然后 带上那个基类
mp.BorrowableItem::checkOut();
当然,此处也可以将 BorrowableItem换成ElectronicGadget,但是会报 无法调用private成员 的错误。
钻石型多重继承 问题
当所继承的基类在它们体系中又有共同的基类,会产生钻石型多重继承问题。class File { … };
class InputFile: public File { … };
class OutputFile: public File { … };
class IOFile: public InputFile, public OutputFile
{ … };
IOFile继承自InputFile与OutputFile类,但它们各自又有共同的基类File。
对于IOFile类中有多少 File类的成分呢?
- IOFile应该从每个基类中获取一份成员函数,所以,它应该有两份
- IOFile对象只该有一个文件名称,所以继承而来的函数不应该重复
C++ 对上述两种都支持,但缺省的方法是 执行复制,也就是有两份。
如果想让其是第二种方法,就需要将高级的基类(也就是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,平常使用non-virtual继承
- 如必须使用virtual base class,尽可能避免在内放置数据
多重继承 的 合理用途
一个关于人的接口类:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
如果想使用IPerson,就必须用它的 指针 或者 引用,因为 抽象类无法被实体化创建对象,为了创建一些可以被使用的对象,就需要用工厂函数将 派生自IPerson的具象类实体化
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID);
// 创建一个对象支持Iperson接口,通过成员函数处理*pp
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
...
如果这个类名为CPerson,就像具象类一样,CPerson必须提供 继承自IPerson的 pure virtual函数的实现代码:
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]
当然,可以更换界限符号,通过 valueDelimOpen 与 valueDelimClose
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和PersonInfo的关系是,PersonInfo有若干函数帮助CPerson实现,所以是 is-implemented-in-terms-of。
本例中,CPerson需要重定义valueDelimOpen与valueDelimClose,所以 单纯的复合无法满足,如果非要用复合,就要用 复合+继承 的形式(令CPerson以private形式继承PersonInfo)
如果 使用private继承
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
class DatabaseID { ... };
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
...
};
class CPerson: public IPerson, private PersonInfo {
public:
explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
// 实现必要的IPerson成员函数
virtual std::string name() const
{ return PersonInfo::theName(); }
virtual std::string birthDate() const
{ return PersonInfo::theBirthDate(); }
private:
// 重定义 继承而来的virtual界限函数
const char* valueDelimOpen() const { return ""; }
const char* valueDelimClose() const { return ""; }
};
请记住
- 多重继承比单一继承复杂。它可能导致歧义,以及对virtual继承的需要。
- virtual继承会增加大小、速度、初始化(或者 赋值)复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和 “private继承某个协助实现的class” 的两相组合。
注释
- ① C++解析重载函数调用的规则:在看到是否有函数可调用之前,C++首先确认这个函数对此调用是否是最佳匹配。找出最佳匹配才去检验科取用性。