Effective C++ 阅读笔记(六)

32:确保public继承符合is-a关系

public 继承意味着is-a。 适用于base class上的每一件事也一定适用于 derived classes,因为每一个 derived class 对象也都是一个 base class 对象。

Public inheritance (公开继承) 意味着 “is-a“ 关系。

如果 class D(Derrived) 以 public 形式继承 class B (Base),便是说每一个类型为 D 的对象同时也是一个类型为 B 的对象,也就意味着:

  • 每个 D 对象同时也是一个类型为 B 的对象,但是类型为 B 的对象不是类型为 D 的对象。
  • B 对象更加一般,而 D 对象更加特殊。
  • B 对象任何可以派的上用处的地方, D 对象一定可以;而 D 对象能处理的问题,B 对象不一定可以处理。

比如每个学生都是人,但是不是所有人都是学生。
有时是不是is-a关系需要仔细考虑。比如鸟可以飞,但企鹅也是一种鸟,但是企鹅不会飞。

一种想法是,在鸟的基础上,拓展出来一个新的类,用来表示会飞的鸟;而另一种想法则是,在基类中定义一个关于分的虚函数fly,如果不会飞的鸟,则重新定义fly,使其产生一个运行时的错误。
换句话说,这里更像是说,”企鹅会费,但是这么尝试是一种错误“。一个好的接口可以防止无效的代码防止编译,所以运行时才能侦测他们是一个不好的设计。

33:避免遮掩继承而来的名称问题

Derived class 内的名称会遮掩 base classes 中的名称。public 继承下没人希望如此。
可以使用 using 或者转交函数来使用被遮掩的函数

类的继承的作用域和一般程序块中的作用域类似,编译器首先从 Derived class 作用域中寻找某个函数或者参数,如果没有则去 base 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:
	virtual void mf1();
	void mf3();
	void mf4();
}

那么这时候如下的调用是无效的:

Derived d;
int x;
d.mf1(x);
d.mf3(x);

因为 Derived class 中的 mf1 和 mf3 遮盖了 base class 中的同名函数,也就是编译器在小作用域中找到了同名的变量或者函数就不会再继续寻找。

如果想继续使用,可以使用如下的方法:

class Derived: public Base {
public:
	using Base::mf1;
	using Base::mf3;
	virtual void mf1();
	void mf3();
	void mf4();
}

这是,上面两个调用都是可用的。

在 public 继承下,默认要继承 base class 所有函数,但是在 private 继承下,我们并不希望继承所有函数。
如果想使用 base clas 中的无参数 mf1 版本,那么使用 using 并不何时,因为他会让所有同名函数在 derived class 都看见。
这时候我们只需要利用 转交函数(forwarding function)

class Base {
public:
	virtual void mf1() = 0;
	virtual void mf1(int);
	...
}
class Derived : private Base {
public:
	virtual void mf1() { Base::mf1(); } // 转交函数
	...
}

这时调用(Derived) d.mf1(12)是错误的,因为mf1(int)被遮掩了。

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

  • public继承下, derived classses 总是继承 base classes 的接口
  • pure virtual 函数只具体指定接口继承
  • impure vritual 函数具体指定接口继承以及缺省实现继承
  • non-virtual 函数具体指定接口继承以及强制性实现继承

成员函数的接口总是会被继承。public 意味着 is-a 关系,所以对 base class 为真的任何事情一定对 derived class 为真。

声明 pure virtual 函数的目的是为了让 derived class 只继承接口函数。
但是,我们可以为 pure virtual 函数提供一份代码实现,但调用它的唯一方法是,调用的时候指定其 class 名称。
也就是说,如果有:

class A {
	virtual void func() = 0;
	...
}

void A::func() {
	//缺省行为
}

那么调用时需要指明 class 名称,A::func()

如果想为某个 pure virtual 函数提供一个默认行为,可以新建一个 protected 的 default 函数;或者利用缺省实现的方式。

声明为 non-virtual 函数,则说明所有的 derived class 都不应该尝试改变其行为。

