Effective C++ 笔记四

这一章探讨的主题是 C++ 的继承和面向对象设计.
C++ 的 OOP 有可能和你原本习惯的 OOP 稍有不同.
这一章只要回答了以下的一些问题:

  • “继承”可以是单一继承或多重继承,每一个继承关系该用 public、private、protected?
  • 成员函数是 virtual 、 non-virtual 还是 pure virtual?
  • 缺省参数值 与 virtual 函数有什么交互影响?
  • 继承如何影响 C++ 的名称查找规则?
  • 设计选项有哪些?
  • 如果class 的行为需要修改, vritual 函数是最佳选择吗?

32. 确定你的 public 继承是 “is-a” 关系

牢记,public 继承是 一种"is-a" 关系, 适用于 base classes 身上的每一件事情也一定适用于 派生类上,因为每一个派生类都是一个 base class 对象

你可能会根据直觉写出这样的代码:

1
2
3
4
5
6
7
8
class Brid {
public:
virtual void fly();
}
//鸟会飞,不是很正常吗? 直到你遇见一只企鹅
class Penguin : public Brid {

}

企鹅并不会飞,但是你必须实现 fly 这个函数,因为Brid 这个基类中fly 被声明为 virtual。下面的继承关系,似乎更接近事实:

1
2
3
4
5
6
7
8
9
10
class Brid {
//没有 fly 函数
}
class FlyingBird : public Bird {
public:
virtual void fly();
}
class Penguin : public Brid {
//不声明 fly 函数,这样如果尝试调用Penguin 的fly(),那么会在编译环境就报错
}

33. avoid hiding inherited names

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
private :
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void f3();
void mf3(int);
};
class Derived : public Base {
public:
virtual void mf1();
void mf3();
void mf4();
};

这里,编译器先查找 local 作用域,接着是 Derived class 作用域, Base 作用域
在上面的代码中, Base 类内所有名为 mf1 和mf3 函数都被遮掩掉了. 从名称查找观点来看, Base::mf1 和 Base:: mf3 不在被 Derived 继承.

1
2
3
4
5
6
7
Derived d;
int x;
d.mf1();
d.mf1(x); //错误,因为 Derived::mf1 掩盖了 Base::mf1
d.mf2();
d.mf3();
d.mf3(x); //错误,因为 Derived::mf3 掩盖了 Base::mf3
这是因为 C++ 对"继承而来的名称"的缺省遮掩行为. 这是为了防止你在程序库或应用框架内建立新的 derived class 时附带从疏远的 base class 继承重载函数.
要避免出现这样的问题也很简单,我们只需要借助 using 声明式就可以了.
1
2
3
4
5
6
7
8
class Derived : public Base {
public:
using Base::mf1; //声明在 public 中,因为是public 继承(is-a关系),所以我们要继承 Base 的所有函数(Base 中public 的,Derived 也应该是 public 的)
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};

不过,在 private 继承下,using 却在显现不出身手,我们需要另一种策略,即一个简单的转交函数(forworading function)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base {
public:
virtual void mf1() = 0;
virtual void mf1(int);
//与前面相同
};
class Derived : public Base {
public:
virtual void mf1() //forworading function
{ Base:: mf1();}
};
Derived d;
int x;
d.mf1();
d.mf1(x); //错误,因为 Base::mf1被 掩盖了

34. 区分接口继承和实现继承

身为 classes 的设计者, 你不断地在”继承接口” 还是 “继承实现”之间做出选择.
这里我们需要按照这样的行为规范做事:

在 public 继承下, derived class 总是继承 base class的接口
声明一个 pure virtual 函数的目的是为了让 derived classes 只继承函数接口.
声明一个 impure virtual 函数的目的,是让 derived classes 继承该函数的接口和缺省实现.
声明 non-virtual 函数的目的是为了令 derived classes 继承函数的接口及一份强制性实现

35. 考虑 virtual 函数以外的其他选择

1
2
3
4
5
class GameCharacter {
public:
virtual int health() const;
// 派生类必须重新定义它
}

有时考虑考虑 其他替代方案来 替换 virtual ,(更恰当的来说是跳出 面向对象设计) 能帮助我们写出更好的代码.

STL 就是一个很典型的例子

(1) 用 non-Virtual Interface ( NVI)手法实现 Template Method 模式

令客户通过 public non-virtual 成员函数间接调用 private virtual 函数. 这样 “调用virtual 函数之前和之后我们可以做一些准备工作”.
例外,NVI 手法允许派生类重新定义 virtual 函数,从而赋予它们 “如何实现”的能力,但让 base 类保留何时调用的权利.

1
2
3
4
5
6
7
8
9
10
11
12
13
class GameCharacter {
public:
int healthValue() const {
//事前工作
int retVal = doHealthValue();
//事后工作
return retVal;
}
private:
virtual int health() const{
//缺省算法,派生类可以重新定义它
}
}

NVI 下 virtual 不一定是 private,也可能是 public,但不能是 public.

(2) 由 Function Pointers 实现 Strategy 模式

