首 页 | 新 闻 | Symbian | Android| Windows Mobile | J2ME | 下载中心 | 游戏策划招聘与求职 | 购书指南 | 视频教程
您现在的位置: 开发视界 >> Symbian >> 语言基础 >> 正文
高效C++编程—面向对象设计(中)
作者:m整理    文章来源:bupt.org    更新时间:2006-2-23 14:04:01
条款39: 避免 "向下转换" 继承层次

在当今喧嚣的经济时代,关注一下我们的金融机构是个不错的主意。所以,看看下面这
个有关银行帐户的协议类(Protocol class )(参见条款34):

class Person { ... };

class BankAccount {
public:
  BankAccount(const Person *primaryOwner,
              const Person *jointOwner);
  virtual ~BankAccount();

  virtual void makeDeposit(double amount) = 0;
  virtual void makeWithdrawal(double amount) = 0;

  virtual double balance() const = 0;

  ...

};

很多银行现在提供了多种令人眼花缭乱的帐户类型,但为简化起见,我们假设只有一种
银行帐户,称为存款帐户:

class SavingsAccount: public BankAccount {
public:
  SavingsAccount(const Person *primaryOwner,
                 const Person *jointOwner);
  ~SavingsAccount();

  void creditInterest();                // 给帐户增加利息

  ...

};

这远远称不上是一个真正的存款帐户,但还是那句话,现在什么年代?至少,它满足我
们现在的需要。

银行想为它所有的帐户维持一个列表,这可能是通过标准库(参见条款49)中的list类
模板实现的。假设列表被叫做allAccounts:

list<BankAccount*> allAccounts;         // 银行中所有帐户

和所有的标准容器一样,list存储的是对象的拷贝,所以,为避免每个BankAccount存
储多个拷贝,银行决定让allAccounts保存BankAccount的指针,而不是BankAccount本
身。

假设现在准备写一段代码来遍历所有的帐户,为每个帐户计算利息。你会这么写:

// 不能通过编译的循环(如果你以前从没
// 见过使用 "迭代子" 的代码,参见下文)
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  (*p)->creditInterest();      // 错误!

}

但是,编译器很快就会让你认识到:allAccounts包含的指针指向的是BankAccount对象
,而非SavingsAccount对象,所以每次循环,p指向的是一个BankAccount。这使得对credi
tInterest
的调用无效,因为creditInterest只是为SavingsAccount对象声明的,而不是BankAccount


如果"list<BankAccount*>::iterator p = allAccounts.begin()" 在你看来更象电话
线中的噪音,而不是C++,那很显然,你以前无缘见识过C++标准库中的容器类模板。标
准库中的这一部分通常被称为标准模板库(STL),你可以在条款49和M35初窥其概貌。
但现在你只用知道,变量p工作起来就象一个指针,它将allAccounts中的元素从头到尾
循环一遍。也就是说,p工作起来就好象它的类型是BankAccount**而列表中的元素都存
储在一个数组中。

上面的循环不能通过编译很令人泄气。的确,allAccounts是被定义为保存BankAccount
*,但要知道,上面的循环中它事实上保存的是SavingsAccount*,因为SavingsAccount
是仅有的可以被实例话的类。愚蠢的编译器!对我们来说这么显然的事情它竟然笨得一
无所知。所以你决定告诉它:allAccounts真的包含的是SavingsAccount*:

// 可以通过编译的循环,但很糟糕
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  static_cast<SavingsAccount*>(*p)->creditInterest();

}

一切问题迎刃而解!解决得很清晰,很漂亮,很简明,所做的仅仅是一个简单的转换而
已。你知道allAccounts指针保存的是什么类型的指针,迟钝的编译器不知道,所以你
通过一个转换来告诉它,还有比这更合理的事吗?

在此,我要拿圣经的故事做比喻。转换之于C++程序员,就象苹果之于夏娃。

这种类型的转换 ---- 从一个基类指针到一个派生类指针 ---- 被称为 "向下转换",
因为它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧可以工作;但正
如下面即将看到的,它将给今后的维护人员带来恶梦。

还是回到银行的话题上来。受到存款帐户业务大获成功的激励,银行决定再推出支票帐
户业务。另外,假设支票帐户和存款帐户一样,也要负担利息:

class CheckingAccount: public BankAccount {
public:
  void creditInterest();    // 给帐户增加利息

  ...

};

