条款26 尽可能延后变量定义式的出现时间

只要定义了一个变量而其类型带有一个构造函数或析构函数,那么程序控制流到达这个变量定义式时,便要承担构造成本;当它离开作用域时便要承担析构成本。即使这个变量最后并未被使用。

如果程序在中间抛出异常的话,在这之前声明的变量就成了“未被使用而被声明的变量”

  • 不止应该延后变量的定义,甚至应该延后直到能够给它初值实参为止,不仅可以避免构造和析构非必要对象,还可以避免无意义的default构造行为

image.png

方法A 方法B
1个构造+1个析构+n个赋值 n个构造函数+n个析构函数

做法A会造成名称w作用域比做法B更大

除非:

  1. 你的赋值成本比构造+析构成本低
  2. 你正在处理的代码中效率高度敏感部分

否则应该用方法B。

条款27 尽量少做转型动作

  • C++提供四种新式转型:

const_cast(expression)dynamic_cast(expression)reinterpret_cast(expression)static_cast(expression)

  • **const_cast**将对象的常量性移除,也是唯一有此能力的;
  • **dynamic_cast**在运行期完成,主要用来执行“安全向下转型”,是唯一无法由旧式语法执行的动作。可能会耗费重大运行成本。
  • **reinterpret_cast**:执行低级转型,实际动作取决于编译器,所以不可移植。e.g.将 pointer to int 转型为一个 int
  • **static_cast**:用来强迫隐式转换,包括将 non-const 转换为 const ,但不可以反过来转换,因为那是const_cast的事。

如果使用static_cast<base class>(derived class).成员函数,视图在派生类中的 virtual 函数里调用基类的 virtual 函数,那么此处的调用并不会是我们所想要的,而是 this 对象的基类成分*暂时副本的虚函数。

解决方法是拿掉转型动作,只需要直接使用 Window::onResize()


  • dynamic_cast的许多实现版本执行速度非常慢

之所以需要dynamic_cast,通常是因为需要在一个认定为派生类对象身上执行派生类的操作函数但是手上却只有一个指向基类的 pointer 或 reference

避免连串的dynamic_cast的使用,会非常慢

条款28 避免返回handles指向对象内部成分

决不能让一个成员函数返回一个指针,指向访问级别较低的成员函数

返回一个代表对象内部数据的 handle ,会带来降低对象封装性的风险。它可能会导致“虽然调用const成员函数却造成对象状态被更改”。

但是operator[]就允许返回string、vector元素的reference。

条款29 为”异常安全“而努力是值得的

异常安全函数提供以下三个保证之一:

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象和数据结构会因此而败坏。
  • 强烈保证:如果异常被抛出,程序状态不改变。如果函数成功,就是完全成功;如果函数失败,程序就会恢复到调用函数状态之前。
  • no-throw保证:承诺绝不抛出异常。

copy and swap:为你打算修改的对象做出一个副本,然后再那个副本身上修改。若有任何修改动作抛出异常,原对象仍保持为改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中swap。

条款30 透彻了解inlining的里里外外

B站UP主 双生子佯廖 说过现代编译器会自动inline它认为值得inline的函数,无论是否手动inline。

在内存有限的机器上,inline造成的代码膨胀会导致额外的换页行为,降低指令高速缓存的命中率

在类内声明并定义的成员函数会进行隐式inline申请

  • 对于 virtual 函数的 inline 调用会落空,因为 virtual 意味着等待,直到运行期才确定调用哪个函数

如果对函数取地址的话,说明一定要为这个函数生成一个函数体,那么 inline 也无能为力了。

通常,编译器不对“通过函数指针而进行的调用”实施inline

条款31 将文件间的编译依存关系降到最低

  1. #include<string>
  2. #include"date.h"
  3. #include"address.h"
  4. class Person{
  5. public:
  6. ...
  7. private:
  8. std::string Name;
  9. Date theBirthDate;
  10. Address theAddress;
  11. };

在Person定义文件和其include的文件之间形成了编译依赖,如果这些头文件中有任何改动,或者这些头文件依赖的头文件有改动,那么Person类所在文件和使用它的文件都要重新编译

  • 如果使用前置声明,编译器必须在编译期间知道对象的大小,如此才能为它分配内存:
    1. int main(){
    2. int x;
    3. Person p(params...);
    4. }
    此处对于p的处理,编译器需要询问Person的定义式。

如果使用Person *****p,相当于“接口与实现分离”,能够只需要前置声明,而不需要明确知道类型的定义、大小


如果可以将本应在函数声明所在的头文件中完成的class定义式,转移到有函数调用的客户文件中,就可以实现去除编译依赖


让Handle类成为没有数据成员,只有函数接口抽象基类是另一种方式。因为它没有任何数据,也没有函数实现,所以所有的函数都只是一个接口,用于调用。

这一章仔细看原书!!!!!!

  • 支持编译依存性最小化的一般构想是:依赖于声明式、不要依赖于定义式。基于此构想的两个手段是Handle classes和Interface classes。
  • 程序库头文件应该以“完全且仅有声明式”的形式存在。