NVI 手法对 public virtual 函数而言是一个有趣的替代方案,不过看起来有点像”花瓶”华而不实? 毕竟我们还是要使用 virtual 函数来实现.
下面的例子是 常见的 Strategy 设计模式的简单使用,通过 函数指针的方式实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GameCharacter; //前置声明

int defalutHealthCalc(const GameCharacter& gc);

class GameCharacter {
public:
typedef int (*HealthCalFunc) (const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defalutHealthCalc) : healthFunc(hcf)
{}
int health() const
{ return healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
// 派生类必须重新定义它
}

(3) 借用 tr1:: function 完成 Stra

(4) 标准的 Strategy 模式

标准的Strategy 模式更容易被辨认出来,而且通过 继承 HealthCalcFunc 就能够加入一个新的算法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class  GameCharacter;
class HealthCalcFunc {
public:
virtual int calc(const GameCharacter& gc) const {
....
}
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) : pHealthCalc(phcf)
{}
int healthValue() const
{return pHealthCalc->calc(*this);}
private:
HealthCalcFunc* pHealthCalc;
}

36. 不重新定义继承而来的 non-virtual 函数

non-virual 函数都是静态绑定,而 virtual 是动态绑定.
如果在派生类中重新定义基类的 non-virtual, 那么除非你明确的指出,否则你将调用的总是派生类版本,而不是基类版本. public 继承指出了 派生类 is-a base 类, 覆盖 base 类的 non-virtual 毫无疑问违反了这一点.如果你想派生类能够改变函数的实现方式,那么你应该将基类中的这个函数定义为 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
class Shape {
public:
enume ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle : public Shape {
public:
// bad,赋予了新的省缺值
virtual void draw(ShapeColor color = Green) const;
};
class Circle : public Shape {
public:
virtual void draw(ShapeColor color) const;
}
// 静态绑定
Shape* ps; //静态类型是 Shape*
Shape* pc = new Circle; //静态类型是 Shape*
Shape* pr = new Rectangle; //静态类型是 Shape*
// 动态绑定
ps = pc; //ps 的类型如今是 Circle*
ps = pr; //ps 的类型如今是 Rectangle*
pc->draw(Shape::Red) // 调用 Circle 中的方法
pr->draw(Shape::Red) // 调用 Rectangle 中的版本
//Rectangle 中的方法

但是当你调用一个定义于 派生类的 virtual 函数的同时,却使用的是 base class 中指定的省缺值.

1
pr->draw(); //pr的类型是 Shape* ,调用 Rectangle::draw(Shape::Red)

我们不该重新定义继承而来的缺省数值,所以你可能会妥协以下,写出下面糟糕的代码

1
2
3
4
5
6
7
8
9
10
11
// 如果你想改变 Shape 内的省缺参数,你不得不也修改 Rectangle 中的代码
class Shape {
public:
enume ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle : public Shape {
public:
// bad,赋予了新的省缺值
virtual void draw(ShapeColor color = Red) const;
};

当virtual 函数实现出现问题时, 聪明的做法是考虑替代方案, 条款35指出 virtual 函数的替代设计.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape {
public:
enum ShapeColor { Red, Green, Blue};
void draw(ShapeColor color = Red) const {
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle : public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const;
}

38. 用复合关系表示has-a 关系或 根据某物实现出

条款 13 中指出, public 继承是一种 “is-a”关系,而复合关系表示一种 has-a 或 is-implemented-in-terms-of 关系.
在 STL 中, 很多容器是通常 复合(neiqian)另一种容器来实现的,现在你想通过继承关系来看看是否能够实现同样的功能.

1
2
3
4
5
//你想复用 std::list 来实现 STL 中的 Set
template <typename T>
class Set: public std::list<T>{
//...
};

看起来不错是吗? 实际上有些东西完全错误了. 我们反复的提到了”public 继承是一种’is-a’关系”,那么对于 std::list 为真的事情,对于Set 也应该为真.即: “Set 是一种 std::list”.
额,你也发现不对劲了吧. Set 并不是一种 std::list .

复用的意思和 public 继承完全不同.

39. 明智而审慎的使用 private 继承

private 继承并不意味 is-a 关系,它意味着 is-implemented-in-terms-of(根据某物实现出).
- 如果 classes 的关系是 private, 编译器不会自动将一个 derived class 对象转化成 一个 base 对象. - 由 private base 类继承而来的所有成员,不管是 protected 或 public 属性, 在derived class 中都会变成 private 属性.
1
2
3
4
5
6
7
8
9
10
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
};
// public 在这里并不是一个好主意,因为 Widget 并不是 一种 Timer,更何况 public 会暴露出 Timer的接口
class Widget : private Timer {
private:
virtual void onTick() const;
};

上面的设计还可以使用 public 继承加复用的方式实现.

1
2
3
4
5
6
7
8
class Widget {
private:
class WidgetTimer : public Timer {
public:
virtual void onTick() const;
}
WidgetTimer timer;
}

这样更复杂了,但是更加合理. 这表明出 private 比 复用的级别更低,我们要优先选择复用.但是对于一些特殊的情景, private 可能更适用.

1
2
3
4
class A {
protected:
void func();
};

使用复合可调用不了 func 函数,但是 private 继承却可以调用.另外 当面对 empty base 最优化时,你也应该z选择private 继承.