实现
大多数情况下,适当提出拟的类定义以及函数声明,是花费最多心力的两件事。尽管如此,还是有很多东西需要小心:太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handls)”可能会破坏封装并留给客户虚吊号码牌;为考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合则可能导致让人不满意的冗长建置时间。
条款26:尽可能延后变量定义式的出现时间
只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并为被使用,仍需耗费这些成本,所以应该尽量避免这种情形。
std::string encryptPassword(const std::string& password) { using namespace std; string encrypted;//不应在这定义 if (password.length() < MinimumPasswordLength) { throw logic_error("Password is too short"); //注意:可能抛出异常 } string encrypted;//应该在这定义 return encrypted; }
“通过默认构造函数构造出一个对象然后对它赋值”比“直接在构造函数时制定初值”效率差。
“尽可能延后”的真正意义应该是:你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
//方法A:定义循环外 Widget w; for (int i = 0; i < n; ++i) { w = some value dependent on i; ... }//1个构造函数+1个析构函数+n个赋值操作; //方法B:定义循环外 for (int i = 0; i < n; ++i) { Widget w(some value dependent on i); ... }//n个构造函数+n个析构函数
除非:1.你知道赋值成本比“构造+析构”成本低;2.你正在处理代码中效率高度敏感的部分,否则应该使用方法B。
请记住:
- 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
条款27:尽量少做转型动作
C++规则的设计目标之一是,保证“类型错误”绝不可能发生。不幸的是,转型(casts)破坏了类型系统。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
//C风格的转型动作看起来像这样: (T)expression //将expression转型为T //函数风格的转型动作看起来像这样: T(expression) //将expression转型为T
C++还提供四种新式转型:
- const_cast:通常被用来将对象的常量性转除;即去掉const。
- dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
- reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
- static_cast:用来强迫隐式转换,例如将non-const转型为const,int转型为double等等。
任何一个类型转换(不论是通过转型操作进行的显示转换还是通过编译器进行的隐式转换),往往真的令编译器编译出运行期间执行的代码。
尽量使用新式转型:
- 它们很容易在代码中被辨识出来,因而得以简化“找出类型系统在哪个地点被破坏”的过程。
- 各转型动作的目标愈窄化,编译器愈可能诊断出错误的运用。
class Base { public: virtual void onSize(); }; class Derived:public Base { public: virtual void onSize() { static_cast<Base>(*this).onSize(); //other action } };
上述代码并非在当前对象身上调用Base::onSize()后又在该对象身上执行other action,他是在“当前对象之base class”成分的副本上执行Base::onSize。然后执行other action,如果Base::onSize修改了对象内容,该动的是副本,如果Derived::onSize也修改了对象,那么对象可能真正的被修改了。这会造成base class成分没有被修改,而derived class部分被修改了。
解决办法:
class Derived:public Base { public: virtual void onSize() { Base::onSize(); //other action } };
dynam_cast是用来在你认定的一个derived class对象身上执行derived class操作函数,但你的手上只有一个指向Base class的指针或引用,你只能靠他来处理对象。解决办法:
一、使用容器并在其中存储直接指向derived class对象的指针,但是这种做法使你无法在同一个容器内存指针指向所有可能之各种Base派生类,如果你想处理多种派生类,你需要多个容器。
class Base{}; class Derived:public Base { public: void onSize(); }; typedef vector<shared_ptr<Derived>> VPSW; VPSW vpswPtr; for(VPSW::iterator it=vpswPtr.begin();it!=vpswPtr.end();++it) (*it)->onSize();
二、通过base class接口处理所有可能之各种基类派生类,就是在base class中提供virtual函数做对各个派生类做的事。
请记住:
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
- 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
- 宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
条款28:避免返回handls指向对象内部成分
struct RectData { Point ulhc; Point lrhc; }; class Rectangle { public: ... Point& upperLeft() const { return pData->ulhc; }1//const只对函数内进行保护,函数返回后呢?? Point& lowerRight() const { return pData->lrhc; }2 //const只对函数内进行保护,函数返回后呢?? private: std::tr1::shared_ptr<RectData> pData; ... };
Rectangle内两函数都返回引用,指向private内部数据,调用者于是可通过这些引用更改内部数据!这严重破坏了数据的封装性,对私有成员进行直接操作。
const Point& upperLeft() const { return pData->ulhc; } const Point& lowerRight() const { return pData->lrhc; }
如上该动。这就限制了客户的“涂改权”,只有“读取权”。
- 成员变量的封装性最多等于“返回其reference”函数的访问级别。
- 如果const成员函数传出一个引用,后者所指数据与对象自身有关联,那么函数的调用者可以修改这批数据。
但终究“返回一个handle代表对象内部成分“带来的是降低数据封装性的的危险,不被公开的成员函数也是对象的一部分(private或protected),也不应该返回他们的handle,不应该令成员函数指向一个访问级别较低的成员函数,这会导致后者的访问级别提高如同前者——访问级别较高的,因为客户可以获取一个指针指向访问级别较低的函数,然后通过那个指针调用它。
请记住:
- 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
条款29:为“异常安全”而努力是值得的
当有异常被抛出时,带有异常安全性的函数会:
- 不泄露任何资源
- 不允许数据被破坏
异常安全函数提供以下三个保证之一:
- 基本承诺:如果异常被抛出,程序内的任何事物仍然保证有效的状态下,没有任何的对象和数据因此而被破坏,所有对象都出于内部前后一致的状态。
- 强烈保证:程序抛异常,程序状态不改变。如果函数成功,就完全成功;如果失败,就回到调用函数以前的状态。
- 不抛保证:绝不抛异常,他们总是能完成先前的承若。
copy and swap策略提供强烈保证:为你打算修改的对象做出一副本,然后在副本身上做一切修改,若有任何修改动作抛出异常,原对象仍保持未改变的状态,待所有改变都成功后,再将修改过的副本和原对象在一个不抛异常的操作中置换。让对象做全有或全无的状态改变。
请记住:
- 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻了解inlining的里里外外
Inline函数,多棒的点子!它们看起来像函数,动作像函数,比宏好得多,可以调用它们又不需蒙受函数调用所招致的额外开销。你实际获得的比想象的还多,编译器有能力对执行语境相关最优化。
inline函数的整体观念为:对此函数的每一个调用都以函数本体替换之,这可能能加你的目标代码(object code)的大小。
如果inline函数的本体很小,编译器针对“函数本体”所产生的码可能比针对“函数调用”所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置击中率。
记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,这样的函数通常是成员函数,friend函数也可被定义于class内,如果真是那样,它们也是被隐喻声明为inline。明确声明inline函数的做法则是在其定义式钱加上关键字inline。
Inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程中进行inlining,inline在C++中是编译期行为。而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。
Template通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。
Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据此template具现出来的函数都应该inlined,请将此template声明为inline;但如果你写的template煤油理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline。
一个表面上看似inline的函数是否真实inline,取决于你的建置环境,主要取决于编译器。大部分编译期拒绝太过复杂的inline(带有循环或递归),virtual的调用(执行期确定哪个函数调用)也会是inline(执行前,将函数的调用动作替换成函数的本体)落空。
如果取某个inline函数的地址,编译器通常为此函数生成一个outlined函数本体。编译器通常不对“通过函数指针进而调用”实施inline。
大部分调试器面对inline函数都束手无策。
这使我们在决定哪些函数该被声明为inline而哪些函数不该时,掌握一个合乎逻辑的策略。一开始先不要将任何函数声明为inline,或至少将inlining施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。
C++对对象的创建和销毁时发生了什么事做了各式各样的保证。如果有个异常在对象构造期间被抛出,那么构造好的那部分对象会被销毁,这些情况中c++描述了一定会发生什么,但没有说如何发生,编译器在编译期间会产生代码并安插到你的代码中。
请记住:
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。
- 不要只因为function templates出现在头文件,就将它们声明为inline。
条款31:将文件间的编译依存关系降至最低
前置声明比较复杂,它涉及到额外的template,而且在编译期间,编译器必须要知道前置声明对象的大小,知道对象大小的办法就是要知道类的定义式。所以我们把类分为两个部分,一个只提供接口,另一个实现接口。
这个分离的关键在于以“声明的依赖性”替换“定义的依赖性”,这是编译依赖性最小化成本的本质:现实中让头文件尽可能满足,万一做不到就让他与其他头文件的声明式(而非定义式)相依。
将提供class的定义式(通过#include完成)的义务从“函数声明”所在之头文件转移到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依赖性去掉。
为定义式和声明式提供不同的头文件。
请记住:
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface classes。
- 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。