不用说,allAccounts现在是一个包含存款和支票两种帐户指针的列表。于是,上面所
写的计算利息的循环转瞬间有了大麻烦。

第一个问题是,虽然新增了一个CheckingAccount,但即使不去修改循环代码,编译还
是可以继续通过。因为编译器只是简单地听信于你所告诉它们(通过static_cast)的
一切:*p指向的是SavingsAccount*。谁叫你是它的主人呢?这会给今后维护带来第一
个恶梦。维护期第二个恶梦在于,你一定想去解决这个问题,所以你会写出这样的代码


for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  if (*p 指向一个 SavingsAccount)
    static_cast<SavingsAccount*>(*p)->creditInterest();
  else
    static_cast<CheckingAccount*>(*p)->creditInterest();

}

任何时候发现自己写出 "如果对象属于类型T1,做某事;但如果属于类型T2,做另外某
事" 之类的代码,就要扇自己一个耳光。这不是C++的做法。是的,在C,Pascal,甚至
Smalltalk中,它是很合理的做法,但在C++中不是。在C++中,要使用虚函数。

记得吗?对于一个虚函数,编译器可以根据所使用对象的类型来保证正确的函数调用。
所以不要在代码中随处乱扔条件语句或开关语句;让编译器来为你效劳。如下所示:

class BankAccount { ... };      // 同上

// 一个新类,表示要支付利息的帐户
class InterestBearingAccount: public BankAccount {
public:
  virtual void creditInterest() = 0;

  ...

};

class SavingsAccount: public InterestBearingAccount {

  ...                           // 同上

};

class CheckingAccount: public InterestBearingAccount {

  ...                           // as above

};

用图形表示如下:

                         BankAccount
                                  ^
                                  |
                 InterestBearingAccount
                                 /\
                                /  \
                               /    \
     CheckingAccount   SavingsAccount

因为存款和支票账户都要支付利息,所以很自然地想到把这一共同行为转移到一个公共
的基类中。但是,如果假设不是所有的银行帐户都需要支付利息(以我的经验,这当然
是个合理的假设),就不能把它转移到BankAccount类中。所以,要为BankAccount引入
一个新的子类InterestBearingAccount,并使SavingsAccoun和CheckingAccount从它继
承。

存款和支票账户都要支付利息的事实是通过InterestBearingAccount的纯虚函数creditInt
erest
来体现的,它要在子类SavingsAccount和CheckingAccount中重新定义。

有了新的类层次结构,就可以这样来重写循环代码:

// 好一些,但还不完美
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  static_cast<InterestBearingAccount*>(*p)->creditInterest();

}

尽管这个循环还是包含一个讨厌的转换,但代码已经比过去健壮多了,因为即使又增加
InterestBearingAccount新的子类到程序中,它还是可以继续工作。

为了完全消除转换,就必须对设计做一些改变。一种方法是限制帐户列表的类型。如果
能得到一列InterestBearingAccount对象而不是BankAccount对象,那就太好了:

// 银行中所有要支付利息的帐户
list<InterestBearingAccount*> allIBAccounts;

// 可以通过编译且现在将来都可以工作的循环
for (list<InterestBearingAccount*>::iterator p =
        allIBAccounts.begin();
     p != allIBAccounts.end();
     ++p) {

  (*p)->creditInterest();

}

如果不想用上面这种 "采用更特定的列表" 的方法,那就让creditInterest操作使用于
所有的银行帐户,但对于不用支付利息的帐户来说,它只是一个空操作。这个方法可以
这样来表示:

class BankAccount {
public:
  virtual void creditInterest() {}

  ...

};

class SavingsAccount: public BankAccount { ... };
class CheckingAccount: public BankAccount { ... };
list<BankAccount*> allAccounts;
// 看啊,没有转换!
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  (*p)->creditInterest();

}

要注意的是,虚函数BankAccount::creditInterest提供一个了空的缺省实现。这可以
很方便地表示,它的行为在缺省情况下是一个空操作;但这也会给它本身带来难以预见
的问题。想知道内幕,以及如何消除这一危险,请参考条款36。还要注意的是,creditInt
erest
是一个(隐式的)内联函数,这本身没什么问题;但因为它同时又是一个虚函数,内联
指令就有可能被忽略。条款33解释了为什么。

正如上面已经看到的,"向下转换" 可以通过几种方法来消除。最好的方法是将这种转
换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数
成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的
指针类型之间没有出入。为了消除向下转换,无论费多大工夫都是值得的,因为向下转
换难看、容易导致错误,而且使得代码难于理解、升级和维护(参见条款M32)。

