继承与面向对象设计
这个主题内容比较多,在进入下一个主题前需要一篇新的文章放下剩余的内容。
条款36:绝不重新定义继承而来的non-virtual函数
这个条款用个例子就能说明:
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
| class Base { public: void mf() { cout << "Base::mf()" << endl; } };
class Derived : public Base { public: void mf() { cout << "Derived::mf()" << endl; } };
void call(Base& base) { cout << "in call(): "; base.mf(); }
int main(int argc, char* argv[]) { Derived d;
Base* pb = &d;
Derived* pd = &d;
pb->mf();
pd->mf();
call(d);
return 0; }
|
编译运行,得到:
1 2 3
| Base::mf() Derived::mf() in call(): Base::mf()
|
也就是说调用哪个版本的函数是由当前持有对象的holder
(中文笔者不知道如何表达,代之指针或者普通变量)的类型决定。这个现象是可以通过C++对象模型
来解释的,不过重点是这样的设计不符合多态
的原则,容易造成误用。
其根源就是里面所有的表达式都是在编译期完成决议的,所有都是静态绑定
,以下是静态绑定的定义:
1
| 静态绑定是指在程序编译过程中,把函数(方法或者过程)调用与响应调用所需的代码结合的过程称之为静态绑定。
|
总之如果一个函数是non-virtual的,该类的子类就不要重新定义该函数,避免后续的错误。
条款37:绝不重新定义继承而来的缺省参数值
在开始该条款之前,读者应该了解:
这个条款要介绍的矛盾是由于缺省参数值
是静态绑定
而virtual函数
是动态绑定
引起的。用书上的例子:
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
| enum ShapeColor{RED, GREEN, BLUE};
class Shape { public:
virtual void draw(ShapeColor color = ShapeColor::RED) const { cout << "Calling Shape::draw(ShapeColor=";
switch(color) { case ShapeColor::RED : cout << "RED)" << endl;break; case ShapeColor::GREEN : cout << "GREEN)" << endl;break; case ShapeColor::BLUE : cout << "BLUE)" << endl;break; } } };
class Rectangle : public Shape { public: virtual void draw(ShapeColor color = GREEN) const { cout << "Calling Rectangle::draw(ShapeColor=";
switch(color) { case ShapeColor::RED : cout << "RED)" << endl;break; case ShapeColor::GREEN : cout << "GREEN)" << endl;break; case ShapeColor::BLUE : cout << "BLUE)" << endl;break; } } };
class Circle : public Shape { public: virtual void draw(ShapeColor color) const { cout << "Calling Circle::draw(ShapeColor=";
switch(color) { case ShapeColor::RED : cout << "RED)" << endl;break; case ShapeColor::GREEN : cout << "GREEN)" << endl;break; case ShapeColor::BLUE : cout << "BLUE)" << endl;break; } } };
int main(int argc, char* argv[]) { Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
pc->draw();
pr->draw();
ps = pc;
ps->draw();
ps = pr;
ps->draw();
Circle* cpc = new Circle;
cpc->draw(ShapeColor::BLUE);
Rectangle* rpr = new Rectangle;
rpr->draw();
return 0; }
|
运行得到:
1 2 3 4 5 6
| Calling Circle::draw(ShapeColor=RED) Calling Rectangle::draw(ShapeColor=RED) Calling Circle::draw(ShapeColor=RED) Calling Rectangle::draw(ShapeColor=RED) Calling Circle::draw(ShapeColor=BLUE) Calling Rectangle::draw(ShapeColor=GREEN)
|
这个现象中,所有的多态
表现正确。ps
、pc
、pr
三个指针的静态类型都是Shape*
,但是分别指向了Circle
和Rectangle
实例,上面的输出显示都根据其动态类型成功地调用了实际类型中的draw
函数。但是一方面又很奇怪,缺省参数值是根据静态类型
来决议的,在静态类型是Shape*
的情况下,所有的缺省参数值都采用了Shape::draw
的版本,不受实际类型的影响。甚至连Circle::draw
本身不具有缺省参数
也采用了父类的版本。
笔者原本以为会引起编译错误,但是仔细想了想也是合理的,首先静态编译期就根据变量类型决议了缺省参数
的版本,并不影响其运行时。
书上也给出了解决这个问题的技巧,这样的做法并不提倡,因而笔者就不给出方法了。遇到这样的问题更应该从设计上来解决,避免总是用技巧,总体原则就是不要在virtual函数
里面施加太多的约束,留有灵活性使得子类继承时方便修改。
条款38:通过复合塑模出has-a或者”根据某物实现出”
该条款并不是编程方面的问题,而是有关概念
以及设计
的问题,例如书上说的”人有一个住址”:
1 2 3 4 5 6 7
| class Person { public: ... private: Address address; };
|
而不是”人是一个住址”:
1 2
| class Person : public Address {...};
|
这个是编程人员对所需要实现的软件的认知引起的问题,当然这个例子是很简单的,书上既然给出了这个条款,那么也是提示读者要注重概念上的问题,否则因为这种与编程无关的事情增加了复杂度那就太浪费人力了。正如上面这个”人是一个住址”错误的示例,读者可以想想这样的设计,在把一个Person
实例持久化
到数据库的时候,Address表
和Person表
的关系,以及sql语句
的设计。
然后就是is-a
和is-implemented-in-terms-of
的区分问题。在编程里面,集合
和列表
是两种不同概念的数据结构,书上给的例子是复用列表
实现集合
:
1 2 3
| template<typename T> class Set : public std::list<T> {...};
|
这样基本上可不用写多少代码就实现了功能,但是集合
是不允许集合的重复的而列表
允许,也就是说产生了概念冲突。
但是如果集合
仅仅是借列表
来实现的话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| template<typename T> class Set { public: ... void insert(const T& item); ... private: std::list<T> container; };
... template<typename T> void Set<T>::insert(const T& item) { ... containter.insert(item); ... } ...
|
这样就解决了Set
不是list
这个概念上的问题,因为这个Set
是”根据list实现出”来的。
这个条款笔者认为没有太多技术上的东西,更多的是编程人员的基础概念认知的知识问题。
条款39:明智而审慎地使用private继承
该条款提到了:
笔者没有该条款的实践,毕竟private继承
在实际应用中太少了,少到几乎没有见到过。
private继承
意味implemented-in-terms-of
关系。
例如书上的例子:
1 2 3 4 5 6 7
| class Timer { public: explicit Timer(int tickFrequency); virtual void onTick() const; ... };
|
然后一个Widget
需要定时器功能,但是显然Widget
不是一个Timer
,需要表现出is-implemented-in-terms-of
关系才能符合常用逻辑,也就是Widget
的定时事件
功能是根据Time实现
的:
1 2 3 4 5 6
| class Widget : private Timer { private: virtual void onTick() const; ... };
|
这样看起来乖乖的,明明是一种继承
语法,却是表现不出is-a
关系设计的实现。这就是为何private继承罕见的原因了。也可以改进为一种更加符合阅读理解的表现形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Widget { public: ... private: class WidgetTimer : public Timer { public: virtual void onTick const; ... };
WidgetTimer timer; ... };
|
第二种做法还有用到的地方,起码这种实现方式容易让人理解。介绍这个条款并不是推广这种private继承的技巧,而是别无他法的时候才采纳这个实现方案。
条款40:明智而审慎地使用多重继承
多重继承(Multiple Inheritance,MI)是C++
其中一把很著名的双刃剑,一方面给C++
带来了丰富灵活的继承语法,另一方面又给C++
的继承带来混乱。其中为了解决这个混乱,Java
只允许单一继承
,解决了不少问题,而且引入了接口(interface)
关键字弥补灵活性的缺失。
首先:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Base1 { public: void f(); ... };
class Base2 { public: void f(); ... };
class Derived : public Base1, Base2 { ... };
|
这样的话,当发生:
就引起了歧义,引起编译错误。为了消除歧义,必须这样指定:
明确调用哪个版本。
接下来就是菱形继承问题
:
写段代码测试:
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 A { public: A() { cout << "A::A(" << this << ")" << endl; } };
class B : public A { public: B() { cout << "B::B(" << this << ")" << endl; } };
class C : public A { public: C() { cout << "C::C(" << this << ")" << endl; } };
class D : public B, public C { public: D() { cout << "D::D(" << this << ")" << endl; } };
|
实例化一个D
对象时,程序输出:
1 2 3 4 5
| A::A(0x7ffecb763bce) B::B(0x7ffecb763bce) A::A(0x7ffecb763bcf) C::C(0x7ffecb763bcf) D::D(0x7ffecb763bce)
|
也就是说一个D
实例中的有两份A
实例,一份是属于B
的A
以及一份属于C
的A
。
为了解决这样不统一的问题,可以用virtual继承
:
1 2 3 4 5 6 7 8 9 10 11
| class A {...};
class B : virtual public A {...};
class C : virtual public A {...};
class D : public B, public C {...};
|
这样实例化一个D
对象时,程序输出:
1 2 3 4
| A::A(0x7fffdd669580) B::B(0x7fffdd669580) C::C(0x7fffdd669588) D::D(0x7fffdd669580)
|
这样就能够处理好公共爷类
的问题了。考虑到继承树下面可能会有D
的子类也出现这样的情况,所有的继承应用virtual继承
防止,然而这样会使得实例占用的内存膨胀,也就是说virtual继承
最好不要滥用。
作者在书上给出的建议,笔者认为就是Java
的单一继承
和多实现
。也就是说约束继承语法使得程序只有单一继承
原则,而灵活性由接口
的多继承来补充,关于接口
笔者在条款34中介绍过。
具体的做法,读者们可以去写写Java
的继承
和接口
,就能总结出在C++
中怎么写了。