《Effective C++》学习笔记——条款34

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************




六、继承与面向对象设计
six、Inheritance and Object-Oriented Design




条款 34:区分接口继承和实现继承
Rule 34:Differentiate between inheritance of interface and inheritance of implementation



首先看看有两部分组成的 public继承:

· 函数接口继承(function interfaces)

· 函数实现继承(function implementations)

它们之前的差别,就像之前的 函数声明 和 函数定义 之间的差异。


当我们设计一个 类 的时候,有时候会希望 派生类 只继承成员函数的接口(就是声明),有时候又希望派生类同时继承函数的接口和实现,但又能控制是否可以 覆写(override)所继承的实现。


1.Shape和它的继承者们

class Shape  {
public:
  virtual void draw() const = 0;
  virtual void error( const std::string& msg);
  int objectID() const;
  ...
};
class Rectangle : public Shape  {  ...  };
class Ellipse : public Shape  {  ...  };

上面例子是关于 几何形状的,Shape是一个抽象类(因为pure virtual函数 draw),

Rectangle 和 Ellipse是Shape的派生类,它们都是 public 继承~


Shape类,定义了三个函数

· draw函数,作用就是在某个地方画出当前对象,pure virtual函数

· error函数,当需要报错时候调用,impure virtual函数

· objectID函数,返回当前对象的标志,nont-virtual函数





2.Shape所拥有的三个遗产

> virtual void draw() const = 0;

pure virtual函数有两个特性:必须被任何继承它们的具象类重新声明,而且在抽象类中通常没定义。

这两个特性,使得 pure virtual函数可以做到——让派生类只继承函数接口

draw的作用:每个Shape都应该可以被画出来,但是基类没办法具体提供一个绘画的版本(毕竟画个圆和画个矩形差别还是很大的),所以每个派生类都应该提供一个draw函数,但基类不管你如何实现。

但如何调用呢?唯一途径就是 调用时,明确指出它的class的名称:

Shape* ps = new Shape;    // error!Shape是抽象类,不能创建实体
Shape* ps1 = new Rectangle;    // 没问题
ps1->draw();    // 调用Rectangle的draw函数
Shape* ps2 = new Ellipse;    // 没问题
ps2->draw();    // 调用Ellipse的draw函数

// 调用 Shape的draw函数
ps1->Shape::draw();
ps2->Shape::draw();

> virtual void error(const std::string& msg);

impure virtual函数可以做到——让派生类继承函数接口,但它会提供一份实现代码,派生类也可以覆写(override)它。

error作用:告诉所有 继承Shape类的派生类,你必须支持一个 error函数,可以自己重写一个,也可以用Shape基类提供的缺省版本。



☆ But 允许 impure virtual函数同时指定函数声明与函数缺省行为,会有些危险,具体可以先看下面这个例子

例:XYZ航空公司设计的飞机继承体系,有A型与B型两种飞机,两者都以相同方式飞行

class Airport  {  ...  };    // 机场
class Airplane  {
public:
  virtual void fly(const Airport& destination);
  ...
};
void Airplane::fly(const Airport& destination)
{
  // 缺省代码,将飞机飞至指定目的地
}

class ModelA : public Airplane  {  ...  };
class ModelB : public Airplane  {  ...  };

这是一份典型的面向对象设计,两个class共享一份相同的性质(fly),共同的性质放到基类中,然后被这两个不同派生类继承。

这样可以避免代码重复,让代码清晰,减少维护成本等~~

目前为止,没问题。


现在第一次维护,XYZ公司决定用一种新式飞机来开辟一条新的航线,但因为项目时间太紧,只来得及添加一个class,而忘记覆写这个航线(fly函数)。

class ModelC : public Airplane  {
  ...
};

这样,显然会出现问题——C型飞机航线是不同于A与B的,我们如果让C型飞机 fly,它会走A与B的航线(缺省行为)。

问题不在于Airplane::fly有缺省行为,而是在于C型飞机在被调用fly时,并没有想调用缺省行为,却被迫调用。

所以,我们来解决这个问题。