至此,我所说的都是事实;但,不是全部事实。有些情况下,真的不得不执行向下转换


例如,假设还是面临本条款开始的那种情况,即,allAccounts保存BankAccount指针,
creditInterest只是为SavingsAccount对象定义,要写一个循环来为每个帐户计算利息
。进一步假设,你不能改动这些类;你不能改变BankAccount,SavingsAccount或allAccou
nts
的定义。(如果它们在某个只读的库中定义,就会出现这种情况)如果是这样的话,你
就只有使用向下转换了,无论你认为这个办法有多丑陋。

尽管如此,还是有比上面那种原始转换更好的办法。这种方法称为 "安全的向下转换"
,它通过C++的dynamic_cast运算符(参见条款M2)来实现。当对一个指针使用dynamic_ca
st
时,先尝试转换,如果成功(即,指针的动态类型(见条款38)和正被转换的类型一致)
,就返回新类型的合法指针;如果dynamic_cast失败,返回空指针。

下面就是加上了 "安全向下转换" 的例子:

class BankAccount { ... };          // 和本条款开始时一样

class SavingsAccount:               // 同上
  public BankAccount { ... };

class CheckingAccount:              // 同上
  public BankAccount { ... };

list<BankAccount*> allAccounts;     // 看起来应该熟悉些了吧...

void error(const string& msg);      // 出错处理函数;
                                    // 见下文

// 嗯,至少转换很安全
for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  // 尝试将*p安全转换为SavingsAccount*;
  // psa的定义信息见下文
  if (SavingsAccount *psa =
        dynamic_cast<SavingsAccount*>(*p)) {
    psa->creditInterest();
  }

  // 尝试将它安全转换为CheckingAccount
  else if (CheckingAccount *pca =
             dynamic_cast<CheckingAccount*>(*p)) {
    pca->creditInterest();
  }

  // 未知的帐户类型
  else {
    error("Unknown account type!");
  }
}

这种方法远不够理想,但至少可以检测到转换失败,而用dynamic_cast是无法做到的。
但要注意,对所有转换都失败的情况也要检查。这正是上面代码中最后一个else语句的
用意所在。采用虚函数,就不必进行这样的检查,因为每个虚函数调用必然都会被解析
为某个函数。然而,一旦打算进行转换,这一切好处都化为乌有。例如,如果某个人在
类层次结构中增加了一种新类型的帐户,但又忘了更新上面的代码,所有对它的转换就
会失败。所以,处理这种可能发生的情况十分重要。大部分情况下,并非所有的转换都
会失败;但是,一旦允许转换,再好的程序员也会碰上麻烦。

上面if语句的条件部分,有些看上去象变量定义的东西,看到它你是不是慌张地擦了擦
眼镜?如果真这样,别担心,你没看错。这种定义变量的方法是和dynamic_cast同时增
加到C++语言中的。这一特性使得写出的代码更简洁,因为对psa或pca来说,它们只有
在被dynamic_cast成功初始化的情况下,才会真正被用到;使用新的语法,就不必在(
包含转换的)条件语句外定义这些变量。(条款32解释了为什么通常要避免多余的变量
定义)如果编译器尚不支持这种定义变量的新方法,可以按老方法来做:

for (list<BankAccount*>::iterator p = allAccounts.begin();
     p != allAccounts.end();
     ++p) {

  SavingsAccount *psa;        // 传统定义
  CheckingAccount *pca;       // 传统定义

  if (psa = dynamic_cast<SavingsAccount*>(*p)) {
    psa->creditInterest();
  }

  else if (pca = dynamic_cast<CheckingAccount*>(*p)) {
    pca->creditInterest();
  }

  else {
    error("Unknown account type!");
  }
}

当然,从处理事情的重要性来说,把psa和pca这样的变量放在哪儿定义并不十分重要。
重要之处在于:用if-then-else风格的编程来进行向下转换比用虚函数要逊色得多,应
该将这种方法保留到万不得已的情况下使用。运气好的话,你的程序世界里将永远看不
到这样悲惨荒凉的景象。



---------------------------------------------------------------------------
-----



条款40: 通过分层来体现 "有一个" 或 "用...来实现"

使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这
一过程称为 "分层"(Layering)。例如:

class Address { ... };           // 某人居住之处

class PhoneNumber { ... };

