继承与面向对象设计
继承可以是单一继承或多重继承,每一个继承链接可以是public、protected、private也可以是virtual或non-virtual的。各个成员函数的各个选项:virtual?non-virtual?pure virtual?以及成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响?继承如何影响c++名称查找规则?设计选项有哪些?如果class的行为需要修改,virtual函数是最佳选择嘛?
public继承意味者is-a,virual函数意味着接口必须被继承,non-virtual意味这接口和实现必须都被继承。
确定你的public继承塑膜出is-a关系
“Base 对象可派上用场的任何地方,Derived 对象一样可以派上用场,反之不成立liskov substitution principle”,每个Derived对象都是一个Base对象。
public继承主张,能够施行于base class对象身上的每件事,也可以施行于derived class对象身上。使用public继承,你就必须继承base内的所有。
像正方形是矩阵这样的存在与我们大脑中直观印象,并不适用于面向对象public继承关系下。
避免遮蔽继承而来的名称
一般局部变量会遮蔽外围作用域的名称,至于名称是否相应和相同或不同的类型,并不重要。
继承的时候,derived class成员函数refer to base class内的某物时,编译器可以找出我们指向的东西,因为derived class继承了声明与base class内的所有东西。实际方式是,derived class作用域被嵌套在base class 作用域内。
上图例子中包含了private,public名称,同时成员函数包括virtual ,pure virtual和non-virtual三种。是为了说明名称和其他无关。
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(); //true
d.mf1(int); //false 因为Derived::mf1 遮蔽了Base::mf1
d.mf2(); //true
d.mf3(); //ture
d.mf3(x); //false 因为Derived::mf3遮蔽了Base::mf3
上述行为是为了防止在建立新的derived class时附带的从疏远的base class继承重载函数,但是有的时候你想要这么做,实际上如果使用public继承又不继承那些重载函数,就是违反了base和derived class之间的is-a的关系。可以通过使用using方法来达到目标:
//可以达到继承base class内的所有函数的目的,不至于破坏is-a的关系
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;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
///
};
上述的方法达到:你可以继承base class并加上重载函数,同时又可以重新定义或覆写其中一部分,那么可以为原本会被遮蔽的每个名称引入一个using声明式,否则某些希望继承的名称就会被遮蔽。
当不希望继承所有base class内的东西时,可以通过private继承来完成。当希望继承base内的一个函数时,可以通过转交函数来实现。
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
//...与前面相同
};
class Derived: private Base{
public:
virtual void mf1(){ Base::mf1();} //转交函数自动成为inline函数
//...
};
Derived d;
int x;
d.mf1(); //true 调用的是Derived::mf1()
d.mf1(x); //false Base::mf1()被遮蔽了
区分接口继承和实现继承
public继承一般是由两部分组成:函数接口继承和函数实现继承,两者的差异很像函数声明和函数定义之间的差别。
(继承:只继承函数的接口(即声明),继承函数的接口和实现,但又能够覆写他们所继承的实现;同时继承接口和实现并且不允许覆写任何东西)
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{ //...};
pure virtual函数的目的是为了让derived class只继承函数接口,继承的类必须重新定义。但是在调用的时候必须明确指出其class名称。
Shape* ps = new Shape; //false Shape 是抽象的
Shape* ps1 = new Rectangle; //true
ps1->draw(); //调用的是Rectangle::draw();
Shape* ps2 = new Ellipse; //true
ps2->draw(); //调用的是Ellipse::draw();
ps1->Shape::draw(); //调用的是Shape::draw()
声明impure virtual函数的目的,是让derived class继承该函数的接口和缺省实现,derived class可能覆写它
但是允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。
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的实现。
//当新增一辆飞机时,它的飞行方式不同与AB,但是忘记重新定义了fly函数
class ModelC: public Airplane{ //...}; //未声明fly函数
Airport PDX(//...);
Airplane *pa = new ModelC;
pa->fly(PDX); //false 因为c的飞行方式不同于AB
因为ModelC在未明白说出“我要”的情况下就继承了缺省行为,导致了错误。可以通过切断virtual函数接口和其缺省实现之间的连接方法来避免。
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
//...
protected:
void defaultFly(const Airport& destination); //没有任何一个derived class应该重新定义此函数
//不能声明为virtual函数,因为derived class可能忘记重新定义它
};
void Airplane::defaultFly(const Airport& destination){
//缺省行为,将飞机飞至指定目的地
}
class ModelA: public Airplane{
public:
virtual void fly(const Airport& destination){
defaultFly(destination);
}
};
class ModelC: public Airplane{
public:
virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination){
//将飞机C飞至指定目的地
}
还可以通过pure virtual函数来完成上述工作:
class Airplane{
public:
virtual void fly(const Airport& destination) = 0;
};
void Airplane::fly(const Airport& destination){
//缺省行为,将飞机飞至指定目的地
}
class ModelA: 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飞至指定目的地
}
**声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。**non-virtual函数代表的意义是不变形凌驾与特异性。
考虑virutal函数以外的其他选择
借由non-virtual interface 手法实现template method 模式:
class GameCharacter{
public:
int healthValue()const{
//.... 做一些事前工作
int retVal = doHealthValue(); //做真正的工作
//.... 做一些事后工作
return retVal;
}
//...
private:
virtual int doHealthValue() const{
//...缺省算法
}
};
//non-virtual 函数称为virtual函数的外敷器(wrapper)
优点:确保得以在一个virtual函数被调用前设定好适当的场景,并在调用结束后清理场景。
NVI手法涉及在derived class内重新定义private virtual函数的。NVI手法允许derived class重新定义virtual函数,从而赋予他们“如何实现技能”的控制能力,但是base class保留述说“函数何时被调用”的权利。(NVI手法下没有必要让virtual函数一定是private的)
藉由function pointers实现strategy模式:
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;
};
//同一人物类型的不同实体可以有不同的健康计算函数
class EvilBadGuy: public GameCharacter{
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc): GameCharacter(hcf){ //...}
//...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);
//某已知人物的健康指数计算函数可以在运行期变更。例如:GameCharacter可提供一个成员函数setHealthCalculator,用以替换当前的健康指数计算函数。
//换句话说,健康指数计算函数不再是GameCharacter继承体系内的成员函数。意味着,这些计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成份。
当需要以non-member函数访问class 的non-public成员的方法是:弱化class 的封装。可以将那个non-member函数声明为class 的friend。或为其实现的某一部分提供public访问函数。用函数指针替换virtual函数的优点(像每个对象都有自己的健康计算函数)是否弥补缺点(降低class的封装性),是必须根据每个设计情况的不同而抉择的。
藉由tr1::function完成strategy模式:
class GameCharacter; //前置声明
int defaultHealthCalc(const GameCharacter& gc)// 此函数计算健康指数的缺省算法
class GameCharacter{
public:
typedef std::tr1::function<int (const GameCharacter&)>HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf){}
int healthValue() const{ return healthFunc(*this); }
//...
private:
HealthCalcFunc healthFunc;
};
tr1::function的签名代表的函数是“接收一个reference指向const GameCharacter,并返回int”,这个tr1::function类型产生的对象可以持有任何与此签名式兼容的可调用物。(兼容是值可调用物的参数可被隐式转换为cons GameCharacter&,而其返回类型可被隐式转换为int)。
short calcHealth(const GameCharacter&);
struct HealthCalculator{
int operator()(const GameCharacter&) const{ //...}
};
class GameLevel{
public:
float health(const GameCharacter&) const;
//...
};
class EvilBadGuy: public GameCharacter{ //同上
//...
};
class EyeCandyCharacter: public GameCharacter{ //同上
//...
};
EvilBadGuy ebg1(calcHealth); //人物1
EyeCandyCharacter ecc1(HealthCalculator()); //人物2
GameLevel currentLevel;
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1)); //具体细节需要另说
以上的方法只是一个开头,其他很多实现设计方法需要探索。
绝不重新定义继承而来的non-virtual函数
class B{
public:
void mf();
};
class D:public B{ //...};
D x;
B* pb = &x;
pb->mf();
D* pd = &x;
pd->mf();
//如果D中重新定义了non-virtual函数,那么B中的mf函数就被遮蔽啦,所以两者的调用结果不一样。
时刻记住public是is-a的关系,即适用于B对象的每一件事,也适用于D对象。B的derived class一定会继承mf的接口和实现
绝不重新定义继承而来的缺省参数值()
virtual函数是动态绑定的,而缺省参数值却是静态绑定。静态绑定是在编译时期就已经决定了的类型。
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 = green) const; //重新定义了继承而来的缺省参数值。应该去掉的
};
class Circle: public Shape{
public:
virtual void draw(ShapeColor color) const;
//当客户以对象调用此函数,一定要指定参数值
//因为静态绑定下这个函数并不从其base继承缺省参数值
//但若以指针(或引用)调用这个函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省参数值
};
Shape * ps;
Shape *pc = new Circle; //静态类型为Shape*
Shape *pr = new Rectangle; //静态类型为Shape*
pr->draw(); //调用Rectangle::draw(Shape::Red)
//pr的动态类型是Rectangle*,索引调用的是Rectangle 的virtual函数,Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape*,所以此调用的缺省参数值来自Shape class而不是Rectangle class
//导致调用很奇怪。
通过符合塑膜出has-a或“根据某物实现出”
class Address{};
class PhoneNumber{};
class Person{
public:
private:
std::string name;
Address address;
PhoneNumber voiceNumber;
};
复合意味这has-a或is-implemented-in-terms-of(根据某物实现出)。在应用域上是has-a的意思,但是在实现域上复合表现为is-implemented-in-term-of的关系。
例如:当用list实现set的时候,如果使用has-a的关系,那么就会导致冲突
template<typename T>
class Set: public std::list<T>{}; //错误的用法,因为list允许元素重复,但是set不行。
此时应该采用is-implemented-in-term-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;
};
明智而审慎的使用private继承()
如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。由private base class继承而来的所有成员,在derived class中都会变成private 属性,纵使它们在base class中原本是protected或public属性。private继承意味这implemented-in-terms-of(根据某物实现出)。
private继承纯粹是一种实现技术(这就是为什么继承子一个private base class的每样东西在你的class内都是private,因为他们都是实现枝节而已)。
如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意思啦
尽可能使用复合,必要时采用private继承,当protected成员和或virtual函数牵涉进来的时候,是必要。其中一个需要访问另一个的protected成员,或需要重新定义一个或多个virtual函数,private继承可能成为正统设计策略。
同时private继承可以造成empty base最优化。这对于致力于对象尺寸最小化的程序库开发者而言,可能很重要。
明智而审慎地使用多重继承()
多重继承比单一继承复杂。它可能会导致新的歧义性,以及对virtual继承的需要。
class BorrowwableItem{
public:
void checkOut();
};
class ElectronicGadget{
private:
void checkOut();
};
class MP3Player: public BorrowableItem, public ElectronicGadget{
//...
};
MP3Player mp;
mp.checkOut(); //false 歧义,不知道调用哪个checkOut
mp.BorrowableItem::checkOut(); //true
virtual继承会增加大小,速度,初始化复杂度等成本。如果virtual base class不带任何数据,将是最具使用价值的情况。
非必要不实用virtual base,平常使用non-virtual继承,第二,如果你必须使用virtual base class 尽可能避免在其中放置数据
以后补充深入探索c++对象模型的内存分讨论
多重继承的正当用途的一个例子:当public继承某个interface class和private继承某个协助实现的class的两相组合的情况下。
//用来塑膜人的interface class
class IPerson{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
//IPerson的客户必须用IPerson的指针或reference来编写程序
//使用factory function可以将派生自IPerson的具象class实体化:
std::shared_ptr<IPerson> makePerson(DatabaseID persoonIdentifier);
//这个函数从使用这手上取得一个数据库ID
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::shared_ptr<IPerson> pp(makePerson(id));
lcass PersonInfo{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
private:
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
};
const char* PersonInfo::valueDelimOpen() const{
return "[";
}
const char* PersonInfo::valueDelimClose() const{
return "]";
}
const char* PersonInfo::theName() const{
static char value[Max_formatted_field_value_length];
std::strcpy(value, valueDelimOpen());
std::strcat(value,valueDelimClose());
return value;
}
为了代码复用,通过PersonInfo来实现CPerson
class CPerson: public IPerson, private PersonInfo{
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 { return "";}
const char* valueDelimClose() const { return "";}
};