第二次维护,我们让派生类决定,它是否要用这个函数的缺省行为

class Airplane  {
public:
  virtual void fly(const Airport& destination) = 0;
  ...
protected:
  void defaultFly(const Airport& destination);
};
void defaultFly(const Airport& destination)
{
  // 缺省行为,将飞机飞向指定目的地
}

这时候,Airplane::fly函数已经是 pure virtual函数了,只继承了接口,但缺省行为也在基类中,需要派生类决定是否去调用它(通过inline调用)

class ModelA : public Airplane  {
public:
  virtual void fly(const Airport& destination)
  {  defaultFly(destination);  }
  ...
};
class ModelB: public Airplane  {
public:
  virtual void fly(const Airport& destination)
  {  defaultFly(destination);  }
  ...
};

这样当定义了C型飞机后,你就不得不让C型飞机提供一份 fly函数版本,给出一份航线。

如果让你给航线了,你还去专门走A、B型飞机的航线,那我也无奈了....


另外,再有一点,有人反对用不同的函数分别提供接口和缺省实现,她们认为,你接口和实现应该用同一个名字,实现起来也不麻烦,就是利用“pure virtual”函数,必须在派生类中重新声明,但它们也可以有自己的实现   这个特性

class Airplane  {
public:
  virtual void fly(const Airport& destination) = 0;
  ...
};
void Airplane::fly(const Airport& destination)  // pure virtual的函数实现
{
  // 缺省行为,将飞机飞向指定目的地
}

class ModelA : public Airplane  {
public:
  virtual void fly(const Airport& destination)
  {  Airplane::fly(destination);  }
  ...
};
class ModelB: public Airplane  {
public:
  virtual void fly(const Airport& destination)
  {  Airplane::fly(destination);   }
  ...
};
class ModelC: public Airplane  {
public:
  virtual void fly(const Airport& destination);
  ...
};
void ModelC::fly(const Airport& destination)
{
  // 指定C型飞机的航线
}

这个几乎和前面设计一模一样,只不过用 pure virtual函数fly替换了之前的独立函数 defaultFly。

但这样做,就失去了让两个函数有不同的保护级别的机会,后者 缺省实现是 public,前者 缺省实现是protected。



> int objectID() const;

non-virtual函数可以做到——让派生类继承函数的接口及一份强制性实现(就是不能再派生类被覆写)

objectID作用:每个Shape对象都有一个用来识别对象的函数,这个识别码总会用同样的方法去产生,该方法由基类的函数(Shape::objectID)决定,派生类不能去改变它。


我们可以看到,将函数声明为不同形式——pure virtual、impure virtual、non-virtual,

它可以实现不同的目的——让派生类 只继承接口、继承接口和一份缺省的实现、继承接口和一份强制的实现。




3. 继承者们面对遗产的要注意两个原则

① 我们要避免将所有函数都声明为 non-virtual

因为这样会让 派生类 没有足够空间去展现它们的特点,尤其是non-virtual的析构函数。

如果不想让这个类成为 基类(base class),是否就可以都设计成non-virtual函数呢?

当然不!

实际上,这如果不是对 virtual 与 non-virtual 之间差异不了解,就是太担心virtual函数的效率成本。

但 80-20法则 告诉我们,平均来说函数调用中可以有80%是virtual而不冲击程序的大体效率,因为一个典型程序的80%执行时间花费在20%的代码上;所以在担心 virtual函数 效率成本之前,应该先去管管那20%的代码。

② 同时要避免所有的函数都声明为 virtual

有时候,这样是没有错误的——比如,前面介绍 interface class(接口类)的时候< 详见 条款31 >

但,某些函数就不该在派生类被修改,就比如 Shape生成ID,如果每个同样的Shape,一个矩形,一个椭圆形,它们ID生成方法不同,就可能导致ID号相同,这显然是不正确的。




4.请记住

> 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口

> pure virtual函数只具体指定接口继承

> impure virtual函数具体指定接口继承与一份缺省实现继承

> non-virtual函数具体指定接口继承以及一份强制性实现继承





***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值