📄 effective c++ 2e item43.htm
字号:
<P>D *pd = new
D;<BR>pd->mf();
// A::mf或者C::mf?</P>
<P>该为D的对象调用哪个mf呢,是直接从C继承的还是间接(通过B)从A继承的那个呢?答案取决于B和C如何从A继承。具体来说,如果A是B或C的非虚基类,调用具有二义性;但如果A是B和C的虚基类,就可以说C中mf的重定义优先度高于最初A中的定义,因而通过pd对mf的调用将(无二义地)解析为C::mf。如果你坐下来仔细想想,这正是你想要的行为;但需要坐下仔细想想才能弄懂,也确实是一种痛苦。</P>
<P>也许至此你会承认MI确实会导致复杂化。也许你认识到每个人其实都不想使用它。也许你准备建议国际C++标准委员会将多继承从语言中去掉;或者至少你想向你的老板建议,全公司的程序员都禁止使用它。</P>
<P>也许你太性急了。</P>
<P>请记住,C++的设计者并没有想让多继承难以使用;恰恰是,想让一切都能以更合理的方式协调工作,这本身会带来某些复杂性。上面的讨论中你会注意到,这些复杂性很多是由于使用虚基类引起的。如果能避免使用虚基类
---- 即,如果能避免产生那种致命的钻石形状继承图 ---- 事情就好处理多了。</P>
<P>例如,条款34中讲到,协议类(Protocol
class)的存在仅仅是为派生类制定接口;它没有数据成员,没有构造函数,有一个虚析构函数(参见条款14),有一组用来指定接口的纯虚函数。一个Person协议类看起来象下面这样:</P>
<P>class Person {<BR>public:<BR> virtual ~Person();</P>
<P> virtual string name() const = 0;<BR> virtual string birthDate()
const = 0;<BR> virtual string address() const = 0;<BR> virtual
string nationality() const = 0;<BR>};</P>
<P>这个类的用户在编程时必须使用Person的指针或引用,因为抽象类不能被实例化。</P>
<P>为了创建 "可以作为Person对象而使用" 的对象,Person的用户使用工厂函数(factory
function,参见条款34)来实例化具体的子类:</P>
<P>// 工厂函数,从一个唯一的数据库ID<BR>// 创建一个Person对象<BR>Person * makePerson(DatabaseID
personIdentifier);</P>
<P>DatabaseID askUserForDatabaseID();</P>
<P><BR>DatabaseID pid = askUserForDatabaseID();</P>
<P>Person *pp = makePerson(pid); //
创建支持Person<BR>
// 接口的对象</P>
<P>...
//
通过Person的成员函数<BR>
// 操作*pp</P>
<P>delete
pp;
// 删除不再需要的对象</P>
<P>这就带来一个问题:makePerson返回的指针所指向的对象如何创建呢?显然,必须从Person派生出某种具体类,使得makePerson可以对其进行实例化。</P>
<P>假设这个类被称为MyPerson。作为一个具体类,MyPerson必须实现从Person继承而来的纯虚函数。这可以从零做起,但如果已经存在一些组件可以完成大多数或全部所需的工作,那么从软件工程的角度来说,能利用这些组件将再好不过。例如,假设已经有一个和数据库有关的旧类PersonInfo,它提供的功能正是MyPerson所需要的:</P>
<P>class PersonInfo {<BR>public:<BR> PersonInfo(DatabaseID pid);<BR>
virtual ~PersonInfo();</P>
<P> virtual const char * theName() const;<BR> virtual const char *
theBirthDate() const;<BR> virtual const char * theAddress()
const;<BR> virtual const char * theNationality() const;</P>
<P> virtual const char * valueDelimOpen()
const; // 看下文<BR> virtual const char *
valueDelimClose() const; </P>
<P> ...</P>
<P>};</P>
<P>可以断定这是一个很旧的类,因为它的成员函数返回的是const
char*而不是string对象。但是,如果鞋合脚,为什么不穿呢?这个类的成员函数名暗示,这双鞋穿上去会很舒服。</P>
<P>随之你会发现,当初设计PersonInfo是用来方便地以各种不同格式打印数据库字段,每个字段值的开头和结尾用特殊字符串分开。默认情况下,字段值的起始分隔符和结束分隔符为括号,所以字段值
"Ring-tailed Lemur" 将会这样被格式化:</P>
<P>[Ring-tailed Lemur]</P>
<P>因为括号不是所有PersonInfo的用户都想要的,虚函数valueDelimOpen和valueDelimClose允许派生类指定它们自己的起始分隔符和结束分隔符。PersonInfo类的theName,theBirthDate,theAddress以及theNationality的实现将调用这两个虚函数,在它们的返回值中添加适当的分隔符。拿PersonInfo::name作为例子,代码看起来象这样:</P>
<P>const char * PersonInfo::valueDelimOpen() const<BR>{<BR> return
"[";
// 默认起始分隔符<BR>}</P>
<P>const char * PersonInfo::valueDelimClose() const<BR>{<BR> return
"]";
// 默认结束分隔符<BR>}</P>
<P>const char * PersonInfo::theName() const<BR>{<BR> //
为返回值保留缓冲区。因为是静态<BR> // 类型,它被自动初始化为全零。<BR> static char
value[MAX_FORMATTED_FIELD_VALUE_LENGTH];</P>
<P> // 写起始分隔符<BR> strcpy(value, valueDelimOpen());</P>
<P> 将对象的名字字段值添加到字符串中</P>
<P> // 写结束分隔符<BR> strcat(value, valueDelimClose());</P>
<P> return value;<BR>}</P>
<P>有些人会挑剔PersonInfo::theName的设计(特别是使用了固定大小的静态缓冲区 ----
参见条款23),但请将你的挑剔放在一边,关注这一点:首先,theName调用valueDelimOpen,生成它将要返回的字符串的起始分隔符;然后,生成名字值本身;最后,调用valueDelimClose。因为valueDelimOpen和valueDelimClose是虚函数,theName返回的结果既依赖于PersonInfo,也依赖于从PersonInfo派生的类。</P>
<P>作为MyPerson的实现者,这是条好消息,因为在研读Person文档的细则时你发现,name及其相关函数需要返回的是不带修饰的值,即,不允许带分隔符。也就是说,如果一个人来自Madagascar,调用这个人的nationality函数将返回"Madagascar",而不是
"[Madagascar]"。</P>
<P>MyPerson和PersonInfo之间的关系是,PersonInfo刚好有些函数使得MyPerson易于实现。仅次而已。没看到有 "是一个" 或
"有一个" 的关系。它们的关系是
"用...来实现",而且我们知道,这可以用两种方式来表示:通过分层(见条款40)和通过私有继承(见条款42)。条款42指出,分层一般来说是更好的方法,但在有虚函数要被重新定义的情况下,需要使用私有继承。现在的情况是,MyPerson需要重新定义valueDelimOpen和valueDelimClose,所以不能用分层,而必须用私有继承:MyPerson必须从PersonInfo私有继承。</P>
<P>但MyPerson还必须实现Person接口,因而需要公有继承。这导致了多继承一个很合理的应用:将接口的公有继承和实现的私有继承结合起来:</P>
<P>class Person
{
//
这个类指定了<BR>public:
// 需要被实现<BR> virtual
~Person();
// 的接口</P>
<P> virtual string name() const = 0;<BR> virtual string birthDate()
const = 0;<BR> virtual string address() const = 0;<BR> virtual
string nationality() const = 0;<BR>};</P>
<P>class DatabaseID { ...
}; //
被后面的代码使用;<BR>
// 细节不重要</P>
<P>class PersonInfo
{
//
这个类有些有用<BR>public:
// 的函数,可以用来<BR> PersonInfo(DatabaseID
pid); // 实现Person接口<BR>
virtual ~PersonInfo();</P>
<P> virtual const char * theName() const;<BR> virtual const char *
theBirthDate() const;<BR> virtual const char * theAddress()
const;<BR> virtual const char * theNationality() const;</P>
<P> virtual const char * valueDelimOpen() const;<BR> virtual const
char * valueDelimClose() const;</P>
<P> ...</P>
<P>};</P>
<P><BR>class MyPerson: public Person,
//
注意,使用了<BR>
private PersonInfo { // 多继承<BR>public:<BR> MyPerson(DatabaseID pid):
PersonInfo(pid) {}</P>
<P> // 继承来的虚分隔符函数的重新定义<BR> const char * valueDelimOpen() const {
return ""; }<BR> const char * valueDelimClose() const { return ""; }</P>
<P> // 所需的Person成员函数的实现<BR> string name() const<BR> { return
PersonInfo::theName(); }</P>
<P> string birthDate() const<BR> { return
PersonInfo::theBirthDate(); }</P>
<P> string address() const<BR> { return PersonInfo::theAddress();
}</P>
<P> string nationality() const<BR> { return
PersonInfo::theNationality(); }<BR>};</P>
<P>用图形表示,看起来象下面这样:</P>
<P>
Person
PersonInfo<BR>
\
/<BR>
\
/<BR>
\/<BR>
MyPerson</P>
<P>这种例子证明,MI会既有用又易于理解,尽管可怕的钻石形状继承图不会明显消失。</P>
<P>然而,必须当心诱惑。有时你会掉进这样的陷阱中:对某个需要改动的继承层次结构来说,本来用一个更基本的重新设计可以更好,但你却为了追求速度而去使用MI。例如,假设为可以活动的卡通角色设计一个类层次结构。至少从概念上来说,让各种角色能跳舞唱歌将很有意义,但每一种角色执行这些动作时方式都不一样。另外,跳舞唱歌的缺省行为是什么也不做。</P>
<P>所有这些用C++来表示就象这样:</P>
<P>class CartoonCharacter {<BR>public:<BR> virtual void dance()
{}<BR> virtual void sing() {}<BR>};</P>
<P>虚函数自然地体现了这样的约束:唱歌跳舞对所有CartoonCharacter对象都有意义。什么也不做的缺省行为通过类中那些函数的空定义来表示(参见条款36)。假设有一个特殊类型的卡通角色是蚱蜢,它以自己特殊的方式跳舞唱歌:</P>
<P>class Grasshopper: public CartoonCharacter {<BR>public:<BR> virtual
void dance(); // 定义在别的什么地方<BR> virtual void
sing(); // 定义在别的什么地方<BR>};</P>
<P>现在假设,在实现了Grasshopper类后,你又想为蟋蟀增加一个类:</P>
<P>class Cricket: public CartoonCharacter {<BR>public:<BR> virtual void
dance();<BR> virtual void sing();<BR>};</P>
<P>当坐下来实现Cricket类时,你意识到,为Grasshopper类所写的很多代码可以重复使用。但这需要费点神,因为要到各处去找出蚱蜢和蟋蟀唱歌跳舞的不同之处。你猛然间想出了一个代码复用的好办法:你准备用Grasshopper类来实现Cricket类,你还准备使用虚函数以使Cricket类可以定制Grasshopper的行为。</P>
<P>你立即认识到这两个要求 ---- "用...来实现" 的关系,以及重新定义虚函数的能力 ----
意味着Cricket必须从Grasshopper私有继承,但蟋蟀当然还是一个卡通角色,所以你通过同时从Grasshopper和CartoonCharacter继承来重新定义Cricket:</P>
<P>class Cricket: public
CartoonCharacter,<BR>
private Grasshopper {<BR>public:<BR> virtual void dance();<BR>
virtual void sing();<BR>};</P>
<P>然后准备对Grasshopper类做必要的修改。特别是,需要声明一些新的虚函数让Cricket重新定义:</P>
<P>class Grasshopper: public CartoonCharacter {<BR>public:<BR> virtual
void dance();<BR> virtual void sing();</P>
<P>protected:<BR> virtual void danceCustomization1();<BR> virtual
void danceCustomization2();</P>
<P> virtual void singCustomization();<BR>};</P>
<P>蚱蜢跳舞现在被定义成象这样:</P>
<P>void Grasshopper::dance()<BR>{<BR> 执行共同的跳舞动作;</P>
<P> danceCustomization1();</P>
<P> 执行更多共同的跳舞动作;</P>
<P> danceCustomization2();</P>
<P> 执行最后共同的跳舞动作;<BR>}</P>
<P>蚱蜢唱歌的设计与此类似。</P>
<P>很明显,Cricket类必须修改一下,因为它必须重新定义新的虚函数:</P>
<P>class Cricket:public CartoonCharacter,<BR>
private Grasshopper {<BR>public:<BR> virtual void dance() {
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -