文章目录
6.继承与面向对象设计
条款32:确定你的public继承塑膜出is-a关系
“public继承”意味着 is-a,所谓 is-a,就是指适用于基类身上的每一件事情一定也适用于继承类身上,因为我们可以认为每一个派生类对象也都是一个基类对象。
条款33:避免遮掩继承而来的名称
之前我们了解过 C++ 名称查找法则,这在继承体系中也是类似的,当我们在派生类中使用到一个名字时,编译器会优先查找派生类覆盖的作用域,如果没找到,再去查找基类的作用域,最后再查找全局作用域。
class Base {
public:
void mf();
void mf(double);
};
class Derived : public Base {
public:
void mf();
};
Dreived d;
int x;
d.mf(); //调用Derived::mf1
d.mf(x); //错误,因为Derived::mf1遮掩了Base::mf1
这样会导致派生类无法使用来自基类的重载函数,因为派生类中的名称mf掩盖了来自基类的名称mf。
对于名称掩盖问题的一种方法是使用using关键字:
class Derived : public Base {
public:
using Base::mf; //Base class内名为mf的所有东西,在Derived作用域内都可见
void mf1();
};
Dreived d;
int x;
d.mf(); //调用Derived::mf1
d.mf(x); //正确,调用了Base::mf1
若有时我们不想要一个函数的全部版本,只想要单一版本(特别是在private继承时),可以考虑使用转发函数(forwarding function):
class Base {
public:
virtual void mf();
virtual void mf(double);
};
class Derived : public Base {
public:
virtual void mf() {
Base::mf();
}
};
- derived classes内的名称会遮掩base classes内的名城。在public继承下从来没有人希望如此
- 为了让被遮掩的名称再见天日, 可使用using声明式或转交函数
条款34:区分接口继承和实现继承
Shape是个抽象class,他的pure virtual 函数draw 使他成为一个抽象class。所以客户不能创建Shape class 的实体,只能创建其derived calsses的实体。
class Shape{
public:
virtual void draw() = 0;
virtual void error(const string& msg);
int objectID() const;
}
class Rectangle:public Shape{...};
class Ellipse:public Shape{...};
pure virtual函数有两个最突出的特性: 它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。
声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
Shape::draw的声明式是对derived classes设计者说“你必须提供一个draw函数,但我不干涉你怎么实现”。
Shape* ps = new Shpae; //错误!Shape是抽象的
Shaoe* ps1 = new Rectangle; //正确
ps1->draw(); //调用 Rectangle::draw
ps1->Shape::drew(); // 调用Shape::draw
impure virtual函数的突出特征是: derived classes继承其函数接口,但impure virtual 函数会提供一份实现的代码,derived classes可以覆写它。
声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
Shape::error声明式是对derived classes设计者说“你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本”。
声明non-virtual函数的目的是并不打算让derived classes中有不同的行为。令derived classes继承函数的接口及一份强制性实现。
条款35:考虑virtual函数以外的其他选择
条款36:绝不重新定义继承而来的non-virtual函数
非虚函数和虚函数具有本质上的不同:非虚函数执行的是静态绑定(statically bound,又称前期绑定,early binding),由对象类型本身(称之静态类型)决定要调用的函数;而虚函数执行的是动态绑定(dynamically bound,又称后期绑定,late binding),决定因素不在对象本身,而在于“指向该对象之指针”当初的声明类型(称之动态类型)。
l’;
k
前面我们已经说过,public继承意味着 is-a 关系,而在基类中声明一个非虚函数将会为该类建立起一种不变性(invariant),凌驾其特异性(specialization)。而若在派生类中重新定义该非虚函数,则会使人开始质疑是否该使用public继承的形式;如果必须使用,则又打破了基类“不变性凌驾特异性”的性质,就此产生了设计上的矛盾。
综上所述,在任何情况下都不该重新定义一个继承而来的非虚函数。
条款37:绝不重新定义继承而来的缺省参数值
虚函数是动态绑定而来,意思是调用一个虚函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。但与之不同的是,缺省参数值却是静态绑定,意思是你可能会在“调用一个定义于派生类的虚函数”的同时,却使用基类为它所指定的缺省参数值。考虑以下例子:
Shape* pr = new Rectangle;
Shape* pc = new Circle;
pr->Draw(Shape::ShapeColor::Green); // 调用 Rectangle::Draw(Shape::Green)
pr->Draw(); // 调用 Rectangle::Draw(Shape::Red)
pc->Draw();
条款38:通过复合塑模出 has-a 或“根据某物实现出”
所谓复合(composition),指的是某种类型的对象内含它种类型的对象。复合通常意味着 has-a 或根据某物实现出(is-implemented-in-terms-of) 的关系,当复合发生于应用域(application domain)内的对象之间,表现出 has-a 的关系;当它发生于实现域(implementation domain)内则是表现出“根据某物实现出”的关系。
下面是一个 has-a 关系的例子
class Address { ... };
class PhoneNumber { ... };
class Person {
public:
...
private:
std::string name; // 合成成分物(composed object)
Address address; // 同上
PhoneNumber voiceNumber; // 同上
PhoneNumber faxNumber; // 同上
};
下面是一个“根据某物实现出”关系的例子:
// 将 list 应用于 Set
template<class T>
class Set {
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep; // 用来表述 Set 的数据
};
条款39:明智而谨慎的使用private继承
private继承的特点:
如果类之间是private继承关系,那么编译器不会自动将一个派生类对象转换为一个基类对象。
由private继承来的所有成员,在派生类中都会变为private属性,换句话说,private继承只继承实现,不继承接口。
private继承的意义是“根据某物实现出”,如果你读过条款 38,就会发现private继承和复合具有相同的意义,事实上也确实如此,绝大部分private继承的使用场合都可以被“public继承+复合”完美解决:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void OnTick() const;
...
};
class Widget : private Timer {
private:
virtual void OnTick() const;
...
};
替换为
class Widget {
private:
class WidgetTimer : public Timer {
public:
virtual void OnTick() const;
...
};
WidgetTimer timer;
...
};
条款 40:明智而审慎地使用多重继承
多重继承是一个可能会造成很多歧义和误解的设计,因此反对它的声音此起彼伏,下面我们来接触几个使用多重继承的场景。
最先需要认清的一件事是,程序有可能从一个以上的基类继承相同名称,那会导致较多的歧义机会:
class BorrowableItem {
public:
void CheckOut();
...
};
class ElectronicGadget {
public:
void CheckOut() const;
...
};
class MP3Player : public BorrowableItem, public ElectronicGadget {
...
};
MP3Player mp;
mp.CheckOut(); // MP3Player::CheckOut 不明确!
mp.BorrowableItem::CheckOut(); // 使用 BorrowableItem::CheckOut
在使用多重继承时,我们可能会遇到要命的“钻石型继承(菱形继承)”:
class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
这时候必须面对这样一个问题:是否打算让基类内的成员变量经由每一条路径被复制?如果不想要这样,应当使用虚继承,指出其愿意共享基类:
class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... };
然而由于虚继承会在派生类中额外存储信息来确认成员来自于哪个基类,虚继承通常会付出更多空间和速度的代价,并且由于虚基类的初始化责任是由继承体系中最底层的派生类负责,就导致了虚基类必须认知其虚基类并且承担虚基类的初始化责任。因此我们应当遵循以下两个建议:
非必要不使用虚继承。
如果必须使用虚继承,尽可能避免在虚基类中放置数据。
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要
- virtual继承会增加大小、速度、初始化复杂度等等成本。如果virtual base classes不带任何数据,将是最具有实用价值的情况。
- 多重继承可用于结合public继承和private继承,public继承用于提供接口,private继承用于提供实现。