35:考虑 virtual 函数之外的其他选择

  • NVI手法: Template Method 的一种形式,以 public non-virtual 成员函数包裹较低访问性的 virtual 函数
  • virtual 函数替换成函数指针成员变量或者std::function
  • 继承体系中的 virtual 函数替换成另一个继承体系中的 virtual 函数
  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将某个功能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无访问class的non-public成员。
  • std::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

考虑在写一个rpg游戏,人会因为各种原因收到伤害而降低健康值。所以决定提供一个成员函数 healthValue,它会返回一个整数,表示任务健康程度。

一种方法是将 healthValue 声明为 virtual,但还有一些其他替代方案。

Non-Virutal Interface 方法实现 Template Method

一种观点认为,一个较好的设计是将 healthValue 作为 public non-virtual 函数,并调用一个 private virtual 函数继续实际工作。

class GameCharacter {
public:
	int healthValue() const { // derived class 不可重新定义它
		... // 事前工作
		int retVal = doHealthValue(); // 做真正的工作
		... //事后工作
		return value;
	}
private:
	virtual int doHealthValue() const { ... } //  derived class 可以重新定义它
}

这种方式,让用户使用 public non-virtual 成员函数间接调用 private virtual 函数,被称为 non-virtual interface (NIV)手法。他是 Template method 设计模式的一个独特表现形式。
一般把 non-virtual 函数(healthValue)称为 virtual 函数的 wrapper。

优点

  • 可以在函数调用前做一些处理,如设定场景,清理场景,而不需要用户操作。
    • 事前工作:锁定互斥器(locking a mutex),制造运转日志记录项(log entry),验证 class 约束条件,验证函数先决条件等等。
    • 事后工作: 互斥器接触锁定,验证时候条件等

缺点:

  • 需要在 derived class 中重新定义函数

NIV 中没必要让 virtual 函数一定得是 private,可以根据情况来选择。

Function Pointers 实现 Strategy 模式

另一种想法是“任务健康指数的计算与任务类型无关“,这样我们可以要求每个任务的构造函数接受一个指针指向一个健康计算函数,然后调用该函数进行实际计算。

class GameCharacter; // 前置声明 (forward declaration)
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
	typedef int (*HealthCalcFunc) (const GameCharacter&);
	explict GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
	int healthValue() const { return healthFuc(*this); }
private:
	HealthCalcFunc healthFunc;
};

PS. 这里使用了typedef的比较复杂的用法

通过这种方式可以让不同人拥有不同健康计算函数,同时可以在运行中动态替换,只要指定不同函数即可。

不过,”健康指数不是GameCharacter继承体系内的成员函数“表明这些函数并没有特别访问即将被计算健康指数的对象的内部成分。
如果都是能根据 public 接口得来的信息计算出来,则没有任何问题。否则,可能需要弱化 class 的封装,比如声明 non-member 函数为 friends, 或者为其一部分提供 public 访问函数。

由 std::function 完成 Strategy 模式

不一定非得用函数,而是用某种“像函数的东西”(例如函数对象)。

std::function对象可以持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象或成员函数指针),只要其签名式兼容。比如参数类型或者返回值类型可以隐式转换。

class GameCharacter;                                 // 如前
int defaultHealthCalc(const GameCharacter& gc);      // 如前
class GameCharacter {
public:
	// HealthCalcFunc可以是任何“可调用物”(callable entity),可被调用并接受
	// 任何兼容于GameCharacter之物,返回任何兼容于int的东西。
	typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf)
	{}
	int healthValue() const               
	{ return healthFunc(*this); }
	...
private:
	HealthCalcFunc healthFunc;
};

使用function相当于一个指向函数的泛化指针,可以带来更多弹性。

short calcHealth(const GameCharacter&); //健康计算函数,返回类型non-int

struct HealthCalculator {
	int operator() (const GameCharacter&) const; 
};

class GameLevel {
public:
	float health(const GameCharacter&)) const;
	...
};

class EvilBadGuy: public GmaeCharacter { ... };
class EyeCandyGuy: public GmaeCharacter { ... };

EvilBadGay ebg1(calcHealth); //以函数计算健康指数

EyeCandyCharacter ecc1(HealthCalculator()); //以成员含糊嵇康健康指数

