《Effective C++》简明笔记-中
32. 确定你的 public 继承模拟了 is-a 关系
- 面向对象编程中最重要的一条。如果派生类D通过public方式继承了基类B,那么所用用于B的方法 或者 基类B自身具有的方法,都适用于D。
33. 避免遮掩继承而来的名称
- 如果派生类D通过public方式继承了基类B,那么D中的函数/变量名会遮掩B中的函数/变量名,如同局部作用域与全局作用域的关系一样。比如
class B{ public: void f(){} void f(int x){} }; class D:public B{ public: void f(){} // f不仅会覆盖B::f(),也会覆盖B::f(int),因为这是变量名覆盖 }; class E:public B{ public: int f; // 即使f不是函数,也会覆盖B::f()和B::f(int) };
34. 区别接口继承与实现继承
- 接口继承,意味着继承方法的签名,包括返回类型,参数列表,方法名。
- 实现继承,意味着继承方法的实现,即功能。
- 基类中的纯虚函数意味着,派生类只继承接口,而自己进行实现。所有派生类都必须对基类的纯虚函数进行显式的继承(即使继承后仍然是个纯虚函数)。理论上,纯虚函数不必须实现(即只有声明没有定义),但也可以定义纯虚函数的函数体。如果定义了纯虚函数,那么调用该函数的唯一方法就是在调用时显式指定基类的名称。这使得我们有时候可以通过实现纯虚函数来进行某种缺省的实现。
class Shape{ public: virtual void draw() const =0 {}; ... }; class Circle:public Shape{ public: void draw(){ ... // 在隐喻的屏幕上绘制圆 } ... }; class InvisibleShape:public Shape{ public: void draw(){ Shape::draw(); // 对不可见的物体,调用缺省的纯虚函数实现 } ... };
- 基类中的非纯虚函数意味着,派生类需要同时集成接口和一份缺省实现。如果派生类中未声明该虚函数,就相当于自动继承了该函数,如果派生类自己实现了同样签名的函数,则使用自己的实现。使用非纯虚函数可能导致的一个风险,就是由于依赖于 “不去声明基类中的虚函数而自动获得继承”,而忘了该虚函数的存在。
class airPlane{ virtual void fly(){ ... // 缺省的实现 } ... }; class planeTypeA:public airPlane{...}; // A依赖缺省的fly()方法 class planeTypeB:public airPlane{...}; // B也一样 class planeTypeC:public airPlane{...}; // C的引擎与A和B不一样,但是忘了实现自己的fly()方法!
一个可选的方法是,定义一个非虚函数,令虚函数调用它
class airPlane{ public: virtual void fly() =0; protected: void defaultFly(){ // 缺省的实现 } }; class planeTypeA:public airPlane{ void fly(){ defaultFly(); // 即使依赖缺省实现,也要显式调用 } }; class planeTypeC:public airPlane{ void fly(){ // C的引擎与A不一样,不能依赖缺省实现,这里是单独的一份实现 } };
- 基类中的非虚函数,表示派生类不仅需要继承接口,还需要继承一份强制的实现。
35. 考虑虚函数以外的选择
- 非常精彩的一节!这一节在 为对象实现“动态的方法” 这个话题上,提供了四种不同的风格:
- 接口不含虚函数的Template Method模式
这种模式认为,虚函数都必须是private的,基类的“动态逻辑”(即不同派生类不同的逻辑)由非虚函数调用虚函数实现。假设我们在设计网络游戏《魔兽世界》每个种族的跳跃动作:
class charactor { public: void jump(){ ... // 准备工作,比如停止施法(如果正在) doJump(); // 跳跃 } private: virtual void doJump() =0; // 跳跃 };
侏儒的跳跃与人类的肯定不一样,所以派生类需要实现基类中的纯虚函数。
class dwarfCharactor:public charactor{ private: void doJump(){ // 侏儒角色的跳跃动作 } }; class orcCharactor:public charactor{ private: void doJump(){ // 兽人角色的跳跃动作 } };
这种模式的有点在于,你可以做一些“事前”或“事后”的事情,比如跳跃时必须停止施法。但是这种模式会产生这样的诡异之处:派生类需要实现一个根本不需要自己调用的函数(而是给基类的函数调用),也就是说基类保留了“何时调用该函数的权利”,却将函数的细节交给派生类掌管。
- 函数指针实现的Strategy模式
兽人不一定是指玩家,也可能是指怪物。如果游戏中有大大小小各色兽人怪物,他们的跳跃方式只有在初始化时才能确定,那么我们可以在类中保存一个函数指针,在初始化时传入函数地址。
void defaultJump(); class charactor{ public: charactor(void (*jump)()=defaultJump): jumpFunc(jump) {} private: void (*jumpFunc)(); }; class orcCharactor:public charactor{ public: orcCharactor(void (*jump)()=defaultJump): charactor(jump) {} };
通过建立如 setJumpFunc 函数甚至可以在运行时改变角色跳跃的方式。
- tr1::function实现的Strategy模式
将函数指针实现的Strategy模式中的“函数指针”替换为函数对象tr1::function。假设我们现在要计算角色剩余的生命值(好吧,还是用书中的例子吧,编不下去了,但是这里真的很精彩啊!为了避免以后忘记,一定要好好记下来,嗯)。
class charactor{ public: // std::tr1::function<int (const charactor*)>对象healthCalc,可以接受一个类似函数的对象,只要该对象能够: // 返回一个与int兼容的对象/变量 // 接受一个与const charactor&兼容的对象/变量 charactor(std::tr1::function<int (const charactor*)> _healthCalc):healthCalc(_healthCalc){} private: std::tr1::function<int (const charactor&)> healthCalc; }; class orcCharactor:public charactor{...};
类 charactor 中包含一个 std::tr1::function<int (const charactor*)> 类型的成员对象 healthCalc ,该对象可以通过任何“像函数的东西”来初始化。如注释中所说,只要这个东西接受和返回具有相应兼容性的对象,就可以初始化healthCalc。比如以下这三样东西:
short calcHeath(const charactor&); // 计算生命值的函数 struct healthCaculator{ // 函数对象 int operator()(const charactor&) const; }; class gameLevel{ public: float healh(const charactor&); // 某个类的成员函数 };
orcCharactor badGuy1(calcHeath); // 用函数初始化 orcCharactor badGuy2(healthCaculator); // 用函数对象初始化 gameLevel level; orcCharactor badGuy3( // 使用成员函数初始化 std::tr1::bind(&gameLevel::healh, level, _l) );
我们分别使用函数和函数对象来进行初始化。最精彩的在第三个,使用成员函数初始化。因为成员函数实际上额外接受一个参数(即调用成员函数的对象自身),所以它实际上是接受两个参数的函数。而std::tr1::bind方法允许为这样一个函数的其中一个参数绑上默认值,使这个函数的行为就像是只接受一个参数的函数那样。这个方法同样适用于具有多个参数的函数(而不仅仅是成员函数,这里拿成员函数只不过又提醒了我,成员函数隐式接受调用对象自身作为参数)。这真的很奇妙。
- 古典Strategy模式
相对简单,将计算生命值 和 角色 分别体系化,角色基类 中 存储 指向“计算生命值基类对象”的指针,并在派生类中实现相应逻辑。通常使用UML图描述这种关系。
- 接口不含虚函数的Template Method模式
36. 绝不重新定义继承而来的非虚函数
37. 绝不重新定义继承而来的缺省参数值
- 非虚函数和缺省参数值都是静态绑定的,对于虚函数中的缺省参数值,是否会影响到派生类中的对应函数,取决于调用的形式。比如:
class B{ virtual void f(int x=8){} }; class D:public B{ void f(int x){} };
这种情况下,如果通过指向派生类实例的基类指针调用函数f(),可以不指定参数x,缺省参数值起作用。但是如果通过派生类指针调用函数f(),不指定参数x就无法通过编译。
- 注意B中的函数f()是虚函数。不应当在public继承的派生类中重载基类的非虚函数。
38. 通过复合模拟出 has-a 或者 is-implemented-in-terms-of 关系
- 应用域:has-a关系。
- 实现域:is-implemented-in-terms-of 关系。
39.明智而审慎地使用 private 继承
- private 继承的特点是:基类中的所有public成员都将称为派生类的private成员,从派生类外无法访问基类的成员。这说明基类的逻辑被隐藏在幕后,派生类需要借助基类实现其自身的功能,即 is-implemented-in-terms-of 关系。
- 与复合不同之处:private继承的派生类具有“对象尺寸最小化”的特征。如下,类B1和类B2都是通过B来实现的(在这里B只是个什么都没有的空类)。但是在几乎所有编译器中,B2对空间的消耗的确比B1稍大一些。
class B{}; class B1:private B{}; class B2{ private: B b; };
40.明智而审慎地使用多重继承
- 多重继承,顾名思义,就是同时继承多个基类。在访问多重继承派生类的时候,如果多个基类中的成员具有相同的名称,需要显示指定访问的是哪个基类中的成员,如:
class B1{ public: void f(){}; }; class B2{ public: void f(){}; }; class D:public B1, public B2{};
D d; d.B1::f();
- 解决钻石型多重继承:如果多重继承的两类又同时继承自同一类,如:
class B{ public: int x; }; class B1:public B{}; class B2:public B{}; class D:public B1, public B2{};
- 使用 virtual 继承会产生额外的开销,而且virtual继承后,基类的初始化由最底层的派生类实现(也就是说,D要负责对B中成员x的初始化,而不是由B1和B2负责)。所以,如果不得不使用virtual继承,那么就尽量避免在可能被virtual继承的基类中放置数据。
41.隐式接口和编译器多态
- 隐式接口是泛型编程中的概念,相对的显式接口则是面向对象编程中的。
- 显式接口,包括函数的签名,或者类的public部分,它规定了类和函数能够做什么,外界如果才能驱动函数和类的工作。
- 隐式接口,指在一个模板元中,待给定的类T需要做什么。比如:
template <typename T> void compareSize(T& t1, T& t2){ return t1.size()>t2.size(); }
- 在这个模板元中,T的隐式接口就是,必须具有size()方法,而且该方法返回的对象重载了>运算符,或者是内置类型。在模板的“具现化”过程中,不会发生什么,但是如果编译到调用compareSize<int>()方法的语句,就会编译出错(因为int没有实现size()方法)。
42.了解typename的双重含义
- 从属属性:在模板中依赖于一个template参数(也就是尖括号中typename后面的T啦)的属性(注意,是属性而不是成员哦)。
- 在使用从属属性的时候,应当在前面加上一个typename关键字,否则就会引发潜在的问题,如下所示。如果在T::someProperty前没有typename关键字,也许编译器会把声明指针用的*认为是用作乘法的乘号。
template <typename T> class C{ public: void f(){ typename T::someProperty* x; }; };
43.处理模板化基类内的名称
- 当基类是一个模板类时,派生类对基类几乎一无所知。事实就是这样,下面这段代码,在严格的编译器中,无法通过编译。虽然基类中已经定义了f()函数,但是派生类却坚持看不到这个函数。(但是我在VS2012中却是可以编译的,而且就算我把f()改成f2()都是可以编译的(f2()在基类B中可没有定义),只要不去实例化某个D类的对象,也就是说编译器对基类的假定相当宽松,把很多事情交给了编译后期完成)。
template <typename T> class B{ public: void f(){} }; template <typename T> class D:public B<T>{ public: void callf(){ f(); // 无法通过编译! } };
这是因为编译器知道,B类可能被特化,因此严格的编译器拒绝让D中对f()的调用通过编译。
template <> class B<int>{ public: // B模板类的这个特化版本并没有f()方法 };
解决这个问题的方法有三种:11
- 使用this指针
template <typename T> class D:public B<T>{ public: void callf(){ this->f(); } };
- 使用using语句
template <typename T> class D:public B<T>{ public: using B<T>::f; void callf(){ f(); } };
- 明确指定调用的函数存在于基类中
template <typename T> class D:public B<T>{ public: void callf(){ B<T>::f(); } };
44.将与参数无关的代码抽离templates
- 非类型模板参数往往引起“代码膨胀”。如
template <typename T, int size> class mat{ public: mat invert(); ... };
就不如:
template <typename T> class mat{ public: mat invert(); ... private: int size; };
作者:一叶斋主人 出处:www.cnblogs.com/yiyezhai 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。