目录
一、确定你的public继承塑模出is-a关系(32)
- piublic inheritance意味着“is-a”的关系。适用于base classes身上 的每一件事情一定也适用于derived classe身上,因为每一个derived classe对象也是一个base classe对象 。
- 现实或数学逻辑上是is-a关系的事务,在程序的世界并不一定是is-a的关系。考虑Penguin(企鹅) 、Bird和Square、Rectangle的例子。
- is-a的关系要保证在Base class中式public的在Derived class也是public的。
二、避免遮掩继承而来的名称(33)
- 全局变量和局部变量的遮掩
int x; //global 变量
void someFunc(){
double x; //local 变量
std::cin >> x; //读一个新值赋予local变量x
}
该例子在说明,遮掩和类型没有关系。C++的名称遮掩规则是:遮掩名称。
- 继承中的遮掩
Derived classed内的名称会遮掩Base classed内的名称。遮掩和类型无关
特别的,对于函数重载,派生类中同名的函数会遮掩基类所有重载的同名函数。。
Derived classes 遮掩Base classes中的重载函数示例:
class Base{
private :
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived:public Base{
public:
virtual void mf1();
void mf3();
void mf4();
...
};
Derived d;
int x;
...
d.mf1(); //没问题,调用Derived::mf1
d.mf1(x); //错误!因为Derivd::mf1遮掩了Base::mf1
d.mf2(); //没问题,调用Base::mf2
d.mf3(); //没问题,调用Derived::mf3
d.mf3(x); //错误!因为Derivd::mf3遮掩了Base::mf3
- 如何推翻对继承而来的名称的缺省遮掩行为?
使用using声明—— using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。
class Base{
private :
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
...
};
class Derived:public Base{
public:
using Base::mf1;//让Base class内名为mf1的所有东西在
using Base::mf3;//Derived class作用域内都可见(并且为public)
virtual void mf1();
void mf3();
void mf4();
...
};
- 转交函数
- 对于private继承,如果想继承Base class的某个函数,可以使用转交函数
- 对于不支持using声明式的旧编译器可以使用转交函数
class Base{
private :
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
...
};
class Derived:private Base{
public:
virtual void mf1()//转交函数,暗自成为inline
{
Base::mf1();
}
...
};
三、 区分接口即成和实现继承(34)
- 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。可以为pure virtual提供定义,调用它的唯一途径是“调用时指出其class名称”,这个特性用处不大。
- 声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
impure virtual函数的缺点是:当derived classes 忘记重写base class的impure virtual函数时,derived classes 会继承base class的缺省实现。从而导致未预期的行为。可通过独立的缺省实现方法+pure virtual 函数解决,如下:
class Airport{...};
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination){
//缺省行为
}
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination){
Airplane::defaultFly(destination);
}
};
通过将接口声明为pure virtual 函数,再将缺省行为独立为protected级别的non-virtual函数,可以避免derived class忘记重写base class的impure virtual 函数。
另一种接口和缺省实现的方法:
class Airport{...};
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination){
//缺省行为
}
class ModelC :public Airplane{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination){
Airplane::fly(destination);
}
- 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
non-virtual函数代表意义是不变性凌驾特异性,所以它绝不应该再derived class中被重新定义。
四、考虑virtual函数以外的其他选择(35)
4.1 Non-Virtual Interface(NVI手法)
- NVI手法:令客户通过public non-virtual 成员函数间接调用private virtual 函数。NVI手法是Template Method设计模式的一个独特表现形式。non-virtual 函数称为virtual函数的外覆器。
class Gamecharacter{
public:
int healthValue() const{. //derived class 不重新定义它,
... //做一些事前工作
int retVal = doHealthValue(); //做真正的工作
... //做一些事后工作
return retVal;
}
...
private:
virtual int doHealthValue() const {
...
}
};
- NVI手法的优点:
- 在virtual函数被调用前设定好适当场景,在调用结束之后清理场景。
- 调用前可以做:锁定互斥器、制造日志运转记录项、验证class约束条件、验证函数先决条件等等。
- 调用后可以做:互斥器解除锁定、验证函数的事后条件、再次验证class的约束条件等等。
- NVI手法允许derived classes 重新定义继承而来的private virtual函数,这赋予derived classes“如何实现机能”的控制能力,base classes 保留“函数何时被调用”的权利。
- NVI手法没有必要让virtua 函数一定得是private。当某些class继承体系要求derived class在virtual函数实现内必须调用其base class的对应兄弟,则virtual 函数必须是protected。
- 有时候virtual必须是public(如多态性质的base classes的析构函数),这种情况就不能用NVI手法了。
4.2 藉由Function Pointer 实现Strategy模式
4.2.1 Stragegy 设计模式的简单应用:
class GameCharacter;
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const{
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
- 同一类型不同实体可以有不同的健康计算函数。
- 某已知实体的健康指数计算函数可以在运行期变更。
- 使用外部函数的缺点在于不能访问base class的内部成分(非public成分)。
4.2.2 藉由tr1::function完成Strategy模式
tr1::function将4.2.1中的函数指针泛化,一切符合或者可转换为相同签名的可调用物皆可。
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
typedef std::tr1::fuction<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const{
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
short calcHealth(const GameCharacter&);//健康计算函数,注意返回值为非int
struct HealthCalculator{ //函数对象
int operator(const GameCharacter&){
...
}
};
class GameLevel{
public:
float health(const GameCharacter&) const; //成员函数,注意其非int返回类型
...
};
class EvilBadGuy : public GameCharacter{...};
class EyeCandyCharacter :public GameCharacter{...};
EviBadyGuy ebg1(calcHEalth);
EyeCandyCharacter ecc1(HealthCalculator());
GameLevel currentLEvel;
EviBadyGuy ebg2(std::tr1::bind(&GameLevel::health,currentLEvel,_1));//health需要2个参数:this 和 GameCharacter
4.2.3 古典的Strategy模式
Strategy模式的继承体系
实现代码:
class GameCharacter;
class HealthCalcFunc{
public:
...
virtual int calc(const GameCharacter& gc) const{
...
}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* hcf = &defaultHealthCalc)
: pHealthFunc(hcf)
{}
int healthValue() const{
return pHealthFunc->calc(*this);
}
private:
HealthCalcFunc& pHealthFunc;
};
古典Strategy模式的优点在于:提供一个“将机油的健康算法纳入使用”的可能性——只要为
HealthCalcFunc继承体系添加一个derived class即可。
五、绝不重新定义继承而来的non-virtual函数(36)
- 多态性质的base classes 中的析构函数应该为virtual,如果为non-virtual,那么derived class中就不应该定义non-virtual析构函数,也就是说派生类不应该有析构函数(这显然是做不到的,因为即使你不写编译器也会生成),否则违反本条款。—— 这说明derived classes的析构函数是对base classed 的重写,这也就解释了为什么通过base class析构函数声明为virtual时,derived classes的析构函数会得到执行,因为derived classes重写base classes的虚析构函数会将虚表中的base classes中的析构函数覆盖。
六、绝不重新定义继承而来的函数的缺省参数值(37)
- 问题:会导致动态类型调用的行为混乱
- 原因 动态类型调用时,会导致使用静态类型的缺省参数值
- 示例:
class Shape{
public:
enum ShapeColor{Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle : public Shape{
public:
virtual void draw(ShapeColor color = Red) const;
...
};
class Circle : public Shape{
public:
virtual void draw(ShapeColor color) const;
...
};
Shape* ps;
Shape* pr = new Rectangle;
Shape* pc = new Circle;
ps、pr、pc都被声明为pointer-to-Shape类型,所以它们都以Shape为静态类型,pr的动态类型为Rectangle,pc的动态类型为Circle;
缺省参数值是静态绑定的
pr->draw(); //调用的是Rectangle::draw(Shape::Red)!
pr的动态类型为Rectangle,所以调用的是Rectangle中的draw。但是缺省参数是静态绑定的,pr的静态类型是Shape,所以调用的是Shape::draw中的缺省参数。
- 如何解决?
使用NVI手法,将virtual函数包裹起来,通过public 函数提供缺省值。如:
class Shape{
public:
enum ShapeColor{Red, Green, Blue};
void void doDraw(ShapeColor color = Red) const
{
draw(color);
}
private:
virtual void draw(ShapeColor color) const = 0;
...
};
class Rectangle : public Shape{
public:
...
private:
virtual void draw(ShapeColor color) const;
...
};
七、通过复合塑模出has-a或“根据某物实现出”(38)
- 复合有两种含义:has-a、is-implemented-in-terms-of(根据某物实现出)
- 在应用域,复合意味着has-a
- 在实现域,复合意味着is-implemented-in-terms-of
//has-a
class Address{...};
class PhoneNumber{...};
class Person{
public:
...
private:
std::String name;
Address address;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
};
//is-implememnted-in-terms-of
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的数据,private访问级别
};
- has-a、is-implemented-in-terms-of的区别:
两者都是复合关系,但所表现的意义不同。应用域为has-a,实现域为is-implemented-in-terms-of,实现域侧重基于某物实现某物,通常为private访问级别。 - is-a、is-implemented-in-terms-of的区别:
is-a用来表述D也是一个B,这种表述不为真时,考虑is-implemented-in-terms-of关系。
八、明智而审慎的使用private继承(39)
- private继承意味着is-implemented-in-terms-of(根据某物实现出)。private继承意味着只有实现被继承,接口部分应略去,因为private继承的base class的每样东西在derived classes都是private的。因此private继承在软件设计层面没有意义,其意义只在软件实现层面。
- 尽可能使用复合,必要时才使用private继承。
- 什么时候使用private继承?
- 当derived class需要访问protected base class的成员,或者需要重写继承而来的virtual函数时,使用private是合理的,因为单纯的复合无法完成这两者,稍微复杂的复合+public继承可以(实现一个内部类public继承需要private继承的class,在用复合的方式添加public继承的class)。
- 空白基类最优化(empty base optimization,EBO):现实中的空基类并不真的是empty,会含有typedef、enums、static成员变量或non-virtual函数,STL中就有很多。C++规定空基类的大小不能为0,应插入一个char对象到空对象内。再加上齐位需求,空对象的大小不只是一个char的大小。因此使用private继承可以实现EBO。EBO一般只在单一继承下才可行。
九、明智而审慎的使用多重继承(40)
对于多重继承有两个观点:
- 如果单一继承是好的,多重继承一定更好。
- 单一继承是好的,但多重继承不值得拥有。
9.1 多重继承会导致歧义
class BorrowableItem{
public:
void checkOut();
...
};
class ElectronicGadget{
private:
void checkOut();
...
};
class Mp3Player :
public BorrowableItem,
public ElectronicGadget
{ ... };
Mp3Player mp;
mp.checkcOut();
虽然两个函数中只有一个可取用(一个为public,另一个为private),但仍会产生歧义。
原因:
C++解析重载的规则:C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配才会校验可取用性。
9.2 多重继承可能会涉及virtual继承
- 钻石多重继承
对于钻石多重继承,C++支持两种方案:1)缺省方案:对于每一条路径都会执行复制,对于公共的base class在derived class中会有多个副本。2)virtual 继承:公共base class采用virtual 继承,公共的base class在derived class中会有一份。 - virtual 继承的成本
- 会增加virtual继承而来的derived class对象的大小,如:会增加virtual base class指针。
- 访问virtual base classes的成员变量时,增加访问间接性,比访问non-virtual base classes的成员变量慢。
- virtua base 的初始化是由继承体系中最底层的class负责。这意味着1)classes若派生子自virtual base而需要初始化,必须认知其virtual base —— 不论那些base距离多远;2)当一个新的derived class加入继承体系中,它必须承担其virtual base(不论直接或间接)的初始化责任。
- virtual继承的建议:1)非必要不使用virtual 继承;2)如果必须使用virtual 继承,尽可能避免在virtual base中放置数据,这样可以避免对virtual base 初始化以及访问其成员带来的诡异问题;
9.3 多重继承的使用场景
- “public 继承某个Interface class” 和“private 继承某个协助实现的class”的两者组合
class IPerson{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
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 Person Info{
public:
explicit CPerson(DataBaseID pid):PersonInfo(pid){}
virtual std::string name() const
{return PersonInfo::theName();}
virtual std::string birthDate() const
{return PersonInfo::theBirthDate();}
private:
const char* valueDelimOpen() const {retuen "";}
const char* valueDelimClose() const {retuen "";}
};