《More Effective C++》笔记
- 条款1 指针与引用的区别
- 条款2 尽量使用C++风格的类型转换
- 条款3 不要对数组使用多态
- 条款4 避免无用的缺省构造函数
- 条款5 谨慎定义类型转换函数
- 条款6 自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别
- 条款7 不要重载“&&”,“||”, 或“,”
- 条款8 理解各种不同含义的new和delete
- 条款9 使用析构函数防止资源泄漏
- 条款10 在构造函数中防止资源泄漏
- 条款11 禁止异常信息(exceptions)传递到析构函数外
- 条款12 理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
- 条款13 通过引用(reference)捕获异常
- 条款14 审慎使用异常规格
- 条款15 了解异常处理的系统开销
- 条款16 牢记 80-20 准则
- 条款17 考虑使用lazy evaluation(懒惰计算法)
- 条款18 分期摊还期望的计算
- 条款19 理解临时对象的来源
- 条款20 协助完成返回值优化
- 条款21 通过重载避免隐式类型转换
- 条款22 考虑用运算符的赋值形式(op=)取代其单独形式(op)
- 条款23 考虑变更程序库
- 条款24 理解虚拟函数、多继承、虚基类和RTTI所需的代价
- 条款25 将构造函数和非成员函数虚拟化
- 条款26 限制某个类所能产生的对象数量
- 条款27 要求或禁止在堆中产生对象
- 条款28 智能指针
- 条款29 引用计数
- 条款30 代理类
条款1 指针与引用的区别
- 引用应该被初始化,指针可以不被初始化。
- 不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。
- 指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是
引用则总是指向在初始化时被指定的对象,以后不能改变。
条款2 尽量使用C++风格的类型转换
- const_cast 最普通的用途就是转换掉对象的 const 属性
- dynamic_cast,它被用于安全地沿着类的继承关系向下进行类型转换。这就是说,你能用 dynamic_cast 把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。它不能被用于缺乏虚函数的类型上。
- 如你想在没有继承关系的类型中进行转换,你可能想到 static_cast。
- reinterpret_cast,使用这个操作符的类型转换,其 的 转 换 结 果 几 乎 都 是 执 行 期 定 义 ( implementation-defined )。 因此,使用reinterpret_casts 的代码很难移植。reinterpret_casts 的最普通的用途就是在函数指针类型之间进行转换。
条款3 不要对数组使用多态
- 多态和指针算法不能混合在一起来用,所以数组与多态也不能用在一起。
条款4 避免无用的缺省构造函数
- 提供无意义的缺省构造函数也会影响类的工作效率。如果成员函数必须测试所有的部分是否都被正确地初始化,那么这些函数的调用者就得为此付出更多的时间。而且还得付出更多的代码,因为这使得可执行文件或库变得更大。它们也得在测试失败的地方放置代码来处理错误。如果一个类的构造函数能够确保所有的部分被正确初始化,所有这些弊病都能够避免。缺省构造函数一般不会提供这种保证,所以在它们可能使类变得没有意义时,尽量去避免使用它们。
条款5 谨慎定义类型转换函数
- 类型转换:有两种函数允许编译器进行这些的转换:单参数构造函数( single-argument constructors)和隐式类型转换运算符。单参数构造函数就是只有一个参数的构造函数(或者多个参数,但是除了第一个参数之外,其他参数均有默认值);隐式类型转换运算符就是operator 关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。
- 一般来说,越有经验的 C++程序员就越喜欢避开类型转换运算符。例子,在打印Rational类实例时,你忘了为 Rational 对象定义 operator<<。你可能想打印操作将失败,因为没有合适的的 operator<<被调用。但是你错了。当编译器调用 operator<<时,会发现没有这样的函数存在,但是它会试图找到一个合适的隐式类型转换顺序以使得函数调用正常运行。类型转换顺序的规则定义是复杂的,但是在现在这种情况下,编译器会发现它们能调用Rational::operator double 函数来把 r 转换为 double 类型。所以上述代码打印的结果是一个浮点数,而不是一个有理数。
- 构造函数用 explicit 声明,如果这样做,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法。
- 如果编译器不支持explicit,可以使用代理类。代理类的每一个对象都为了支持其他对象的工作。例如Array类和嵌套子类ArraySize类的例子。
- 让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。
条款6 自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别
- C++规定后缀形式有一个 int 类型参数,当函数被调用时,编译器传递一个 0 做为 int 参数的值给该函数。前缀形式返回一个引用,后缀形式返回一个 const 类型。从你开始做 C 程序员那天开始,你就记住 increment 的前缀形式有时叫做“增加然后取回”,后缀形式叫做“取回然后增加”。
UPInt& operator++(); // ++ 前缀
const UPInt operator++(int); // ++ 后缀
- 让我们明确一下,当处理用户定义的类型时,尽可能地使用前缀increment,因为它的效率较高。
- 后缀++,- -的形式需要参照前缀++和- -的形式实现。
条款7 不要重载“&&”,“||”, 或“,”
- 与 C 一样,C++使用布尔表达式短路求值法(short-circuit evaluation)。这表示一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。
条款8 理解各种不同含义的new和delete
- new operator、operator new、 placement new、 array new、 array delete
条款9 使用析构函数防止资源泄漏
- 就是RAII的思想
条款10 在构造函数中防止资源泄漏
- C++拒绝为没有完成构造操作的对象调用析构函数
- 如果你用对应的 auto_ptr 对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const 指针一样使用 const 指针,给其赋值。
条款11 禁止异常信息(exceptions)传递到析构函数外
- 在有两种情况下会调用析构函数。第一种是在正常情况下删除一个对象,例如对象超出了作用域或被显式地 delete。第二种是异常传递的堆栈辗转开解(stack-unwinding)过程中,由异常处理系统删除一个对象。
- 异常没有被 Session 的析构函数捕获住,所以它被传递到析构函数的调用者那里。但是如果析构函数本身的调用就是源自于某些其它异常的抛出,那么 terminate 函数将被自动调用,彻底终止你的程序。
- 我们知道禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止 terminate 被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。
条款12 理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
- 你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
- try…catch块中,异常通常不会做类型转换。不过在 catch 子句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类间的转换。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有 const void* 指针的 catch 子句能捕获任何类型的指针类型异常
- 综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。最后一点,catch 子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的 catch 将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。
条款13 通过引用(reference)捕获异常
- 当你写一个 catch 子句时,必须确定让异常通过何种方式传递到 catch 子句里。你可以有三个选择:与你给函数传递参数一样,通过指针(by pointer),通过传值(by value)或通过引用(by reference)。
- 如果你通过引用捕获异常(catch by reference),你就能避开上述所有问题,不会为是否删除异常对象而烦恼;能够避开 slicing 异常对象;能够捕获标准异常类型;减少异常对象需要被拷贝的数目。
条款14 审慎使用异常规格
- 毫无疑问,异常规格是一个引人注目的特性。它使得代码更容易理解,因为它明确地描述了一个函数可以抛出什么样的异常。
- 我们几乎不可能为一个模板提供一个有意义的异常规格。因为模板总是采用不同的方法使用类型参数。解决方法只能是模板和异常规格不要混合使用。
条款15 了解异常处理的系统开销
- 粗略地估计,如果你使用 try 块,代码的尺寸将增加 5%-10%并且运行速度也同比例减慢。
- 不论异常处理的开销有多大我们都得坚持只有必须付出时才付出的原则。为了使你的异常开销最小化,只要可能就尽量采用不支持异常的方法编译程序,把使用 try 块和异常规格限制在你确实需要它们的地方,并且只有在确为异常的情况下(exceptional)才抛出异常.
条款16 牢记 80-20 准则
- 80-20 准则说的是大约 20%的代码使用了 80%的程序资源;大约 20%的代码耗用了大约 80%的运行时间;大约 20%的代码使用了 80%的内存;大约 20%的代码执行 80%的磁盘访问;80%的维护投入于大约 20%的代码上。
条款17 考虑使用lazy evaluation(懒惰计算法)
- 还记得么?当你还是一个孩子时,你的父母叫你整理房间。你如果象我一样,就会说“好的“,然后继续做你自己的事情。你不会去整理自己的房间。在你心里整理房间被排在了最后的位置,实际上直到你听见父母下到门厅来查看你的房间是否已被整理时,你才会猛跑进自己的房间里并用最快的速度开始整理。如果你走运,你父母可能不会来检查你的房间,那样的话你就能根本不用整理房间了。 同样的延迟策略也适用于具有五年工龄的 C++程序员的工作上。在计算机科学中,我们尊称这样的延迟为 lazy evaluation(懒惰计算法)。
- 应用领域例子
a. 引用计数。除非你确实需要,不去为任何东西制作拷贝。我们应该是懒惰的,只要可能就共享使用其它值。在一些应用领域,你经常可以这么做。
b. 区别对待读取和写入。
c. Lazy Fetching(懒惰提取)
d. Lazy Expression Evaluation(懒惰表达式计算)
条款18 分期摊还期望的计算
- 在本条款中我提出的建议,即通过 over-eager 方法分摊预期计算的开销,例如 caching和 prefething,这并不与我在条款 M17 中提出的有关 lazy evaluation 的建议相矛盾。当你必须支持某些操作而不总需要其结果时,lazy evaluation 是在这种时候使用的用以提高程序效率的技术。当你必须支持某些操作而其结果几乎总是被需要或被不止一次地需要时,over-eager 是在这种时候使用的用以提高程序效率的一种技术。它们所产生的巨大的性能提高证明在这方面花些精力是值得的。
条款19 理解临时对象的来源
- 在 C++中真正的临时对象是看不见的,它们不出现在你的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时
- C++语言禁止为非常量引用(reference-to-non-const)产生临时对象
- 在任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性。在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)
条款20 协助完成返回值优化
- 这种特殊的优化――通过使用函数的 return 位置(或者在函数被调用位置用一个对象来替代)来消除局部临时对象――是众所周知的和被普遍实现的。它甚至还有一个名字:返回值优化(return value optimization)
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
即,是返回 constructor argument 而不是直接返回对象
条款21 通过重载避免隐式类型转换
- 在 C++中有一条规则是每一个重载的 operator 必须带有一个用户定义类型(user-defined type)的参数
条款22 考虑用运算符的赋值形式(op=)取代其单独形式(op)
- 这里谈论的命名对象、未命名对象和编译优化是很有趣的,但是主要的一点是 operator的赋值形式(operator+=)比单独形式(operator+)效率更高。做为一个库程序设计者,应该两者都提供,做为一个应用程序的开发者,在优先考虑性能时你应该考虑考虑用 operator赋值形式代替单独形式。
条款23 考虑变更程序库
条款24 理解虚拟函数、多继承、虚基类和RTTI所需的代价
- 实际上虚函数不能是内联的
- 运行时类型识别(RTTI)。RTTI 能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询。这些信息被存储在类型为 type_info 的对象里,你能通过使用 typeid 操作符访问一个类的 type_info 对象。
- RTTI被设计为在类的 vtbl 基础上实现。
条款25 将构造函数和非成员函数虚拟化
- 这里所说的虚拟化,指的是在构造函数中调用虚函数,在非成员函数中调用虚函数。
- 理论上,构造函数中调用虚函数是不会有多态的行为,但是在拷贝构造中调用虚函数是可行的,原型模式就是这种设计思想
条款26 限制某个类所能产生的对象数量
条款27 要求或禁止在堆中产生对象
条款28 智能指针
条款29 引用计数
- string类设计使用引用计数,当有两个字符串指向同一个字符串时,需要修改其中一个字符串,应该使用写时拷贝的思想。
条款30 代理类
- string类的“[]”运算符,无法得知是左值操作还是右值操作。因此需要使用代理类。具体链接,只能说,太妙了