GameLevel currentLevel;
EvilBadGay ebg2(std::bind(&GameLevel::health, currentLevel, _1));

这里 GameLevel class 中的 health 函数表面上看只接受一个参数const GameCharacter&,但实际上还有一个隐含的参数,即它自己,也就是*this所指向的对象。
这个例子中,std::bind 实现了用 currentLevel 作为 *this 所指的对象进行计算(_1参数)。

可以看到,使用 std::function 可以计算健康指数时使用任何兼容的可调用物。

古典 Strategy 模式

古典 Strategy 会将健康计算函数作为另一个继承体系中的 virtual 成员函数,类似:
在这里插入图片描述
图中是说,EvilBadGay 和 EyeCandyCharacter 是 GameCharacter 的 derived classes。 GameCharacter 中有一个指向 HealthCalcFunc 继承体系中对象的指针。

class GameCharacter;                // 前置声明(forward declaration)
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-virtual 函数

假设 class D 继承 class B

  • 若重新定义 non-virtual函数,则不符合“每一个D都是B”
  • 如果真的需要不同的函数,那么应该把他声明为 virtual 函数

37:绝不重新定义继承而来的缺省参数值

不要重新定义继承而来的缺省参数,因为缺省参数都是静态绑定,virtual 函数却是动态绑定。而实际应该只复写 virtual 函数。

virtual 函数是动态绑定(dynamically bound)的,而缺省参数值是静态绑定(statcially bound)的。

所谓静态绑定,是指的定义时类型是啥就是啥,比如使用 D 对象,那么就使用 D 中的函数,如果使用的时 B* 指针,那么就使用 B 中的函数。

如果使用 virtual 函数,那么每次在 Derived class 中都需要指定相同的缺省值。这样会造成代码重复,并且当 Base class 中默认值改变之后,所有 derived class 中的默认值都需要改变。

这时候可以使用 NIV 手法。创建一个 non-virtual 函数,使之调用 virtual 函数,并在 non-virtual 函数中设定默认参数。

38:通过复合体现has-a或者“根据某物实现出”

在应用域(application domain),复合意味着 has-a (有一个)。在实现域(implementation domain),复合意味着 is-implemented-in-terms-of(根据某物实现出)

所谓 is-implemented-in-terms-of 是说利用某种东西实现一些东西。比如想要用list实现set,一种想法是继承list,但是由于继承的含义:
所有对list成立的东西,一定对set成立。而list可以有两个重复元素,set不可以。

所以这里不应该使用继承。而是将list作为一个私有成员,利用 list 进行实验。

39:明智而审慎地使用 private 继承

  • Private 继承意味着 is-implemented-in-terms-of (根据某物实现出)。通常比复合(composition)的级别低。但是当 derived classes 需要访问 protected base class 的成员或者重新定义继承来的 virtual 函数的时候,这么设计是合理的。
  • private 继承可以使得 empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者来说可能比较重要。

使用复合的好处:

  • 可以模拟 Java 中的 final 功能
  • 可以将编译依存性降到最低,如果使用指针,只需要一个声明即可。

Empty class:

  • 没有任何成员变量,virtual 函数(会生成vptr)等。但是可能有静态成员变量,enum,typedef,non-virtual函数。
  • 仅在单一继承下才会有EBO(empty base optimization, 空白基类最优化)

40:明智而审慎地使用多重继承

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需要
  • virtal 继承会增加大小、速度、初始化(及赋值)复杂度成本。但如果 virtual base class 不带有任何数据,那就是比较有使用价值的情况。
  • 多重继承有正当用途。比如“public 继承某个 interface class” 和 “private 继承某个协助实现的 class” 的组合。

所谓的 virtual 继承是为了处理“钻石型多重继承”问题:

class File {...};
class InputFile: public File {...};
class OutputFile: public File {...};
class IOFile: public InputFile, public OutputFile {...};

继承图如下:
在这里插入图片描述
这就会导致,FIle 中的成员会被复制一份。

而 virtual 继承会保证只有一份,来避免变量重复。但是往往会造成极大的代价。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值