每一个Item都很经典,都需要去思考揣摩,我在这里将要点抽象出来,便于日后快速回忆;我只是在做文章的“搬运工”。
Item 18 使接口易于正确使用
1. function接口,class接口,template接口......每一种接口都是客户(调用者)与你的代码互动的手段。
2. 防止可能的客户(调用)错误的另一个方法是:限制类型内什么事可以做什么事不能做;施加限制的一个普通方法就是加上 const。
3. 避免自定义与内置类型不兼容的一个方法是提供行为一致的接口。很少有其他性质比得上“一致性”更能使“接口容易被正确使用”,也很少有其它性质比得上“不一致性”更加剧接口的恶化。
4. shared_ptr 能使接口易于正确使用,一个消除某些客户错误的简单方法。但它比一个裸指针大,比一个裸指针慢,而且要使用辅助的动态内存。在许多应用程序中,这些附加的运行时开销并不显著,而对客户错误的减少却是每一个人都看得见的。另外shared_ptr将动态分配内存用于簿记和 deleter 专用(deleter-specific)数据,当调用它的 deleter 时使用一个虚函数来调用,在一个它认为是多线程的应用程序中,当引用计数被改变,会导致线程同步开销。
小节:不同的function,class,template之间的调用、交互、读、写都是接口操作。
疑问:如果想深入理解shared_ptr,还需要很多时间,暂时跳过,后面再看。
Item 19 视类设计为类型设计
1. 像语言设计者一样在语言内建类(类型)的设计中倾注大量心血。
小节:这一小节中的所有内容都包含在其它小节中,看完之后再回来想想,就不再拷贝了。
Item 20 传“常量引用”取代传值
1. 传const引用, 没有任何构造函数和析构函数被调用,因为没有新的对象被构造。
2. 以传引用方式传递参数还可以避免切断问题(slicing problem)。当一个派生类对象作为一个基类对象被传递(传值方式),基类的拷贝构造函数被调用,而那些使得对象的行为像一个派生类对象的特殊特性被“切断”了。
3. 如果你有一个内建类型的对象(例如,一个 int),以传值方式传递它常常比传引用方式更高效。那么,对于内建类型,当你需要在传值和传引用给 const 之间做一个选择时,没有道理不选择传值。同样的建议也适用于 STL 中的迭代器(iterators)和函数对象(function objects),因为,作为惯例,它们就是为传值设计的。迭代器(iterators)和函数对象(function objects)的实现有责任保证拷贝的高效并且不受切断问题的影响。
疑问:STL 中的迭代器(iterators)和函数对象(function objects)使用值传递为什么也高效呢?
Item 21 当你必须返回一个对象时不要试图返回一个引用。
1. 一个引用仅仅是一个名字,一个实际存在的对象的名字。无论何时只要你看到一个引用的声明,你应该立刻问自己它是什么东西的另一个名字,因为它必定是某物的另一个名字。
2. 任何返回一个引向局部变量的引用的函数都是错误的。(对于任何返回一个指向局部变量的指针的函数同样成立。)
2. 返回一个引向堆内存的引用,通过 new 分配的内存要通过调用一个适当的构造函数进行初始化,但是现在你有另一个问题:谁是删除你用 new 做出来的对象的合适人选?
3. 返回一个引向局部static的引用,这个会立即引起我们的线程安全(thread-safety)的混乱。
小节:本小节讲了不少内容,实际都是:绝不要返回一个局部栈对象的指针或引用,绝不要返回一个被分配的堆对象的引用,绝不要返回一个局部static 对象的指针或引用。
Item 22, 将数据成员声明为private
1. 使用函数(相对于变量)可以让你更加精确地控制成员的可存取性。
2. 将数据成员隐藏在功能性的接口(函数)之后能为各种实现提供弹性。例如,它可以在读或者写的时候很简单地通报其他对象,可以检验类的不变量以及函数的前置或后置条件,可以在多线程环境中执行同步任务,等等。
3. 关于封装的要点可能比它最初显现出来的更加重要。如果你对你的客户隐藏你的数据成员(也就是说,封装它们),你就能确保类的不变量总能被维持,因为只有成员函数能影响它们。
4. 假设我们有一个 public 数据成员,随后我们消除了它。有多少代码会被破坏呢?所有使用了它的客户代码,其数量通常大得难以置信。从而 public 数据成员就是完全未封装的。假设我们有一个 protected 数据成员,随后我们消除了它。现在有多少代码会被破坏呢?所有使用了它的派生类,典型情况下,代码的数量还是大得难以置信。从而 protected 数据成员就像 public 数据成员一样没有封装,因为在这两种情况下,如果数据成员发生变化,被破坏的客户代码的数量都大得难以置信。从封装的观点来看,实际只有两个访问层次:private(提供了封装)与所有例外(没有提供封装)。
小节:私有数据可以被自己自由改变,而不会影响客户端。
Item 23, 用非成员非友元函数取代成员函数
1. 面性对象原则指出:数据和对它们进行操作的函数应该被绑定到一起,而且建议成员函数是更好的选择。不幸的是,这个建议是不正确的。它产生于对面向对象是什么的一个误解。面向对象原则指出数据应该尽可能被封装。在很多方面非成员方法比一个成员函数更好。
2. 如果某物被封装,它被从视线中隐藏。越多的东西被封装,就越少有东西能看见它。越少有东西能看见它,我们改变它的弹性就越大,因为我们的改变仅仅直接影响那些能看见我们变了什么的东西。某物的封装性越强,那么我们改变它的能力就越强。这就是将封装的价值评价为第一的原因:它为我们提供一种改变事情的弹性,而仅仅影响有限的客户。结合一个对象考虑数据。越少有代码能看到数据(也就是说,访问它),数据封装性就越强,我们改变对象的数据的特性的自由也就越大,比如,数据成员的数量,它们的类型,等等。作为多少代码能看到一块数据的粗糙的尺度,我们可以计数能访问那块数据的函数的数量:越多函数能访问它,数据的封装性就越弱。
疑问:如果是纯面向对象编程(只有类,没有非成员函数),还需要这一小节吗?我承认非成员函数有更强的封装性。或许我还没有理解面向对象编程。
Item 24, 当类型转换应该用于所有参数时,声明为非成员函数。
1. 如果你需要在一个函数的所有参数(包括被 this 指针所指向的那个)上使用类型转换,这个函数必须是一个非成员。
2. 与成员函数相对的是非成员函数,而不是友元函数。太多的程序员假设如果一个函数与一个类有关而又不应该作为成员时(例如,因为所有的参数都需要类型转换),它应该作为友元。这个示例证明这样的推理是有缺陷的。无论何时,只有你能避免友元函数,你就避免它,因为,就像在现实生活中,朋友的麻烦通常多于他们的价值。
小节:
使用
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
代替
class Rational
{
...
const Rational operator*(const Rational& rhs) const{}
...
};
Item 25 考虑支持不抛异常的swap
小节:看的有点糊涂,标题是不抛异常的swap,我却很少看到如何保证不抛异常;或许是我没有读懂,对template也不太熟,在这里做个笔记。