每一个Item都很经典,都需要去思考揣摩,我在这里将要点抽象出来,便于日后快速回忆;我只是在做文章的“搬运工”。
Item 26 只要有可能就推迟变量定义
1. 不仅应该推迟一个变量的定义直到你不得不用它之前的最后一刻,而且应该试图推迟它的定义直到你得到了它的初始化参数。通过这样的做法,你可以避免构造和析构无用对象,而且还可以避免不必要的缺省构造。更进一步,通过在它们的含义已经非常明确的上下文中初始化它们,有助于对变量的作用文档化。
2.只要有可能就推迟变量定义。这样可以增加程序的清晰度并提高程序的性能。
[很好]
Item 27 将强制转换减到最少
1.旧风格的强制转型依然合法,但是新的形式更可取。首先,在代码中它们更容易识别(无论是人还是像 grep 这样的工具都是如此),这样就简化了在代码中寻找类型系统被破坏的地方的过程。第二,更精确地指定每一个强制转型的目的,使得编译器诊断使用错误成为可能。
2. 避免强制转型的随时应用,特别是在性能敏感的代码中应用 dynamic_casts,如果一个设计需要强制转型,设法开发一个没有强制转型的侯选方案。
[减少使用强制转换的原因是因为效率较低;关于强转,参考之前的“ C++类型强转”和“ 理解RTTI”]
Item 28 避免返回对象内部构件的“句柄”
1.避免返回对象内部构件的句柄(引用,指针,或迭代器)。这样会提高封装性,帮助 const 成员函数产生 cosnt 效果,并将空悬句柄产生的可能性降到最低。
[这是最容易忽视的错误,我们常常不经意间就会返回一个对象内部的句柄,而外面这层对象很可能先于内部句柄销毁。详见“ 避免返回对象内部构件的句柄”]
Item 29 争取异常安全(exception-safe)的代码
1.当一个异常被抛出,异常安全的函数应该:
*没有资源泄漏
*不允许原数据结构恶化
2.异常安全函数提供下述三种保证之一:
*函数提供 基本保证(the basic guarantee),允诺如果一个异常被抛出,程序中剩下的每一件东西都处于合法状态。没有对象或数据结构被破坏,而且所有的对象都处于内部调和状态(所有的类不变量都被满足)。然而, 程序的精确状态可能是不可预期的。例如,我们可以重写 changeBackground,以致于如果一个异常被抛出,PrettyMenu 对象可以继续保留原来的背景图像,或者它可以持有某些缺省的背景图像,但是客户无法预知到底是哪一个。
*函数提供 强力保证(the strong guarantee),允诺如果一个异常被抛出,程序的状态不会发生变化。调用这样的函数在感觉上是极其微弱的,如果它们成功了,它们就完全成功, 如果它们失败了,程序的状态就像它们从没有被调用过一样。
*函数提供 不抛异常保证(the nothrow guarantee), 允诺决不抛出异常,因为它们只做它们答应要做的。所有对内建类型(例如,ints,指针,等等)的操作都是不抛出(nothrow)的(也就是说,提供不抛出保证)。这是异常安全代码中必不可少的基础构件。
3."copy and swap",它的原理:先做出一个你要改变的对象的拷贝,然后在这个拷贝上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,最初的对象保持不变。在所有的改变完全成功之后,将被改变的对象和最初的对象在一个不会抛出异常的操作中进行交换。这就需要做出每一个要改变的对象的拷贝,这可能会用到你不能或不情愿动用的时间和空间。
4.系统即使只有一个函数不是异常安全的,那么系统作为一个整体就不是异常安全的,因为调用那个函数可能发生泄漏资源和恶化数据结构。不幸的是,很多 C++ 的遗留代码在写的时候没有留意异常安全,所以现在的很多系统都不是异常安全的。它们混合了用非异常安全(exception-unsafe)的方式书写的代码。
[我基本上没有用过异常,我常常使用返回错误码、日志来记录异常;目前能够说服我使用异常的原因是:引入异常的原因之一是为了能让构造函数报错,毕竟构造函数没有返回值,没有异常的话调用方如何得知对象构造是否成功呢?参考之前的“ “异常处理”学习小结”]
Item 30 理解 inline 化的介入和排除
1. inline 函数背后的思想是用函数本体代替每一处对这个函数的调用,这样可能会增加你的目标代码的大小。在有限内存的机器上,过分热衷于 inline 化会使得程序对于可用空间来说过于庞大。即使使用了虚拟内存,inline 引起的代码膨胀也会导致附加的分页调度,减少指令缓存命中率,以及随之而来的性能损失。
2.一个 inline 函数本体很短,为函数本体生成的代码可能比为一个函数调用生成的代码还要小。如果是这种情况,inline 化这个函数可以实际上导致更小的目标代码和更高的指令缓存命中率!
3. inline 是一个编译器可能忽略的请求。大多数编译器拒绝它们认为太复杂的 inline 函数(例如,那些包含循环或者递归的),而且,除了最细碎的以外的全部虚拟函数的调用都不会被 inline 化。不应该对这后一个结论感到惊讶。虚拟意味着“等待,直到运行时才能断定哪一个函数被调用”,而 inline 意味着“执行之前,用被调用函数取代调用的地方”。如果编译器不知道哪一个函数将被调用,你很难责备它们拒绝 inline 化这个函数本体。
4.假设Derived继承于Base, Derived的构造函数会先构造Base数据,再构造Derived数据,如果Derived的构造函数是inline的,调用者的代码会很庞大;如果Base的构造函数也是 inline 的,插入它的全部代码也要插入 Derived 的构造函数;而Base内部类对象碰巧也是 inline 的,Derived 的构造函数中将增加拷贝。到现在,为什么说是否 inline 化 Derived 的构造函数不是一个不经大脑的决定就很清楚了。类似的考虑也适用于 Derived 的析构函数,用同样的或者不同的方法保证所有被 Derived 的构造函数初始化的对象被完全销毁。
5.如果 f 是一个库中的一个 inline 函数,库的客户将函数 f 的本体编译到他们的应用程序中。如果一个库的实现者后来决定修改 f,所有使用了 f 的客户都必须重新编译。这常常会令人厌烦。
6.如果 f 是一个非 inline 函数,对 f 的改变只需要客户重新连接。这与重新编译相比显然减轻了很大的负担,而且,如果库中包含的函数是动态链接的,这就是一种对于用户来说完全透明的方法。
7.一个典型的程序用 80% 的时间执行 20% 的代码。这是一个重要的规则,因为它作为一个软件开发者的目标是识别出能全面提升你的程序性能的 20% 的代码。你可以 inline 或者用其他方式无限期地调节你的函数,但除非你将精力集中在正确的函数上,否则就是白白浪费精力。
[使用好inline还是需要一定功力的。]
Item 31 最小化文件之间的编译依赖
1.将类的实现隐藏在一个指针后面。
2. 用对声明的依赖替代对定义的依赖。这就是最小化编译依赖的精髓:只要能实现,就让你的头文件独立自足,如果不能,就依赖其它文件中的声明,而不是定义。其它每一件事都从这个简单的设计策略产生。所以:
*当对象的引用和指针可以做到时就避免使用对象。仅需一个类型的声明,你就可以定义到这个类型的引用或指针。而定义一个类型的对象必须要存在这个类型的定义。
*只要你能做到,就用对类声明的依赖替代对类定义的依赖。注意你声明一个类的函数时绝对不需要有这个类的定义,即使这个函数通过传值方式传递或返回这个类:
3. 如果有一个包含很多函数声明的库,每一个客户都要调用每一个函数是不太可能的。通过将提供”类定义的责任”从你的”声明函数的头文件”转移到”客户的包含函数调用的文件”,你就消除了客户对他们并不真的需要的类型的依赖。
*为声明和定义分别提供头文件。为了便于坚持上面的指导方针,头文件需要成对出现:一个用于声明(.h),另一个用于定义(.cpp)。当然,这些文件必须保持一致。如果一个声明在一个地方被改变了,它必须在两处都被改变。得出的结果是:库的客户应该总是 #include 一个声明文件,而不是自己前向声明某些东西,而库的作者应该提供两个头文件。
4.“将类的实现隐藏在一个指针后面”,一种方法就是将实现分开到两个类中,一个仅仅提供一个接口,另一个实现这个接口。这样一个设计经常被说成是使用了 pimpl 惯用法(指向实现的指针 "pointer to implementation")。在这样的类中,那个指针的名字经常是 pImpl。
5. Handle 类和 Interface 类从实现中分离出接口,因此减少了文件之间的编译依赖。在开发过程中,使用 Handle 类和 Interface 类来最小化实现发生变化时对客户的影响。当能看出在速度和/或大小上的不同足以证明增加类之间的耦合是值得的时候,可以用具体类取代 Handle 类和 Interface 类供产品使用。
Item 26 只要有可能就推迟变量定义
1. 不仅应该推迟一个变量的定义直到你不得不用它之前的最后一刻,而且应该试图推迟它的定义直到你得到了它的初始化参数。通过这样的做法,你可以避免构造和析构无用对象,而且还可以避免不必要的缺省构造。更进一步,通过在它们的含义已经非常明确的上下文中初始化它们,有助于对变量的作用文档化。
2.只要有可能就推迟变量定义。这样可以增加程序的清晰度并提高程序的性能。
[很好]
Item 27 将强制转换减到最少
1.旧风格的强制转型依然合法,但是新的形式更可取。首先,在代码中它们更容易识别(无论是人还是像 grep 这样的工具都是如此),这样就简化了在代码中寻找类型系统被破坏的地方的过程。第二,更精确地指定每一个强制转型的目的,使得编译器诊断使用错误成为可能。
2. 避免强制转型的随时应用,特别是在性能敏感的代码中应用 dynamic_casts,如果一个设计需要强制转型,设法开发一个没有强制转型的侯选方案。
[减少使用强制转换的原因是因为效率较低;关于强转,参考之前的“ C++类型强转”和“ 理解RTTI”]
Item 28 避免返回对象内部构件的“句柄”
1.避免返回对象内部构件的句柄(引用,指针,或迭代器)。这样会提高封装性,帮助 const 成员函数产生 cosnt 效果,并将空悬句柄产生的可能性降到最低。
[这是最容易忽视的错误,我们常常不经意间就会返回一个对象内部的句柄,而外面这层对象很可能先于内部句柄销毁。详见“ 避免返回对象内部构件的句柄”]
Item 29 争取异常安全(exception-safe)的代码
1.当一个异常被抛出,异常安全的函数应该:
*没有资源泄漏
*不允许原数据结构恶化
2.异常安全函数提供下述三种保证之一:
*函数提供 基本保证(the basic guarantee),允诺如果一个异常被抛出,程序中剩下的每一件东西都处于合法状态。没有对象或数据结构被破坏,而且所有的对象都处于内部调和状态(所有的类不变量都被满足)。然而, 程序的精确状态可能是不可预期的。例如,我们可以重写 changeBackground,以致于如果一个异常被抛出,PrettyMenu 对象可以继续保留原来的背景图像,或者它可以持有某些缺省的背景图像,但是客户无法预知到底是哪一个。
*函数提供 强力保证(the strong guarantee),允诺如果一个异常被抛出,程序的状态不会发生变化。调用这样的函数在感觉上是极其微弱的,如果它们成功了,它们就完全成功, 如果它们失败了,程序的状态就像它们从没有被调用过一样。
*函数提供 不抛异常保证(the nothrow guarantee), 允诺决不抛出异常,因为它们只做它们答应要做的。所有对内建类型(例如,ints,指针,等等)的操作都是不抛出(nothrow)的(也就是说,提供不抛出保证)。这是异常安全代码中必不可少的基础构件。
3."copy and swap",它的原理:先做出一个你要改变的对象的拷贝,然后在这个拷贝上做出全部所需的改变。如果改变过程中的某些操作抛出了异常,最初的对象保持不变。在所有的改变完全成功之后,将被改变的对象和最初的对象在一个不会抛出异常的操作中进行交换。这就需要做出每一个要改变的对象的拷贝,这可能会用到你不能或不情愿动用的时间和空间。
4.系统即使只有一个函数不是异常安全的,那么系统作为一个整体就不是异常安全的,因为调用那个函数可能发生泄漏资源和恶化数据结构。不幸的是,很多 C++ 的遗留代码在写的时候没有留意异常安全,所以现在的很多系统都不是异常安全的。它们混合了用非异常安全(exception-unsafe)的方式书写的代码。
[我基本上没有用过异常,我常常使用返回错误码、日志来记录异常;目前能够说服我使用异常的原因是:引入异常的原因之一是为了能让构造函数报错,毕竟构造函数没有返回值,没有异常的话调用方如何得知对象构造是否成功呢?参考之前的“ “异常处理”学习小结”]
Item 30 理解 inline 化的介入和排除
1. inline 函数背后的思想是用函数本体代替每一处对这个函数的调用,这样可能会增加你的目标代码的大小。在有限内存的机器上,过分热衷于 inline 化会使得程序对于可用空间来说过于庞大。即使使用了虚拟内存,inline 引起的代码膨胀也会导致附加的分页调度,减少指令缓存命中率,以及随之而来的性能损失。
2.一个 inline 函数本体很短,为函数本体生成的代码可能比为一个函数调用生成的代码还要小。如果是这种情况,inline 化这个函数可以实际上导致更小的目标代码和更高的指令缓存命中率!
3. inline 是一个编译器可能忽略的请求。大多数编译器拒绝它们认为太复杂的 inline 函数(例如,那些包含循环或者递归的),而且,除了最细碎的以外的全部虚拟函数的调用都不会被 inline 化。不应该对这后一个结论感到惊讶。虚拟意味着“等待,直到运行时才能断定哪一个函数被调用”,而 inline 意味着“执行之前,用被调用函数取代调用的地方”。如果编译器不知道哪一个函数将被调用,你很难责备它们拒绝 inline 化这个函数本体。
4.假设Derived继承于Base, Derived的构造函数会先构造Base数据,再构造Derived数据,如果Derived的构造函数是inline的,调用者的代码会很庞大;如果Base的构造函数也是 inline 的,插入它的全部代码也要插入 Derived 的构造函数;而Base内部类对象碰巧也是 inline 的,Derived 的构造函数中将增加拷贝。到现在,为什么说是否 inline 化 Derived 的构造函数不是一个不经大脑的决定就很清楚了。类似的考虑也适用于 Derived 的析构函数,用同样的或者不同的方法保证所有被 Derived 的构造函数初始化的对象被完全销毁。
5.如果 f 是一个库中的一个 inline 函数,库的客户将函数 f 的本体编译到他们的应用程序中。如果一个库的实现者后来决定修改 f,所有使用了 f 的客户都必须重新编译。这常常会令人厌烦。
6.如果 f 是一个非 inline 函数,对 f 的改变只需要客户重新连接。这与重新编译相比显然减轻了很大的负担,而且,如果库中包含的函数是动态链接的,这就是一种对于用户来说完全透明的方法。
7.一个典型的程序用 80% 的时间执行 20% 的代码。这是一个重要的规则,因为它作为一个软件开发者的目标是识别出能全面提升你的程序性能的 20% 的代码。你可以 inline 或者用其他方式无限期地调节你的函数,但除非你将精力集中在正确的函数上,否则就是白白浪费精力。
[使用好inline还是需要一定功力的。]
Item 31 最小化文件之间的编译依赖
1.将类的实现隐藏在一个指针后面。
2. 用对声明的依赖替代对定义的依赖。这就是最小化编译依赖的精髓:只要能实现,就让你的头文件独立自足,如果不能,就依赖其它文件中的声明,而不是定义。其它每一件事都从这个简单的设计策略产生。所以:
*当对象的引用和指针可以做到时就避免使用对象。仅需一个类型的声明,你就可以定义到这个类型的引用或指针。而定义一个类型的对象必须要存在这个类型的定义。
*只要你能做到,就用对类声明的依赖替代对类定义的依赖。注意你声明一个类的函数时绝对不需要有这个类的定义,即使这个函数通过传值方式传递或返回这个类:
3. 如果有一个包含很多函数声明的库,每一个客户都要调用每一个函数是不太可能的。通过将提供”类定义的责任”从你的”声明函数的头文件”转移到”客户的包含函数调用的文件”,你就消除了客户对他们并不真的需要的类型的依赖。
*为声明和定义分别提供头文件。为了便于坚持上面的指导方针,头文件需要成对出现:一个用于声明(.h),另一个用于定义(.cpp)。当然,这些文件必须保持一致。如果一个声明在一个地方被改变了,它必须在两处都被改变。得出的结果是:库的客户应该总是 #include 一个声明文件,而不是自己前向声明某些东西,而库的作者应该提供两个头文件。
4.“将类的实现隐藏在一个指针后面”,一种方法就是将实现分开到两个类中,一个仅仅提供一个接口,另一个实现这个接口。这样一个设计经常被说成是使用了 pimpl 惯用法(指向实现的指针 "pointer to implementation")。在这样的类中,那个指针的名字经常是 pImpl。
5. Handle 类和 Interface 类从实现中分离出接口,因此减少了文件之间的编译依赖。在开发过程中,使用 Handle 类和 Interface 类来最小化实现发生变化时对客户的影响。当能看出在速度和/或大小上的不同足以证明增加类之间的耦合是值得的时候,可以用具体类取代 Handle 类和 Interface 类供产品使用。
[降低文件之间的编译依赖,同时做到了松耦合,一种好的设计思想]