class Person {
public:
  ...

private:
  string name;                   // 下层对象
  Address address;               // 同上
  PhoneNumber voiceNumber;       // 同上
  PhoneNumber faxNumber;         // 同上
};

本例中,Person类被认为是置于string,Address和PhoneNumber类的上层,因为它包含
那些类型的数据成员。"分层" 这一术语有很多同义词,它也常被称为:构成(composition
),包含(containment)或嵌入(embedding)。

条款35解释了公有继承的含义是 "是一个"。对应地,分层的含义是 "有一个" 或 "用
...来实现"。

上面的Person类展示了 "有一个" 的关系。一个Person对象 "有一个" 名字,地址,电
话号码和传真号码。你不能说,一个人 "是一个" 名字或一个人 "是一个" 地址;你得
说,一个人 "有一个" 名字, "有一个" 地址,等等。大多数人对区分这些没什么困难
,所以混淆 "是一个" 和 "有一个" 的情况相对来说比较少见。

稍微有点麻烦的是区分 "是一个" 和 "用...来实现"。例如,假设需要一个类模板,用
来表示任意对象的集合,并且集合中没有重复元素。程序设计中,重用(Reuse)是再
好不过的一件事了,而且你也许已经读过条款49中关于C++标准库的总体介绍,那么,
你的第一反应一定是想采用标准库中的set模板。是啊,既然可以使用别人所写的东西
,为什么还要再去写一个新的模板呢?

但是,深入研究set的帮助文档后,你会发现,set的下述限制将不能满足你的程序要求
:set要求包含在它内部的元素必须是完全有序的,即,对set中的任两个元素a和b来说
,一定可以确定:要么a<b,要么b<a。对许多类型来说,这个要求很容易满足,而且,
对象间完全有序使得set可以在性能方面提供某些保证,这一点很吸引人。(参见条款
49了解标准库在性能上更多的保证)然而,你所需要的是更广泛的东西:一个类似set
的类,但对象不必完全有序;用C++标准所包装的术语来说,它们只需要所谓的 "相等
可比较性":对于同种类型的a和b对象来说,要能确定是否a==b。这种要求更简单,它
更适合于那些表示颜色这类东西的类型。总不能说红色比绿色更少或绿色比红色更少吧
?看来,对你的程序来说,还是得需要自己来写个模板。

当然,重用还是件好事。作为数据结构专家,你知道,在实现集合的众多选择中,一个
最简单的办法是采用链表。你一定猜到了什么。对,标准库中正有这么一个list模板(
用来产生链表类)!所以可以重用它。

具体来说,你决定让自己的Set模板从list继承。即,Set<T>将从list<T>继承。因为,
在你的实现中,Set对象实际上将是list对象。于是你这样声明Set模板:

// Set中错误地使用了list
template<class T>
class Set: public list<T> { ... };

至此,一切好象都很正确,但实际上错误不小。正如条款35所说明的,如果D "是一个
" B,对B成立的所有事实对D也成立。但是,list对象可以包含重复元素,所以如果3051
这个值被增加到list<int>中两次,list中将包含3051的两个拷贝。相反,Set不可以包
含重复元素,所以如果3051被增加到Set<int>中两次,Set中将只包含这个值的一个拷
贝。于是,说一个Set "是一个" list就是弥天大谎,因为如上所述,有一些在list对
象中成立的事实在Set对象中不成立。

因为这两个类的关系并非 "是一个",所以用公有继承来表示它们的关系就是一个错误
。正确的方法是让Set对象 "用list对象来实现":

// Set中使用list的正确方法
template<class T>
class Set {
public:
  bool member(const T& item) const;

  void insert(const T& item);
  void remove(const T& item);

  int cardinality() const;

private:
  list<T> rep;                       // 表示一个Set
};

Set的成员函数可以利用list以及标准库其它部分所提供的大量功能,所以,实现代码
既不难写也很易读:

template<class T>
bool Set<T>::member(const T& item) const
{ return find(rep.begin(), rep.end(), item) != rep.end(); }

template<class T>
void Set<T>::insert(const T& item)
{ if (!member(item)) rep.push_back(item); }

template<class T>
void Set<T>::remove(const T& item)
{
  list<T>::iterator it =
    find(rep.begin(), rep.end(), item);

  if (it != rep.end()) rep.erase(it);
}

template<class T>
int Set<T>::cardinality() const
{ return rep.size(); }

