第六章 继承与面向对象设计
2014-08-11 13:44
357 查看
面向对象编程OOP非常流行。但是可能会遇到很多问题。例如继承可以是单一继承或多重继承,每个继承连接可以是public,protected、private,也可以是Virtual或者non-virtual。然后是成员函数的各个选项:Virtual?non-virtual? pure virtual?以及成员函数和其他语言特性的交互营销:缺省参数值与Virtual函数有什么交互影响?继承如何影响C++的名称查找规则?设计选项有哪些?如果class的行为需要修改,Virtual函数时最佳选择吗?本章主要解决上诉问题。
但是,有时候会犯认识上的错误:比如为类bird定义了函数“fly”,但是当我们从bird派生出企鹅Penguin时,却发现企鹅应该是不会飞的。这该怎么办呢?
第一,我们可以修改我们的设计:
C++ Code
第二,我们可以为企鹅重新定义一个fly函数,然后让它产生错误:
C++ Code
在调用函数中:
C++ Code
它的基本思想是:企鹅会飞,但是这种尝试是错误的。
和第一种相比,这是运行时错误,只有当程序运行时才能检测出来;而第一种是编译时错误,p.fly()本身就不能被调用。
第三种做法,就是对penguin不定义fly函数,这样当你试图调用时也会出错。
is-a 并非是唯一存在于calsses之间的关系,另两个常见关系是has-a和is-implemented-in-terms-of(根据某物实现出来)。将上诉这些重要的相互关系中的任何一个误塑为is-a并不常见,需要理解清楚。
请记住:
public继承意味is-a。适用于base classes 身上的每一件事情一定也适用于derived classes 身上,因为每一个derived class 对象也都是一个base classes。
C++ Code
而对于继承派生体系也是如此,因为派生类继承了基类的所有public部分,所以:
C++ Code
中f1调用的是派生类的f2,如果想调用基类的f2,那么必须指明作用域:
C++ Code
明白了作用域关系之后,让我们看一个例子:
C++ Code
此时
C++ Code
因为派生类中的f2(int i)屏蔽了基类中的f2()。同理还有f3(int i)函数。f1函数由于抽象类声明了接口为f1(),所以没有改变。这说明,这种关系的掩盖只取决于函数的名字,与函数的形参、返回值类型,是否为虚函数之类的无关。
总之,我们发现基类的函数在这里不能使用了:即is-a关系不再成立了!为了让is-a关系继续满足,你可以通过声明来实现:
C++ Code
此时
C++ Code
都是正确的。
但是,using声明带来的问题是:假如我只想使用基类的f3的某个特定版本,但是这里会把f3的所有版本暴漏给派生类。怎么办呢?有一种称为转交函数的办法,在派生类中定义:
C++ Code
此时就避免了基类中的f3()函数暴漏出来了。
总之,派生类内的名称会掩盖基类中的名称,如果不想让这些名称被掩盖,可以使用using声明或者使用转交函数。
C++ Code
则对于eat函数你必须在派生类中给出声明和定义,否则派生类还是一个抽象类。对于move函数,假如你没有给出声明和定义,那么就会自动的去调用基类的move,而对于基类定义好的mad,你直接拿来用就好了,没有必要重新定义,因为重新定义会发生前一小节介绍的派生类函数屏蔽基类函数的问题。
当然,事情总有一些特例,比如,我们可以对基类的纯虚函数eat给出定义。
C++ Code
然后通过派生类去访问它:
C++ Code
不过这种用法并不常见。
对于虚函数,如果派生类定义了自己的版本,那么派生类的对象就会调用自己的;如果没有,就会调用基类。这本来是一件多么美好而方便的事啊!可是,它有可能让程序员忘掉了这个函数的行为不是这个类中定义的,而是一种默认的行为。我们希望的结果是告诉编译器我要用默认行为了,再用默认行为;如果我没说,请编译器提醒我这样做不对:
class Animal
{
public:
virtual void eat() = 0;
virtual void move() = 0;
void mad();
protected:
void defaultMove();
};
void Animal::defaultMove()
{
cout<<"move from A to B"<<endl;
}
void Bird::move()
{
defaultMove();
// cout<<"fly from A to B"<<endl;
}
首先,将move设为纯虚函数,并设定了defaultMove函数,那么在派生类的move中,如果我们要使用defaultMove,必须指明。有一点需要注意,此时defaultMove应该是一个非虚函数,这意味着派生类不应该重新定义它。因为如果它被重新定义,则整个默认动作就乱套了。
C++ Code
此时,move仍为纯虚函数,但是我们给出了它的实现。如果我们想调用默认动作,必须显式指明使用的是基类中的move。
按照前面的论述,则可以避免两种常见的错误:
1.把基类中的所有函数都声明为非虚函数—这就意味着你不打算给派生类一点自我发挥的空间。而且如果析构函数不声明为虚函数,可能会带来各种问题。
2.将所有函数都声明为虚函数,但这也表明了你对自己的设计的基类很没有信心,基本没有什么东西是确定的。
总而言之,纯虚函数使得派生类只继承基类的接口;虚函数让派生类可以自我发挥,但是如果不想发挥,也可以提供默认版本;而非虚函数则要求你必须继承它,没有自由空间。
C++ Code
就是说基类里定义了一个计算生命值的函数,派生类通过重新定义这个函数来完成不同类型的角色的生命值的计算。
假如生命值的计算分为如下几步:
1.获得生命值。
2.通过一个函数计算生命值。
3.将生命值返回。
那么每个派生类的healthValue函数都需要完成这几步,我们能不能重构这个代码呢?先看一下重构的结果:
C++ Code
这种做法乍看起来不是很习惯,我们要对它的思路仔细说说:
1.它在基类中声明了2个不会被继承的虚函数getInitialVal()和calcVal(int val)。但是在基类的可以被继承的(且不希望被修改的)healthValue函数中调用了。
2.在派生类中定义了healthValue所要调用的函数。
他这样做的好处是:在基类中,限定了先做什么,后做什么。但是具体怎样做,把权力移交给了派生类。
这种思路,称为模板方法模式,它的定义为:定义一个操作中的算法的骨架,而将一些方法实现延迟到子类。模板方法使得子类可以不改变一个算法的结构即可以重定义该算法的某些特定步骤。
但是这样做其实并不灵活,假如我希望同一个类型的不同对象有不同的计算生命值的方法,就麻烦了。换个角度思考,人物健康指数的计算,其实,不一定与人物的特定类型有关,对于同一个类型,也可以有不同的计算方法。由此我们想到,不能让每个类型的声明计算与一个函数相关,而对于不同的对象,可以调用不同的函数来完成这件事。依照这个思路,我们可以这么写:
C++ Code
此时,人物类型与计算声明的方法就无关了:
C++ Code
注意,计算生命值的函数并没有访问对象的non-public部分。如果计算生命值的确不需要,那么这样做是没问题的,就想我们例子中,生命值只与初始生命值有关,所以就OK,如果还需要这个类的其他非公有成分,那么就会破坏类的封装性:要么把这些成分设定为public,那么提供get函数访问他们,要么把这个非成员函数设为friend。其实,把一个类内的成员函数变为非成员函数,总会遇到这样的问题,这取决于你的设计思路。
这个例子一下子拓宽了我们的眼界:为什么非得使用某个函数去计算生命值,能不能使用某个类似函数的,可以被调用的东西来计算呢?比如函数对象、类的成员函数等等。通过tr1中的function可以帮你完成这样的设想:
C++ Code
这个例子跟前面的很类似,但是又有所区别:我们没有定义类型确定的函数指针,而是定义了一个“泛化的”函数指针:HealthCalcFunc,它的返回值为int(或可以转化为int),输入参数为GameCharacter引用(或可以转化为GameCharacter)的可调用物:可以是一般函数、可以是函数对象,也可以是成员函数。
对于普通函数,和函数对象,可以直接用来给HealthCalcFunc赋值。对非静态成员函数,需要通过bind绑定:为了计算s4的健康函数,需要使用GameLevel里面的easy函数,这个函数实际上有两个参数:*this(GameLevel类型)和GameCharacter&,HealthCalcFunc只接受一个参数:GameCharacter&。需要将GameLevel类型中的easy函数与调用它的对象绑定起来,此后每次调用easy函数,都是调用绑定的那个对象(level)的这个函数。其中_1是占位符,表示的是这个函数的第一个参数。
经过上一个想法的洗礼,尤其是调用类成员函数,使我们不禁想到了为什么不把所有的计算生命函数设成一个基类,然后从中派生出各个子类方法,然后让GameCharacter调用这些方法呢?下面是实现的程序:
C++ Code
这个架构的特点就是可扩展性很强,我们可以新加入不同的角色—从GameCharacter中派生,也可以加入新的计算健康值的方法—从HealthCalcFunc中派生。甚至,假如我们要大幅度的修改游戏,比如,给角色装配一个武器,那么在GameCharacter中,增加一个指向武器类的指针,然后定义武器类就行了,是得程序修改更加方便。
这个方法称为strategy模式,它的定义如下:Strategy模式定义了一系列的算法,将它们每一个进行封装,并使它们可以相互交换。Strategy模式使得算法不依赖于使用它的客户端。
这个条款略微有些长,但总结起来,无非是这样对于虚函数,有如下几种替代方案:
1.模版方法模式。在类中确定派生类要做的事情的顺序,然后让派生类自己实现它们。
2.使用函数指针。将虚函数移到类的外部,但是它不能访问类的private成分。
3.使用tr1::function指定“泛型”函数指针,是得我们可以通过函数、函数对象、成员函数来替代虚函数。
4.使用策略模式,将虚函数所要完成的事情封装成类,以便于扩展。
C++ Code
我们发现,对于同一个对象d,使用不同类型的指针指向它时,他表现得不尽相同。同理对于引用也是一样。
造成这个原因是,non-virtual函数如B::mf和D::mf都是静态绑定的。
但是在另一方面,Virtual函数确定动态绑定的,所以他们不受这个问题之苦,如果mf是个Virtual函数,不论是通过pB或者pD去调用mf 都会导致调用D::mf,因为pB和pD真正指的都是一个类型为D的对象。
此时,我们之前提过的public继承是一种is-a关系:
对于一件事情如果Base能做,那么Drived也能做。但是当你重新定义了派生类的非虚函数时,那么在使用指针或者引用时,从直观上看,派生类做的事情就可以与基类做的大不相同。
总之,绝不重定义继承而来的non-virtual函数。
C++ Code
运行结果如下:
有没有觉得很奇怪,根据动态绑定的原则,这里的确调用的是派生类的getVal函数,可是为什么会输出0呢?如果把这里的指针换成引用,也会得出相同的结果。原因出在:virtual函数是动态绑定的,但是函数的默认参数却是静态绑定的!所以当编译器看到pb是Base*这个类型时,在调用函数时,就会选择基类的默认参数。
这是多么容易出错的一件事啊!调用派生类的函数,但是他的参数的默认值却是基类提供的。但编译器也有自己的苦衷:运行期效率。
那么我们似乎应该将派生类的默认参数改成与基类相同,但是这又带来其他的问题,其中最严重的是:如果基类的默认参数需要修改,那你不得不修改所有派生类的默认参数!
一种比较好的替代方案是前面介绍的NVI(non-virtual interface)手法:
做法规则:
令base class内的一个public non-virtual 函数调用private Virtual函数,后者可被derived classes 重新定义。这里我们可以让non-virtual函数指定缺省参数,而private Virtual函数负责真正的工作:
C++ Code
由于 non-virtual函数应该绝对不被derived classes 覆写,此时,基类和派生类就使用了共同的默认参数了。
总之,virtual函数是动态绑定,但是函数的默认参数是静态绑定的。所以绝不要重新定义派生类的函数的默认参数值,而且最好使用NVI手法。
比如,定义一个“人”类,而人又有地址,那么你应该这么写:
C++ Code
而不是用Person去public继承Address,虽然这样做也能让一个人有地址,但是它却违背了public表示is-a的关系。人又不是一个特化了的“地址”。所以,这里使用复合会更恰当一些。复合往往表示2种意思,这取决于你程序的应用:第一种是has-a:当你程序描述的是实际的东西时,大都是这种关系:人有名字,学校有教室等等;第二种是“由XX实现”,比如,集(set)由链表(list)实现等等。
下面就用这个例子来说明:
C++ Code
让然,与标准库的set相比,这个例子还是有些瑕疵,因为标准库每次插入元素后,都会自动排序。而这里却不行。
总之,复合与public继承完全不同,它意味着has-a或者通过XX实现。
C++ Code
2.基类的public和protected成员在派生类中全为private属性。
因此,private继承意味着:根据某物实现。这与前面条款介绍的复合很类似。在大多数时候,我们应该使用复合,而不是private继承来实现这种功能。但是当有protected成员和虚函数牵扯进来的时候,我们又不得不用private继承。
其次,与复合相比,private可以使空基类的最优化。先看一个例子:
C++ Code
在vs2010中,sizeof(int)为4,sizeof(Empty)为1,sizeof(HoldsAnInt)为8这似乎有悖于我们学过常理:首先,类的大小取决于其数据成员的大小。sizeof(Empty)应该为0,但是由于在编译器会将它的大小设为1(安插一个char),而“齐位需求”会将它放大为1个int,所以,izeof(HoldsAnInt)为8。
C++ Code
但是,如果使用的是private继承来实现,就不存在这种问题了:sizeof(HoldsAnInt)只有一个int的大小:4.
总之,private继承意味着根据某物实现。当派生类需要访问基类的的受保护成员或者重新定义虚函数时,我们才使用它。而且private继承可以是得空基类最优化,如果在开发中需要是得对象尺寸最小,那么也用得着它。
C++ Code
也许你会奇怪,Base2中的func应该是不会被继承下来的,可是为什么在调用func时还会产生二义性呢?这是因为编译器在检查函数是否可取之前,先会判断这个函数是否是最佳匹配。此时,有两个最佳匹配,所以就会出现二义性。所以,最好是明确你要调用的是哪个基类的函数:d.Base1::func();
这还不是最可怕的情况,最可怕的是出现“钻石型多重继承”:B和C继承自A,而且D继承自B和C。此时,理论上讲,D中会有两份A的public成员(这里假定是public继承),但实际上,大多数时候,我们只希望有一份,此时只能通过虚继承来避免这种现象。
C++ Code
此时,d.print();将会有二义性,因为他不知道自己调用的是从Middle1继承下来的print还是从Middle2继承下来的print。此时,只需要将Middle1和Middle2声明为虚继承,就会只从基类中保存一份print副本。正确的做法如下所示:
C++ Code
我们觉得,似乎所有的继承都该被声明为虚继承,但是虚继承会使派生类的大小变得更大,而且访问速度更慢。还有一点需要注意的,就是在使用虚继承之后,构造函数的调用顺序也变了:不再是派生类调用自己的基类,依次向上层调用了,而是从最高的基类开始,向下调用。
因此,不到万不得以,不要使用虚基类,在使用时,尽量避免在其中放置数据,因为它的初始化规则跟一般情况不同。
然后书中给出了一个使用的多重继承的例子:有一个接口类,我们需要实现它;恰巧我们有一个现成的类,这个类的功能跟我们要实现的类相似,但是我们需要对它的虚函数进行重写,已达到我们希望的功能,于是我们public继承了这接口,又private继承了它的实现,并对其中的某些实现重定义了。这时候,多重继承真的就派上了用场。
总的来说,多重继承可能会引起很多问题:二义性、构造、析构函数的顺序,类的大小、访问速度等等。但有时的确也会使用到它。
条款32:确定你的public继承塑模出is-a关系
public继承意味着“is-a”关系。它的意思是:如果B以public形式继承自A,那么B类型对象肯定是一个A对象,反之不成立。A是B的一种抽象,B是A的特例。任何使用A的地方,都能使用B。但是,有时候会犯认识上的错误:比如为类bird定义了函数“fly”,但是当我们从bird派生出企鹅Penguin时,却发现企鹅应该是不会飞的。这该怎么办呢?
第一,我们可以修改我们的设计:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class Bird { }; class CanFly: public Bird { public: virtual void fly() ; }; class CannotFly: public Bird { }; class Penguin: public CannotFly { }; |
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Bird { public: virtual void fly() {} }; class Penguin { public: void fly() { throw runtime_error("attempt to make a penguin fly"); } }; |
C++ Code
1 2 3 4 5 6 7 8 9 | Penguin p; try { p.fly(); } catch(const runtime_error &e) { cerr << e.what() << endl; } |
和第一种相比,这是运行时错误,只有当程序运行时才能检测出来;而第一种是编译时错误,p.fly()本身就不能被调用。
第三种做法,就是对penguin不定义fly函数,这样当你试图调用时也会出错。
is-a 并非是唯一存在于calsses之间的关系,另两个常见关系是has-a和is-implemented-in-terms-of(根据某物实现出来)。将上诉这些重要的相互关系中的任何一个误塑为is-a并不常见,需要理解清楚。
请记住:
public继承意味is-a。适用于base classes 身上的每一件事情一定也适用于derived classes 身上,因为每一个derived class 对象也都是一个base classes。
条款33:避免掩盖继承而来的名称
这其实是一个作用域带来的问题:局部变量会掩盖同名的外围变量。注意,只要同名就会被掩盖,与类型无关:C++ Code
1 2 3 4 5 6 7 8 9 10 | void main() { int a = 10; { double a = 0.1; cout << a << endl; //结果为0.1 } cout << a << endl; //结果为10 } |
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class Base { public: virtual void f1() = 0; virtual void f2() { cout << "Base f2" << endl; } }; class Drived: public Base { public: void f1() { f2(); } void f2() { cout << "Drived f2" << endl; } }; //main.cpp Drived d; d.f1(); |
C++ Code
1 2 3 4 | void f1() { Base::f2(); } |
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class Base { public: virtual void f1() = 0; virtual void f2() { cout << "Base f2" << endl; } void f3() { cout << "Base f3" << endl; } void f3(double d) { cout << "Base f3 double" << endl; //重载 } }; class Drived: public Base { public: void f1() { cout << "Drived f1" << endl; } void f2(int i) { cout << "Drived f2" << endl; } void f3() { cout << "Base f3 int" << endl; } }; |
C++ Code
1 2 3 | Drived d; d.f2(1);//正确 //错误d.f2(); |
总之,我们发现基类的函数在这里不能使用了:即is-a关系不再成立了!为了让is-a关系继续满足,你可以通过声明来实现:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public: using Base::f2; using Base::f3; void f1() { cout << "Drived f1" << endl; } int f2(int i) { cout << "Drived f2" << endl; return 1; } void f3(int i) { cout << "Drived f3" << endl; } |
C++ Code
1 2 3 | d.f3(); d.f3(1); d.f3(1.1); |
但是,using声明带来的问题是:假如我只想使用基类的f3的某个特定版本,但是这里会把f3的所有版本暴漏给派生类。怎么办呢?有一种称为转交函数的办法,在派生类中定义:
C++ Code
1 2 3 4 | void f3(double i) { Base::f3(i); } |
总之,派生类内的名称会掩盖基类中的名称,如果不想让这些名称被掩盖,可以使用using声明或者使用转交函数。
条款34:区分接口继承和实现继承
作为类的设计者,我们有时候希望派生类只继承基类成员函数的接口,有时候希望派生类继承基类的接口及实现,同时又能自己重写这些实现,有时希望派生类老老实实的继承这些函数的接口和实现,而且不做任何修改。这三种不同的策略是通过声明基类成员为纯虚函数、虚函数和非虚函数来实现的。看一个例子:C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | class Animal { public: virtual void eat() = 0; virtual void move(); void mad(); }; class Bird: public Animal { public: void eat(); void move(); }; void Animal::mad() { cout << "mad" << endl; } void Animal::move() { cout << "move from A to B" << endl; } void Bird::eat() { cout << "eat worm" << endl; } void Bird::move() { cout << "fly from A to B" << endl; } |
当然,事情总有一些特例,比如,我们可以对基类的纯虚函数eat给出定义。
C++ Code
1 2 3 4 | void Animal::eat() { cout << " eat everything" << endl; } |
C++ Code
1 2 | Bird b; b.Animal::eat(); |
对于虚函数,如果派生类定义了自己的版本,那么派生类的对象就会调用自己的;如果没有,就会调用基类。这本来是一件多么美好而方便的事啊!可是,它有可能让程序员忘掉了这个函数的行为不是这个类中定义的,而是一种默认的行为。我们希望的结果是告诉编译器我要用默认行为了,再用默认行为;如果我没说,请编译器提醒我这样做不对:
class Animal
{
public:
virtual void eat() = 0;
virtual void move() = 0;
void mad();
protected:
void defaultMove();
};
void Animal::defaultMove()
{
cout<<"move from A to B"<<endl;
}
void Bird::move()
{
defaultMove();
// cout<<"fly from A to B"<<endl;
}
首先,将move设为纯虚函数,并设定了defaultMove函数,那么在派生类的move中,如果我们要使用defaultMove,必须指明。有一点需要注意,此时defaultMove应该是一个非虚函数,这意味着派生类不应该重新定义它。因为如果它被重新定义,则整个默认动作就乱套了。
C++ Code
1 2 3 4 5 6 7 8 9 10 11 | void Animal::move()//纯虚函数的实现 { cout << "move from A to B" << endl; } void Bird::move() { //调用纯虚函数的实现 Animal::move(); // cout<<"fly from A to B"<<endl; } |
按照前面的论述,则可以避免两种常见的错误:
1.把基类中的所有函数都声明为非虚函数—这就意味着你不打算给派生类一点自我发挥的空间。而且如果析构函数不声明为虚函数,可能会带来各种问题。
2.将所有函数都声明为虚函数,但这也表明了你对自己的设计的基类很没有信心,基本没有什么东西是确定的。
总而言之,纯虚函数使得派生类只继承基类的接口;虚函数让派生类可以自我发挥,但是如果不想发挥,也可以提供默认版本;而非虚函数则要求你必须继承它,没有自由空间。
条款35:考虑Virtual函数以外的其他选择
我们考虑下面这个问题:假设我们在开发一款游戏,游戏中有不同的角色,每个角色有自己的生命值的初始值,生命值的计算方法等等。你会怎么设计这个类呢?我们很自然的就会想到:C++ Code
1 2 3 4 5 | class GameCharacter { public: virtual int healthValue()const; }; |
假如生命值的计算分为如下几步:
1.获得生命值。
2.通过一个函数计算生命值。
3.将生命值返回。
那么每个派生类的healthValue函数都需要完成这几步,我们能不能重构这个代码呢?先看一下重构的结果:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | class GameCharacter { public: int healthValue() { int val = getInitialVal(); val = calcVal(val); return val; } protected: virtual int getInitialVal() = 0; virtual int calcVal(int ) = 0; }; class Soldier: public GameCharacter { private: int getInitialVal(); int calcVal(int ); }; class Patient: public GameCharacter { private: int getInitialVal(); int calcVal(int ); }; //战士的初始生命值较高 int Soldier::getInitialVal() { return 50; } //但是生命值会减半 int Soldier::calcVal(int val) { return val = val / 2; } //病人的声明值较低 int Patient::getInitialVal() { return 10; } //但是生命值会翻倍 int Patient::calcVal(int val) { return val = val * 2; } |
1.它在基类中声明了2个不会被继承的虚函数getInitialVal()和calcVal(int val)。但是在基类的可以被继承的(且不希望被修改的)healthValue函数中调用了。
2.在派生类中定义了healthValue所要调用的函数。
他这样做的好处是:在基类中,限定了先做什么,后做什么。但是具体怎样做,把权力移交给了派生类。
这种思路,称为模板方法模式,它的定义为:定义一个操作中的算法的骨架,而将一些方法实现延迟到子类。模板方法使得子类可以不改变一个算法的结构即可以重定义该算法的某些特定步骤。
但是这样做其实并不灵活,假如我希望同一个类型的不同对象有不同的计算生命值的方法,就麻烦了。换个角度思考,人物健康指数的计算,其实,不一定与人物的特定类型有关,对于同一个类型,也可以有不同的计算方法。由此我们想到,不能让每个类型的声明计算与一个函数相关,而对于不同的对象,可以调用不同的函数来完成这件事。依照这个思路,我们可以这么写:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | //人物健康指数的计算与人物类型无关 //要求每个人物的构造函数接受一个指针,指向一个健康计算函数 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); } virtual int getInitHealth()const = 0; private: HealthCalcFunc healthFunc; }; class Soldier: public GameCharacter { public: explicit Soldier(HealthCalcFunc hcf = defaultHealthCalc): GameCharacter(hcf) {} int getInitHealth()const; }; class Patient: public GameCharacter { public: explicit Patient(HealthCalcFunc hcf = defaultHealthCalc): GameCharacter(hcf) {} int getInitHealth()const; }; int loseHealthQuickly(const GameCharacter &); int loseHealthSlowly(const GameCharacter &); int recoverHealth(const GameCharacter &); int Soldier::getInitHealth()const { return 50; } int Patient::getInitHealth()const { return 10; } int defaultHealthCalc(const GameCharacter &gc) { int health = gc.getInitHealth(); health = health / 2; return health; } int loseHealthQuickly(const GameCharacter &gc) { int health = gc.getInitHealth(); health = health / 4; return health; } int loseHealthSlowly(const GameCharacter &gc) { int health = gc.getInitHealth(); health = health / 1.6; return health; } int recoverHealth(const GameCharacter &gc) { int health = gc.getInitHealth(); health = health * 2; return health; } |
C++ Code
1 2 3 4 5 6 7 8 9 10 11 | int main() { Soldier s1; cout << loseHealthQuickly(s1) << endl; Soldier s2(recoverHealth); cout << recoverHealth(s2) << endl; Patient p1; cout << loseHealthSlowly(p1) << endl; return 0; } |
这个例子一下子拓宽了我们的眼界:为什么非得使用某个函数去计算生命值,能不能使用某个类似函数的,可以被调用的东西来计算呢?比如函数对象、类的成员函数等等。通过tr1中的function可以帮你完成这样的设想:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | //.h //前置声明 class GameCharacter; int defaultHealthCalc(const GameCharacter &gc); class GameCharacter { public: //std::tr1::function相当于一个泛化的函数指针 typedef std::tr1::function<int (const GameCharacter &)> HealthCalcFunc; explicit GameCharacter( HealthCalcFunc hcf = defaultHealthCalc ): healthFunc(hcf) {} int healthVaule()const { return healthFunc(*this); } virtual int getInitHealth()const = 0; private: HealthCalcFunc healthFunc; }; class Soldier: public GameCharacter { public: explicit Soldier(HealthCalcFunc hcf = defaultHealthCalc): GameCharacter(hcf) {} int getInitHealth()const; }; //可以采取以下3种措施调用计算健康值的函数 //计算健康值的函数,其返回类型为short short HalfHealth(const GameCharacter &); //计算健康值的函数对象 struct AddHealth { int operator()(const GameCharacter &gc)const { int health = gc.getInitHealth(); health = health + 10; cout << "生命值加10" << endl; return health; } }; //类成员函数 class GameLevel { public: float hard(const GameCharacter &)const; float easy(const GameCharacter &)const; }; //cpp int Soldier::getInitHealth()const { return 50; } int defaultHealthCalc(const GameCharacter &gc) { int health = gc.getInitHealth(); health = health * 2; cout << "默认计算生命值,为初始值的2倍" << endl; return health; } short HalfHealth(const GameCharacter &gc) { short health = gc.getInitHealth(); health = health / 2; cout << "生命值减半" << endl; return health; } float GameLevel::hard(const GameCharacter &gc)const { float health = gc.getInitHealth(); cout << "困难模式,生命值为初始值的四分之一" << endl; return health / 4; } float GameLevel::easy(const GameCharacter &gc)const { float health = gc.getInitHealth(); cout << "简单模式,生命值为初始值的四倍" << endl; return health * 4; } //main int main() { //调用默认函数生命值翻倍 Soldier s1; cout << s1.healthVaule() << endl; //生命值减半 Soldier s2(HalfHealth); cout << s2.healthVaule() << endl; //生命值加10 AddHealth add; Soldier s3(add); cout << s3.healthVaule() << endl; GameLevel level; //对非静态成员函数,需要通过bind绑定 //easy函数有一个参数,所以需要一个占位符 Soldier s4(std::tr1::bind(&GameLevel::easy, level, std::tr1::placeholders::_1)); cout << s4.healthVaule() << endl; Soldier s5(std::tr1::bind(&GameLevel::hard, level, std::tr1::placeholders::_1)); cout << s5.healthVaule() << endl; return 0; } |
对于普通函数,和函数对象,可以直接用来给HealthCalcFunc赋值。对非静态成员函数,需要通过bind绑定:为了计算s4的健康函数,需要使用GameLevel里面的easy函数,这个函数实际上有两个参数:*this(GameLevel类型)和GameCharacter&,HealthCalcFunc只接受一个参数:GameCharacter&。需要将GameLevel类型中的easy函数与调用它的对象绑定起来,此后每次调用easy函数,都是调用绑定的那个对象(level)的这个函数。其中_1是占位符,表示的是这个函数的第一个参数。
经过上一个想法的洗礼,尤其是调用类成员函数,使我们不禁想到了为什么不把所有的计算生命函数设成一个基类,然后从中派生出各个子类方法,然后让GameCharacter调用这些方法呢?下面是实现的程序:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | //healthCalcFunc.h //前置声明 class GameCharacter; class HealthCalcFunc { public: virtual int calc(const GameCharacter &gc)const; }; //计算生命值方法派生类 class AddHealth: public HealthCalcFunc { public: int calc(const GameCharacter &gc)const; }; //计算生命值方法派生类 class DoubleHealth: public HealthCalcFunc { public: int calc(const GameCharacter &gc)const; }; //头文件中声明 extern HealthCalcFunc defaultHealthCalc; //healthCalcFunc.cpp #include "healthCalcFunc.h" #include "gameCharacter.h" int HealthCalcFunc::calc(const GameCharacter &gc)const { cout << "返回原始生命值" << endl; return gc.getInitHealth(); } int AddHealth::calc(const GameCharacter &gc)const { cout << "生命值+10" << endl; return gc.getInitHealth() + 10; } int DoubleHealth::calc(const GameCharacter &gc)const { cout << "生命值翻倍" << endl; return gc.getInitHealth() * 2; } //源文件中定义 HealthCalcFunc defaultHealthCalc; //gameCharacter.h #include "healthCalcFunc.h" class GameCharacter { public: explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc): pHealthCalc(phcf) {} int healthVaule()const; int getInitHealth()const ; private: HealthCalcFunc *pHealthCalc; }; //gameCharacter.cpp #include "gameCharacter.h" int GameCharacter::healthVaule()const { return pHealthCalc->calc(*this); } int GameCharacter::getInitHealth()const { return 50; } //main.cpp int main() { GameCharacter gc0; cout << gc0.healthVaule() << endl; GameCharacter gc1(&AddHealth()); cout << gc1.healthVaule() << endl; GameCharacter gc2(&DoubleHealth()); cout << gc2.healthVaule() << endl; return 0; } |
这个方法称为strategy模式,它的定义如下:Strategy模式定义了一系列的算法,将它们每一个进行封装,并使它们可以相互交换。Strategy模式使得算法不依赖于使用它的客户端。
这个条款略微有些长,但总结起来,无非是这样对于虚函数,有如下几种替代方案:
1.模版方法模式。在类中确定派生类要做的事情的顺序,然后让派生类自己实现它们。
2.使用函数指针。将虚函数移到类的外部,但是它不能访问类的private成分。
3.使用tr1::function指定“泛型”函数指针,是得我们可以通过函数、函数对象、成员函数来替代虚函数。
4.使用策略模式,将虚函数所要完成的事情封装成类,以便于扩展。
条款36:绝不重新定义继承而来的non-Virtual函数
先看一个例子:C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Base { public: void func() { cout << "base function" << endl; } }; class Drived : public Base { public: void func() { cout << "drived function" << endl; } }; int main() { Drived d; Base *pb = &d; pb->func(); Drived *pd = &d; pd->func(); return 0; } |
造成这个原因是,non-virtual函数如B::mf和D::mf都是静态绑定的。
但是在另一方面,Virtual函数确定动态绑定的,所以他们不受这个问题之苦,如果mf是个Virtual函数,不论是通过pB或者pD去调用mf 都会导致调用D::mf,因为pB和pD真正指的都是一个类型为D的对象。
此时,我们之前提过的public继承是一种is-a关系:
对于一件事情如果Base能做,那么Drived也能做。但是当你重新定义了派生类的非虚函数时,那么在使用指针或者引用时,从直观上看,派生类做的事情就可以与基类做的大不相同。
总之,绝不重定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参数值
我们先上代码吧:C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include<iostream> using namespace std; class Base { public: virtual int getVal(int i = 0) { cout << "基类函数" << endl; return i; } }; class Derived: public Base { public: int getVal(int i = 1) { cout << "派生类函数" << endl; return i; } }; int main() { Derived d; Base *pb = &d; cout << pb->getVal() << endl; system("pause"); return 0; } |
有没有觉得很奇怪,根据动态绑定的原则,这里的确调用的是派生类的getVal函数,可是为什么会输出0呢?如果把这里的指针换成引用,也会得出相同的结果。原因出在:virtual函数是动态绑定的,但是函数的默认参数却是静态绑定的!所以当编译器看到pb是Base*这个类型时,在调用函数时,就会选择基类的默认参数。
这是多么容易出错的一件事啊!调用派生类的函数,但是他的参数的默认值却是基类提供的。但编译器也有自己的苦衷:运行期效率。
那么我们似乎应该将派生类的默认参数改成与基类相同,但是这又带来其他的问题,其中最严重的是:如果基类的默认参数需要修改,那你不得不修改所有派生类的默认参数!
一种比较好的替代方案是前面介绍的NVI(non-virtual interface)手法:
做法规则:
令base class内的一个public non-virtual 函数调用private Virtual函数,后者可被derived classes 重新定义。这里我们可以让non-virtual函数指定缺省参数,而private Virtual函数负责真正的工作:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Base { public: int getVal(int i = 0) { doGetVal(i); return i; } private: virtual int doGetVal(int i) { cout << "基类函数" << endl; return i; } }; class Derived: public Base { private: virtual int doGetVal(int i) { cout << "派生类函数" << endl; return i; } }; |
总之,virtual函数是动态绑定,但是函数的默认参数是静态绑定的。所以绝不要重新定义派生类的函数的默认参数值,而且最好使用NVI手法。
条款38:通过复合塑模出has-a或“根据某物实现出”
不要被这个诡异的条款名字欺骗了,他说的是一件简单的事情。比如,定义一个“人”类,而人又有地址,那么你应该这么写:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Address { public: string country; string province; string city; }; class Person { string name; Address address; }; |
下面就用这个例子来说明:
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | template<typename T> class Set { public: bool member(const T &item)const { return find(rep.begin(), rep.end(), item) != rep.end(); } void insert(const T &item) { if(!member(item)) rep.push_back(item); } void remove(const T &item) { typename list<T>::iterator it = find(rep.begin(), rep.end(), item); if(it != rep.end()) rep.erase(it); } size_t size()const { return rep.size(); } void print() { list<T>::iterator it = rep.begin(); for(; it != rep.end(); ++it) cout << *it << "\t" << endl; } private: list<T> rep; }; |
总之,复合与public继承完全不同,它意味着has-a或者通过XX实现。
条款39:明智而审慎地使用private继承
先说private继承的特点:1.也就是说,编译器不会讲一个private继承而来的派生类对象转化为一个基类对象。这意味着,priavte继承不再是is-a关系:C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Person { protected: string name; }; class Student: private Person { private: string schoolNumber; }; void eat(const Person &p) { cout << "eat" << endl; } void study(const Student &s) { cout << "study" << endl; } int main() { Person p1; eat(p1); Student s1; study(s1); // eat(s1);错误 return 0; } |
因此,private继承意味着:根据某物实现。这与前面条款介绍的复合很类似。在大多数时候,我们应该使用复合,而不是private继承来实现这种功能。但是当有protected成员和虚函数牵扯进来的时候,我们又不得不用private继承。
其次,与复合相比,private可以使空基类的最优化。先看一个例子:
C++ Code
1 2 3 4 5 6 7 8 9 10 | //定义一个空基类 class Empty {}; class HoldsAnInt { private: int x; Empty e;//复合 }; |
C++ Code
1 2 3 4 5 6 7 8 9 | //定义一个空基类 class Empty {}; class HoldsAnInt: private Empty { private: int x; }; |
总之,private继承意味着根据某物实现。当派生类需要访问基类的的受保护成员或者重新定义虚函数时,我们才使用它。而且private继承可以是得空基类最优化,如果在开发中需要是得对象尺寸最小,那么也用得着它。
条款40:明智而审慎地使用多重继承
多重继承往往会导致二义性:C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | class Base1 { public: void func() { cout << "base1 func" << endl; } }; class Base2 { private: bool func() { cout << "base2 func" << endl; return true; } }; class Derived: public Base1, public Base2 { }; int main() { Derived d; //d.func();二义性 return 0; } |
这还不是最可怕的情况,最可怕的是出现“钻石型多重继承”:B和C继承自A,而且D继承自B和C。此时,理论上讲,D中会有两份A的public成员(这里假定是public继承),但实际上,大多数时候,我们只希望有一份,此时只能通过虚继承来避免这种现象。
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class Base { public: void print() { cout << "base" << endl; } }; class Middle1: public Base { }; class Middle2: public Base { }; class Derived: public Middle1, public Middle2 { }; |
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class Base { public: void print() { cout << "base" << endl; } }; class Middle1: public virtual Base { }; class Middle2: public virtual Base { }; class Derived: public Middle1, public Middle2 { }; |
因此,不到万不得以,不要使用虚基类,在使用时,尽量避免在其中放置数据,因为它的初始化规则跟一般情况不同。
然后书中给出了一个使用的多重继承的例子:有一个接口类,我们需要实现它;恰巧我们有一个现成的类,这个类的功能跟我们要实现的类相似,但是我们需要对它的虚函数进行重写,已达到我们希望的功能,于是我们public继承了这接口,又private继承了它的实现,并对其中的某些实现重定义了。这时候,多重继承真的就派上了用场。
总的来说,多重继承可能会引起很多问题:二义性、构造、析构函数的顺序,类的大小、访问速度等等。但有时的确也会使用到它。
相关文章推荐
- 第六章 继承和面向对象设计(35-36)
- 第六章 继承和面向对象设计 (Effective C++ Second Edition 读书笔记)
- 第六章 继承和面向对象设计(37-42)
- Effective C++ 第六章--继承与面向对象设计笔记
- 第六章 继承和面向对象设计(43-44)
- Effective C++学习有感--第六章 继承与面向对象设计(二)
- Effective C++第六章-继承与面向对象设计
- 《Entity Framework 6 Recipes》中文翻译系列 (31) ------ 第六章 继承与建模高级应用之自引用关联
- Effective C++读书笔记(十)继承与面向对象设计部分(上)
- Effective C++读书笔记(十一)继承与面向对象设计部分(下)
- 除了封装,继承,多态 您还知道那些?-面向对象设计的金字塔
- Ext的面向对象设计(如何建立一个继承Ext控件的"类")
- Head First C# 中文版 图文皆译 第六章 继承 page221
- Head First C# 中文版 图文皆译 第六章 继承 page246
- Effective C++ 3nd 读书摘要(六、继承与面向对象设计)Item32 - 40
- Effective C++读书笔记(十一)继承与面向对象设计部分(下)
- PHP面向对象设计模式-8.1继承和组合对比-代码实现
- Effective C++ 6.继承与面向对象设计
- Effective C++笔记(六):继承与面向对象设计
- Head First C# 中文版 图文皆译 第六章 继承 page233