《Effective C++》简明笔记-中
来源:互联网 发布:美洲文明落后知乎 编辑:程序博客网 时间:2024/05/18 00:31
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();
- 解决钻石型多重继承:如果多重继承的两类又同时继承自同一类,如:此时类D中实际上有两份x(B1::x和B2::x),这两份x又同时继承自B。在语义上往往只要一份x。C++默认的实现是,维持两份x,但是相互复制。改动一份则两份都受到影响。一种语义上更自然,但是却会造成额外开销的方法是,将B1和B2对B的继承都实现为 virtual public 继承,这样在类D中就只有一份x了。
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;};
原文链接:http://www.cnblogs.com/yiyezhai/archive/2013/04/07/2961999.html
- 《Effective C++》简明笔记-中
- 《Effective C++》简明笔记-中
- 《Effective C++》简明笔记-中
- 《Effective C++》简明笔记-上
- 《Effective C++》简明笔记-上
- 《Effective C++》简明笔记-上
- C 语言简明笔记
- 《Effective C++》 笔记
- 《Effective C++》阅读笔记
- Effective C++--笔记
- <Effective C++: 资源管理> 笔记
- <<Effective C++>>笔记1
- <<Effective C++>>笔记3
- <<Effective C++>>笔记4
- <<Effective C++>>笔记5
- 《Effective C++》学习笔记
- 《Effective C++》阅读笔记
- 《Effective C++》学习笔记
- PHP 知识汇总
- AbandonedObjectPool 的相關屬性
- 计算机网络
- S5PV310 Android 电容屏驱动调试
- web服务器iptables策略
- 《Effective C++》简明笔记-中
- yahoo前端优化34条规则——Server篇
- ubuntu的dos2unix
- c++ class does not name a type
- Perl useage
- 多线程记录
- osg视点跟随
- .NET 调用 SAP RFC 乱码问题解决方法!
- OpenSSL-0.9.8g 安装与配置指南