文章目录
一、确定你的public继承塑模出is-a模型
1、public继承与“is-a”
以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系。请务必牢记。当你令class D 以public形式继承class B,你便是告诉C++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。is-a关系只对public继承才成立。
class Person { ... };
class Student : public Person { ... };
有时public和is-a之间的关系会误导我们。例如,企鹅(penguin)是一种鸟,这是事实;鸟可以飞,这也是事实。如果以C++描述这层关系:
class Bird
{
public:
virtual void fly(); // 鸟可以飞
.....
};
class Penguin:public Bird // 企鹅是一种鸟
{
.....
};
但是我们知道,企鹅不会飞,这个是事实。这个问题的原因是语言(英语)不严谨。当我们说鸟会飞时,我们表达的意思是一般的鸟都会飞,并不是表达所有的鸟都会飞。我们还应该承认一个事实:有些鸟不会飞。这样可以塑造一下继承关系。
class Bird
{
..... // 没有声明fly函数
};
class FlyingBird:public Bird
{
public:
virtual void fly();
.....
};
class Penguin:public Bird // 企鹅是一种鸟
{
..... // 没有声明fly函数
};
2、请记住
- “public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。
二、避免遮掩继承而来的名称
1、继承而来的名称有什么风险
derived classes内的名称会遮掩base classes内的名称,即使base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数式virtual 或 non-virtual一体适用。在public继承下从来没有人希望如此。考虑如下代码:
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(); // ok,调用Derived::mf1
d.mf1(x); // error, 因为Derived::mf1遮掩了Base::mf1
d.mf2(); // ok, 调用Base::mf2
d.mf3(); // ok, 调用Derived::mf3
d.mf3(x); //error, 因为Derived::mf3遮掩了Base::mf3
如果你继承base class 并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个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 和mf3的所有东西
using Base::mf3; // 在Derived 作用域内都可见
virtual void mf1();
void mf3();
void mf4();
.....
};
//考虑下面调用
Derived d;
int x;
....
d.mf1(); // ok,调用Derived::mf1
d.mf1(x); // ok, 调用Base::mf1
d.mf2(); // ok, 调用Base::mf2
d.mf3(); // ok, 调用Derived::mf3
d.mf3(x); //ok, 调用Base::mf3
2、请记住
-
derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
-
为了让被遮掩的名称再见天日,可使用using声明式或转交函数。
三、区分接口继承和实现继承
1、虚函数
在基类中声明为vitual,并在一个或者多个派生类中被重新定义的函数。虚函数用于提供一类操作的统一命名。一般声明为虚函数是为了实现多态:使用基类指针指向一个派生类对象,通过这个指针调用基类和派生类的同名函数的时候,调用的是派生类中的方法(前提vitual函数肯定实现了)。
【Note】:
虚函数:继承类必须含有的接口,可以自己实现,也可以不实现,而采用基类的实现。
2、纯虚函数
被声明为纯虚函数的类一定是作为基类来使用的,含有纯虚函数的类被称为抽象类,抽象类不能实例化对象。因此纯虚函数一般用来声明接口。其派生类必须实现这个函数。纯虚函数在基类中可以有函数实现,也可以没有。声明纯虚函数的原因是,在基类往往不合适进行实例化,比如一个shape类,中的draw方法。必须为纯虚函数,因为他不是任何一种形状。纯虚函数在基类中是没有定义的,必须在子类中加以实现。
【Note】:
纯虚函数:要求派生类必须实现的函数,在基类中实现没有什么具体的意义。
3、重载函数
重载函数在类型和参数数量上一定不相同,而重定义的虚函数则要求参数的类型和个数、函数返回类型相同;虚函数必须是类的成员函数,重载的函数则不一定是这样; 构造函数可以重载,但不能是虚函数,析构函数可以是虚函数。
函数重载是在程序编译阶段确定操作的对象的,属于静态联编。虚函数是在程序运行阶段确定操作对象的,属于动态联编。重载的函数必须具有相同的函数名,函数类型可以相同也可以不同,但函数的参数个数和参数类型二者中至少有一个不同,否则在编译时无法区分。而虚函数则要求同一类族中的所有虚函数的函数名,函数类型,函数的参数个数和参数类型都全部相同,否则就不是重定义了,也就不是虚函数了。
4、接口继承和实现继承
-
接口继承:派生类只继承函数接口,也就是声明。
-
实现继承:派生类同时继承函数的接口和实现。
5、请记住
-
成员函数的接口总是会被继承,public继承意味is-a。
-
声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口。令人意外的是,C++竟然允许我们为pure virtual函数提供定义,但调用它的唯一途径是“调用时明确指出其class名称”。
(baseObj>BaseName::virtualFuncName();)
-
声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。
-
声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
四、考虑virtual函数以外的选择
1、non-virtual interface:提供非虚接口
class Object{
public:
void Interface(){
···
doInterface();
···
}
private/protected:
virtual doInterface(){}
}
令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。
2、使用std::fun提供回调函数
class GameCharacter; // 前置声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
3、请记住
-
使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。
-
以std::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
五、绝不重新定义继承而来的non-virtual函数
class B
{
public:
void mf();
....
};
class D : public B
{
public:
void mf(); // 遮掩B::mf,条款33
.....
};
D x; // D 对象
B* pB = &x;
D* pD = &x;
// 针对D对象x,下面语句竟然调用的是不同的mf
pB->mf(); // 调用B::mf
pD->mf(); // 调用D::mf
当mf(base class 内为 non-virtual 并在derived class 内被重载的函数)被调用,如何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初的声明类型。
1、请记住
- 绝对不要重新定义继承而来的non-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 继承缺省参数值。
// 但若以指针(或reference)调用此函数,可以不指定参数值。
// 因为动态绑定下这个函数会从其base 继承缺省参数值。
...
};
// 考虑下面指针
Shape* ps; // 静态类型为Shape*, 没有动态类型, 尚未指向任何对象
Shape* pc = new Circle; // 静态类型为Shape*, 动态类型为Circle*
Shape* pr = new Rectangle; // 静态类型为Shape*, 动态类型为Rectangle*
// 动态类型一如其名称所示,可在程序执行过程中改变(通常经由赋值完成)
ps = pc; // ps 的动态类型如今是Circle*
ps = pr; // ps 的动态类型如今是Rectangle*
对象的所谓“静态类型”是指它在程序中被声明时所采用的类型(初始)。对象的所谓“动态类型”是指“目前所指对象的类型”(当前)。
1、请记住
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数——你唯一应该覆写的东西——却是动态绑定。
七、通过复合塑模出has-a或者"根据某物实现出"
复合(组合)(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。条款32曾说,“public继承”带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个,包含关系)或 is-implemented-in-terms-of(根据某物(底层)实现出(适配器),类似STL的容器适配器如Stack,Queue等)。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。
1、请记住
-
复合的意义和public继承完全不同。
-
在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。
八、明智而审慎地使用private继承
1、private继承有什么问题
class Person{……};
class Student: private Person{……};
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p);
eat(s); // 出错,因为private继承不是is-a关系
private继承意味is-implemented-in-terms-of(根据某物实现出)。private继承纯粹只是一种实现技术(这就是为什么继承自一个private base class 的每样东西在你的class 内都是private:因为它们都只是实现枝节而已)。
如果D 以private 形式继承 B,意思是 D 对象根据 B 对象实现而得,再没有其他意涵了。
2、请记住
-
Private 继承意味 is-implemented-in-terms-of(根据某物实现出),它通常比复合的级别低(尽量使用组合)。但是当derived class需要访问 protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。即便如此,一个混合了public继承和复合的设计,也能达成你所要的行为。
-
和复合不同,private 继承可以造成empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
九、明智而审慎地使用多重继承
1、菱形继承
//包含A对象
class A{
};
//包含A,B对象
class B:public A{
};
//包含A,C对象
class C:public A{
};
//包含A,A,B,C,D对象
class D:public B, public C{
}
由于菱形继承,基类被构造了两次。其实,C++也提供了针对菱形继承的解决方案的:
//包含A对象
class A{
};
//包含A,B对象
class B:virtual public A{
};
//包含A,C对象
class C:virtual public A{
};
//包含A,B,C,D对象
class D:public B, public C{
}
使用虚继承,B,C对象里面会产生一个指针指向唯一一份A对象。
virtual 继承所需要付出的成本是巨大的:首先,使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们的体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes的成员变量速度慢。其次,virtual base 的初始化责任是由继承体系中的最低层 class 负责,这暗示:(1)classes 若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases 距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。
2、请记住
-
多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承(虚基类)的需要。
-
virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base class 不带任何数据,将是最具实用价值的情况。
-
多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private 继承某个协助实现的class”的两相组合。