条款 26:尽可能延后变量定义式的出现时间
- 只要我们定义了一个变量而且这个类型有一个构造函数或析构函数,那么,我们的程序到达这个变量的定义式时,就不得不承受构造的成本。
- 当我们的变量离开作用域时,就要承担析构的成本。即使这个变量没有被使用。
- 太快定义变量可能造成效率上的拖延。
- 不仅要延后到确实需要使用对象,更要延后到对象具有初值时,这样可以避免调用default构造函数的开销。
- 同时这样还可以增加程序的可读性。(定义和使用在相邻部份)
条款 27:尽量少做转型动作
- 尽可能的用 C++ 类型转换,在注重效率的代码中避免使用 dynamic_cast,如果一定需要,可以设计一个无转型的方案,在本条款中讲述了这样一个例子:
class Window { public: virtual void onResize(){...} ... }; class SpecialWindow :public Window { public: virtual void onResize() { static_cast<Window>(*this).onResize();//derived onResize实现代码 //将*this转型为Window,然后调用其onResize,行不通,见分析 } };
- 这里存在的问题是,将
*this
对象转型成 base 类,相当于创建了一个临时对象;如果这段代码的原意是想要调用onResize
修改 base 部分的变量,这里就变了味,因为这只能修改临时变量中的 base 部分,正确的方法应该是将转型放入函数中:class SpecialWindow :public Window { public: virtual void onResize() { Window::onResize();//调用Window::onResize作用于*this身上,此处使用了作用域操作符 } };
- 如果转型是必要的,试着把它隐藏在某个函数背后,客户可以调用函数,而不需要显式转型。
条款 28:避免返回 handles 指向对象的内部成员
- 所谓的 handles(号码牌,用来获得某个对象)包括了 reference、指针和迭代器,如果返回一个内部对象的 handle,会降低对象封装性。
- 遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const。
- 用例子来分析:【注意 return-by-reference、return-by-reference-to-const 的问题】
class Point { public: Point(int x, int y); void setX(int newVal); void setY(int newVal); } struct RectData { Point ulhc;//ulhc="upper left-hand corner"(左上角) Point lrhs;//lrhc="lower right-hand corner"(右下角) }; class Rectangle { private: shared_ptr<RectData> pData;// 要注意这里是private的,本意就是不想被修改 public: Point& upperLeft()const { return pData -> ulhc; }//这里可不是条款21说的返回对象的引用 Point& lowerRight()const { return pData->lrhc; } }; Point coord1(0, 0); Point coord2(100, 100); const Rectangle rec(coord1, coord2); rec.upperLeft().setX(50);//此处ulhc与lrhc都被声明为private,但实际上却是public, //因为仍然是在直接修改私有成员。 // 这里能被修改的原因就是函数返回的是 `Point&`,与本条款的内容相违背 // 对此进行改进如下:【还要注意尽量防止 handle 管理一个空对象】 class Rectangle { private: shared_ptr<RectData> pData; public: const Point& upperLeft()const { return pData->ulhc; } const Point& lowerRight()const { return pData->lrhc; } }; // 但这里仍然是返回了handles,可能会造成dangling(空悬的) handles,空悬的意思是handles所真正指的东西不复存在 // 错误的代码使用如下: class GUIObject{}; const Rectangle boundingBox(const GUIObject& obj); //客户使用这个函数: GUIObject* pgo; const Point& pUpperLeft = &(boundingBox(*pgo).upperLeft()); // 上面这个式子的问题在于:boundingBox是值传递,返回了一个无名的临时值 // 这个临时值调用upperLeft产生的引用也是临时对象内部的,所以后面会dangling
- 所以尽量不要返回 handles,但也有例外,operator[] 就必须返回 handles。
条款 29:为“异常安全”而努力是值得的
- 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏。
- 这样的函数区分为三种可能的保证:基本型(至少保证原来程序仍在有效状态下)、强烈型(保证程序状态不变)、不抛异常型(承诺不抛出异常)。【异常安全代码必须提供上述三种保证之一】
- 强烈保证往往能够以 copy-and-swap 实现出来,但强烈保证并非对所有函数都可实现或具备现实意义。
- copy and swap 策略(存在连带影响、效率问题需要考虑,具体看书中内容):为打算修改的对象做出一份副本,然后在那副本身上做一切必要修改,若有任何修改动作抛出异常,原对象仍保持未改变状态;待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换。
- 如果强烈保证无法实现,最少要提供基本保证。
- 函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者。
条款 30:透彻了解 inlining 的里里外外
- 将大多数 inlining 限制在小型、被频繁调用的函数身上,这可以使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。【inline 可以免除函数的某些开销】
- inline 只是对编译器的一个申请,不是强制命令。
- 该申请可以隐喻提出,也可以明确提出:隐喻方式是将函数定义于 class 定义式内;明确声明 inline 函数,即在其定义式前加上关键字 inline。
- inlining 在大多数 C++ 程序中是编译期行为。
- 并不是 inline 申请,编译器就会允许,以下情况编译器是拒绝 inlined 的:
- 拒绝将太过复杂(例如带有循环或递归)的函数 inlining。
- 所有对 virtual 函数的调用(除非是最平淡无奇的)也都会使 inlining 落空。【原因:virtual 意味着等待,需要直到运行期才确定调用哪个函数;而 inline 意味执行前先将调用动作替换为被调用函数的本体】
- 编译器通常不对通过函数指针而进行的调用实施 inlining。【即使这个函数指针是对 inline 函数的调用,也不能确定是否真的 inline】
- 构造函数和析构函数往往是 inlining 的糟糕候选人。
- 还需要值得注意的是,程序序设计者必须评估将函数声明为 inline 的冲击:inline 函数无法随着程序库的升级而升级,一旦 inline 的函数被改变,则所有牵涉该函数的代码都要变。
- 大部分调试器面对 inline 函数都束手无策,因为我们无法在一个并不存在的函数内设立断点,于是许多编译器禁止在 debug 版本中进行 inlining。
条款 31:将文件间的编译依存关系尽可能的降到最低
- 在进入本条款之前,需要理解 C/C++ 的编译知识。
- 假设有三个类
a
、b
和ab
,头文件将类的声明和类的实现分开,这样就有 6 个文件(3 个 h 文件和 3 个 cpp 文件);且假设ab
是由a
和b
复合而来,a
和b
也相互独立。【意思就是ab
的 h 头文件必须包含a
和b
的 h 头文件】 - 如果 a.h 发生了变化,a.h 需要重新编译,同时,ab.h 也要重新编译。
- 既然包含了 a.h,就需要重新编译,那如果在 ab.h 中把 a.h 去掉,改成声明一个
a
类(即告知编译器a
的存在),这样仍然是不能通过编译的。【因为编译器需要确切知道ab
类中a
成员变量的大小】【如果a
只是作为形参或返回值,声明一下又是可以的】 - 但这里有一条捷径可以走:就是把成员变量
a
的实现,变成是成员变量指针a
。【指针大小是确定的】- 此时 ab.h 可以不包括 a.h,只需要简单声明一下 class
a
即可;因为 ab.h 也不包括实现,所以这里不会用到a
中的方法;ab.cpp 进行真正的实现,再包含文件即可。
- 此时 ab.h 可以不包括 a.h,只需要简单声明一下 class
- 这里还需要注意的是 h 文件实际就是接口,cpp 文件实际就是实现,因此 cpp 文件的一些细节更改,不会影响到接口:假设 a.cpp 更改了一些函数里的细节,则 a.h 需要重新编译,重点是 ab.h 不需要重新编译。
- 前面说到的,ab.h 不包含 a.h(只声明了一个
a
的 class),ab.cpp 包含了 a.h,那实现里还是包括了 a.h,好处究竟在哪里?- 虽然 ab.cpp 无可避免的包含了 a.h(a.h 重新编译的同时,ab.cpp 也会重新编译),但是更重要的一个好处是,
a
的改变影响不了所有包含 ab.h 的地方。
- 虽然 ab.cpp 无可避免的包含了 a.h(a.h 重新编译的同时,ab.cpp 也会重新编译),但是更重要的一个好处是,
- 假设有三个类
- 因此,总结前面的:
- 对于 C++ 类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编。
- 如果它的实现文件(cpp 文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。
- 避免大量依赖性编译的解决方案就是:在头文件中用 class 声明外来类,用指针或引用代替变量的声明;在 cpp 文件中包含外来类的头文件。
- 有了这些补充,可以加以讨论如何降低文件间的编译依存性。
- Handler Classes(用指针指向真正实现的方法):
- 和前面提及的一样,可以用指针来代替(这样的设计就是 pimpl idiom,全称 pointer to implementation);所以客户所能看到的 h 文件中,不包含类的自定义头文件(前面说了,包含在他处)。
- 但这里有所不同的是:只用一个指针(智能指针)去管理所有的成员,而不是一个指针换一个成员。
- 客户端代码大量使用的将是
Person
(包含指针的那个类),而不必关心PersonImp
(真正实现),用于幕后实现的PersonImp
只面向于软件开发者而不是使用者。【以后Person
添加成员变量,可以直接在PersonImp
中进行添加了,从而起到了隔离和隐藏的作用】
- Interface Classes(利用继承关系和多态的特性):
- 令
Person
成为一种特殊的 abstract base class(抽象基类),称为 Interface class。- 这种 class 是用来描述 derived class 的接口,因此它通常不带成员变量,也没有构造函数,只有一个 virtual 析构函数以及一组 pure virtual 函数,用来叙述整个接口。
- 将细节放在子类中,父类只是包含虚方法和一个静态的
Create
函数声明,子类将虚方法实现,并实现Create
接口。 - 在客户端只需要使用到
Person
的引用或者指针,就可以访问到子类的方法。 - 由于父类的头文件里面不包含任何成员变量,所以不会导致重编(其实由于父类是虚基类,不能构造其对象,所以也不用担心由于父类头文件变化导致的重编问题)。
- 令
- Handler Classes(用指针指向真正实现的方法):
- 程序库头文件应该以完全且仅有声明式的形式存在,这种做法不论是否涉及 templates 都适用。