这些函数很简单,所以很自然地想到将它们作为内联函数;但在做最后决定前,还是回
顾一下条款33所做的讨论。(上面的代码中,find, begin, end, push_back等函数是
标准库基本框架的一部分,它们可用来对list这样的容器模板进行操作。标准库框架的
总体介绍参见条款49和M35。)

值得指出的是,Set类的接口没有做到完整并且最小(参见条款18)。从完整性上来说
,它最大的遗漏在于不能对Set中的内容进行循环,而这一功能对很多程序来说是必需
的(标准库中的所有成员都提供了这一功能,包括set)。Set的另一个缺陷是没有遵循
标准库所采用的容器类常规(见条款49和M35),从而造成使用Set时更难以利用库中其
它的部分。

Set的接口尽管有这些瑕疵,但下面这一点不能被掩盖:Set在理解它和list的关系上,
具有无可辩驳的正确性。这种关系并非 "是一个"(虽然初看会以为是),而是 "用..
.来实现",通过分层来实现这种关系是类的设计者应该感到自豪的。

顺便说一句,当通过分层使两个类产生联系时,实际上在两个类之间建立了编译时的依
赖关系。关于为什么要考虑到这一点以及如何减少这方面的麻烦,参见条款34。



---------------------------------------------------------------------------
-----



条款41: 区分继承和模板

考虑下面两个设计问题:

· 作为一位立志献身计算机科学的学生,你想设计一个类来表示对象的堆栈。这将需
要多个不同的类,因为每个堆栈中的元素必须是同类的,即,它里面包含的必须只是同
种类型的对象。例如,会有一个类来表示int的堆栈,第二个类来表示string的堆栈,
第三个类来表示string的堆栈的堆栈,等等。你也许对设计一个最小的类接口(参见条
款18)很感兴趣,所以会将对堆栈的操作限制在:创建堆栈,销毁堆栈,将对象压入堆
栈,将对象弹出堆栈,以及检查堆栈是否为空。设计中,你不会借助标准库中的类(包
括stack ---- 参见条款49),因为你渴望亲手写这些代码。重用(Reuse)是一件美事
,但当你的目标是探究事情的工作原理时,那就只有挖地三尺了。

· 作为一位爱猫的宠物迷,你想设计一个类来表示猫。这也将需要多个不同的类,因
为每个品种的猫都会有点不同。和所有对象一样,猫可以被创建和销毁,但,正如所有
猫迷所知道的,猫所做的其它事不外乎吃和睡。然而,每一种猫吃和睡都有各自惹人喜
爱的方式。

这两个问题的说明听起来很相似,但却导致完全不同的两种设计。为什么?

答案涉及到"类的行为" 和 "类所操作的对象的类型"之间的关系。对于堆栈和猫来说,
要处理的都是各种不同的类型(堆栈包含类型为T的对象,猫则为品种T),但你必须问
自己这样一个问题:类型T影响类的行为吗?如果T不影响行为,你可以使用模板。如果
T影响行为,你就需要虚函数,从而要使用继承。

下面的代码通过定义一个链表来实现Stack类,假设堆栈的对象类型为T:

class Stack {
public:
  Stack();
  ~Stack();

  void push(const T& object);
  T pop();

  bool empty() const;             // 堆栈为空?

private:
  struct StackNode {              // 链表节点
    T data;                       // 此节点数据
    StackNode *next;              // 链表中下一节点

    // StackNode构造函数,初始化两个域
    StackNode(const T& newData, StackNode *nextNode)
    : data(newData), next(nextNode) {}
  };

  StackNode *top;                 // 堆栈顶部

  Stack(const Stack& rhs);               // 防止拷贝和
  Stack& operator=(const Stack& rhs);    // 赋值(见条款27)
};

于是,Stack对象将构造如下所示的数据结构:

Stack对象 top--> data+next--> data+next--> data+next--> data+next
                        ---------------------------------------------------
---------------------------------
                                                 StackNode对象

链表本身是由StackNode对象构成的,但那只是Stack类的一个实现细节,所以StackNode
被声明为Stack的私有类型。注意StackNode有一个构造函数,用来确保它所有的域都被
正确初始化。即使你闭着眼睛都可以写出一个链表,但也不要忽视了C++的一些新特性
,如struct中的构造函数。

下面看看你对Stack成员函数的实现。和许多原型(prototype)的实现(离制作成软件
产品相差太远)一样,这里没有错误检查,因为在原型世界里,没有东西会出错。

Stack::Stack(): top(0) {}      // 顶部初始化为null

void Stack::push(const T& object)
{
  top = new StackNode(object, top);    // 新节点放在
}                                      // 链表头部

T Stack::pop()
{
  StackNode *topOfStack = top;    // 记住头节点
  top = top->next;

  T data = topOfStack->data;      // 记住节点数据
  delete topOfStack;

  return data;
}

Stack::~Stack()                   // 删除堆栈中所有对象
{
  while (top) {
    StackNode *toDie = top;       // 得到头节点指针
    top = top->next;              // 移向下一节点
    delete toDie;                 // 删除前面的头节点
  }
}

bool Stack::empty() const
{ return top == 0; }

这些代码毫无吸引人之处。实际上,唯一有趣的一点在于:即使对T一无所知,你还是
能够写出每个成员函数。(上面的代码中实际上有个假设,即,假设可以调用T的拷贝
构造函数;但正如条款45所说明的,这是一个绝对合理的假设)不管T是什么,对构造
,销毁,压栈,出栈,确定栈是否为空等操作所写的代码不会变。除了 "可以调用T的
拷贝构造函数" 这一假设外,stack的行为在任何地方都不依赖于T。这就是模板类的特
点:行为不依赖于类型。

将stack类转化成一个模板就很简单了,即使是Dilbert的老板都会写:

template<class T> class Stack {

  ...                          // 完全和上面相同

};

但是,猫呢?为什么猫不适合模板?

重读上面的说明,注意这一条:"每一种猫吃和睡都有各自惹人喜爱的方式"。这意味着
必须为每种不同的猫实现不同的行为。不可能写一个函数来处理所有的猫,所能做的只
能是制定一个函数接口,所有种类的猫都必须实现它。啊哈!衍生一个函数接口的方法
只能是去声明一个纯虚函数(参见条款36):

class Cat {
public:
  virtual ~Cat();                     // 参见条款14

  virtual void eat() = 0;             // 所有的猫吃食
  virtual void sleep() = 0;           // 所有的猫睡觉
};

Cat的子类 ---- 比如,Siamese和BritishShortHairedTabby ---- 当然得重新定义继
承而来的eat和sleep函数接口:

class Siamese: public Cat {
public:
  void eat();
  void sleep();

  ...

};

class BritishShortHairedTabby: public Cat {
public:
  void eat();
  void sleep();

  ...

};

好了,现在知道了为什么模板适合Stack类而不适合Cat类,也知道了为什么继承适合Cat
类。唯一剩下的问题是,为什么继承不适合Stack类。想知道为什么,不妨试着去声明
一个Stack层次结构的根类 ---- 所有其它的堆栈类都从这个唯一的类继承:

class Stack {      // a stack of anything
public:
  virtual void push(const ??? object) = 0;
  virtual ??? pop() = 0;

  ...

};

现在问题很明显了。该为纯虚函数push和pop声明什么类型呢?记住,每一个子类必须
重新声明继承而来的虚函数,而且参数类型和返回类型都要和基类的声明完全相同。不
幸的是,一个int堆栈只能压入和弹出int对象,而一个Cat堆栈只能压入和弹出Cat对象
。Stack类要怎样声明它的纯虚函数才能使用户既可以创建出int堆栈又可以创建出Cat
堆栈呢?冷酷而严峻的事实是,做不到。这就是为什么说继承不适合创建堆栈。

但也许你做事喜欢偷偷摸摸。或许你认为自己可以通过使用通用(void*)指针来骗过编
译器。但事实证明,现在这种情况下,通用指针也帮不上忙。因为你无法避开这一条件
:派生类虚函数的声明永远不能和它在基类中的声明相抵触。但是,通用指针可以帮助
解决另外一个不同的问题,它和模板所生成的类的效率有关。详细介绍参见条款42。

讲完了堆栈和猫,下面将本条款得到的结论总结如下:

· 当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
· 当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。

真正消化了以上两点的含义,你就可以在设计中游刃于继承或模板之间。



---------------------------------------------------------------------------
-----

相关文章:
在没有ui的程序中捕获所有的key事件
Symbian类里的extension和reserved
Symbian应用程序中如何备份和载入
播放WAV文件
Symbian程序中的观察者模式
S60平台:Bluetooth API开发伙伴指南——搜索和发布
如何在SYMBIAN60中编写DLL
C++ 基类和派生类
 

站点地图 | 加入收藏 | 联系站长 | 广告服务 |
QQ:280529124  Tel:0592-8271361 辽ICP备05021703号