我之前边读《Effective C++》边写下每个条款的读书笔记,这一版是C++11之前的版本。这里我将每个条款令我印象深刻的点小结一下。
1、C++包括:Plain C(面向过程)、OOP(面向对象)、模板(泛型和模板元编程)、STL(C++标准库)。
2、用inline、enum、const代替#define。#define定义的宏,一旦复杂起来,高手都很难掌控。不要带入C的习惯。
3、灵活使用const前缀。不需要进行改变的数据加上const前缀。指针的const前缀有两种形式,const放在*左边表示数据不变,放右边表示指针地址不变。返回不需要修改的成员数据的成员函数需要加上const标记。
4、使用对象前确保初始化。简单的内置数据结构(POD),有些编译器都会直接初始化为0。自定义数据结构,尤其是包含指针的类,最好要有明确定义的初始化过程,指针要赋给0或者NULL值。
5、了解C++暗地里编写和调用了什么函数。类会自动生成default 构造函数,copy 构造函数和copy assign函数,如果不明确定义拷贝构造函数,那么类对象拷贝的时候只进行浅拷贝操作,有可能与原有意图不符。因此需要明确定义这些函数,不让C++编译器自动生成。
6、可以拒绝使用编译器生成的默认函数。方法是把这些默认函数放到private里面,这样程序就无法使用默认函数了。
7、为多态基类声明virtual析构函数。如果基类没有定义virtual析构函数,那么当我们用基类指针指向derived 对象,通过基类指针析构derived对象,无法调用derived对象的析构函数。所以要预先为base类编写virtual 析构函数,这样基类和派生类都会执行析构函数。STL不是完全的面向对象思想,比如没有虚析构函数,因此不能以STL类为基类实现多态。
8、别让异常逃离析构函数。在处理异常之前,先要保证资源都析构了,不然会发生资源泄露导致bug;如果不行,那么终止程序是最保险的办法。保证资源析构的可行的办法使用资源管理类或者智能指针管理资源。
9、绝不在构造和析构中调用virtual函数。derived实例构造时,base构造部分如果调用virtual函数,则调用的是base类的virtual函数而不是derived类的virtual函数。
10、 令operator=返回一个reference to *this。这是标准,很多人会忘记。
11、 在operator=中处理“自我赋值”。判断参数是不是本身,如果是本身,就直接返回,不要做没用的事情。
12、 复制对象不要忘了任何一个成分。因为忘记复制对象是不会报警告和错误的,靠开发者自己仔细编写。
13、 以对象管理资源。由于基本数据类型是不存在资源泄露问题的,因此我们可以定义对象的行为使其与基本数据类型相似。让对象管理资源,让对象行为类似基本数据类型,这样就可以像基本数据类型一样操作自定义对象。资源管理类和智能指针是常用的两种资源管理对象。
14、 在资源管理类中小心coping行为。资源管理类可以有四种coping行为:a禁止复制,比如继承boost的Uncopyable类b引用计数,不复制资源只是增加一个引用,比如智能指针c复制资源d转移使用权,比如std::auto_ptr,比较少用。
15、 在资源管理类中提供对原始资源的访问。对资源管理类的最优设计是让资源管理类用起来和基本数据类型一样。有必要设计一个好接口用来访问资源管理类封装的原始资源。
16、 成对使用new/delete要采用相同形式。如果用new,就使用delete;如果用new [],就使用delete [];用malloc申请资源,就用free析构资源。
17、 以独立语句将newed对象置入智能指针。processWidget(std::tr1::shared_ptr<Widget> (new Widget),priority());有三种操作:1、new Widget2、构造shared_ptr3、priority函数。编译器对上面的执行步骤不确定,所以如果执行顺序恰好这样1、new Widget2、priority函数3、构造shared_ptr。如果在priority函数执行时抛出异常,那么构造shared_ptr这一步就没有发生,于是Widget泄露了。
18、 让接口容易正确使用,不易被误用。设计接口行为最好和内置数据类型一致。
19、 设计class犹如设计type。设计需要注意:1、创建与销毁2、初始化和赋值是不同的3、如何复制4、错误检查5、继承关系6、类型转化7、有哪些合法行为8、明确拒绝使用某些函数9、成员数据的存取操作接口10、是否使用模板泛化11、没有接口的属性怎么处理12、需要设计这样一个类吗。
20、 尽量用const引用传递代替值传递,来传递不可修改的数据。如果是基本数据类型,值传递和const引用传递没有区别。如果传递的数据很大那么值传递会消耗大量工作在构造复制数据上面。
21、 必须返回对象时,别妄想返回其reference。局部作用域的对象必须返回值,不可返回引用,因为程序运行走出作用域,局部对象会被自动析构。
22、 将成员变量声明为private。遵循OOP的封装原则。在测试和调试的时候其实可以临时变通的。
23、 宁可以non-member、non-friend替换member函数。使用非成员函数可以避免类变得冗杂,而且扩展性较强。第二个原因见24条。
24、 若所有参数需要类型转换,请使用non-member函数。双目重载运算符成员函数只能对第二个参数进行类型转换。重载双目运算符非成员函数,可以对运算符号前/后对象进行类型转换。
25、 考虑写出一个不抛出异常的swap函数。Swap用于高效交换两个对象的数据,内部使用了很多技巧,多用swap可以提升程序运算速度,如果swap函数鲁棒性强不抛出异常,那么程序会健壮很多。
26、 尽可能延迟变量定义式的出现时间。标准C要求在程序开头定义好变量。在C++中,可以在要用数据的时候才定义变量,这样方便程序的开发。
27、 尽量少做转型操作。转型操作会改变数据,容易让开发者写出bug。
28、 避免返回handle指向对象内部成分。使用类提供的数据存取操作函数,不要直接返回引用、指针、迭代器。
29、 为异常安全而努力是值得的。异常安全有两个条件:1、不泄露资源2、不允许影响其他数据。异常安全等级有三个:1、基本承诺:抛出异常,但是程序仍然可以使用。不过当前状态是不可知的2、强烈保证:抛出异常,程序状态不变。如果成功就完全成功,如果失败就回滚到原始状态3、不抛掷异常,绝对成功。大部分情况下需要保证前两个异常安全等级;swap要保证第三个等级。
30、 透彻了解inlining的里里外外。Inline函数会展开到调用处,相当于安全性很高的#define函数;inline函数可以预先声明;inline函数放在头文件中;类中的函数,如果放在头文件里面,就是inline函数;inline函数可以模板化;inline函数不能作为虚函数;inline函数是小函数,如果规模太大会造成程序膨胀。
31、 将文件间的编译依存关系降至最低。保持模块之间的低耦合度;采用pimpl技术,用指针指向实现模块,这样将实现和接口分离。
32、 确定你的public继承塑造出is-a关系。D继承自B,则D是B的一个特例,B能用的地方,D也可以。
33、 避免遮掩继承来的名称。如果覆盖了基类中的可用的函数foo,那么使用B.foo和使用D.foo的效果是不同的,有可能导致B能用的地方D不能用。
34、 区分接口继承和实现继承。1、non-virtual函数表示这个函数被继承之后也没有更改的必要,属于接口+强制实现继承2、pure virtual函数表示只继承接口。3、impure virtual函数表示继承接口和缺省实现
35、 考虑virtual函数接口以外的方法。虚函数可以根据类别自动调用1、非虚函数接口(NVI)接口不是虚函数,调用虚函数,安全性更好,可以继承缺省参数2、函数指针,类似于委托的方法3、C++11的std::function方法,可以将普通函数、函数对象、成员函数、lambda都当做类似于函数指针的对象4、策略设计模型
36、 不要重新定义继承而来的non-virtual函数。非虚函数是接口+实现继承,原则上不能重新定义;此外要保证基类使用的地方派生类也能使用,也不能重新定义基类自有的非虚函数。
37、 不要重新定义继承而来的缺省参数。36条说明了非虚函数是不能重定义的,因此缺省参数也最好不要修改;对于虚函数,由于虚函数是动态绑定,缺省参数是静态绑定,因此缺省函数不具有动态性,比如派生类D继承基类B, class B{public:virtual void foo(int i = 10){printf(“B :%d”,i);}};class D:public B{public:virtual void foo(int i = 20){printf(“D: %d”,i);}};,现在有指向D的B类指针,调用B.foo()实际上调用的是foo(inti = 10){printf(“D:%d”,i);}。
38、 通过复合塑模出has-a或“根据某物实现出”。has-a表示组成,组成类C通过内嵌的类D实现某种功能,C包含D;可以类比设计模式中的composite模式。
39、 明智而审慎地使用private继承。Private继承是实现继承,不继承接口,比较难以驾驭。一般使用public继承+复合或者pimpl来代替private继承。
40、 明智而审慎地使用多重继承。多继承难以驾驭,能用单继承和其他模式的,就不要用多重继承;多重继承会给大型系统带来不必要的损耗,导致程序体积膨胀、性能下降。
41、 了解隐式接口与编译期多态。模板类的模板类型T,虽然不清楚T的类型,但是实际使用中可能会使用T的接口。如果T没有给定的接口,在编译时就会报错。
42、 了解typename的双重意义。在表示模板的时候typename和class可以通用。如果使用模板类型的内部定义,那么需要添加typename。T::const_iterator it1 = ...//编译报错 typenameT::const_iterator it2 = ....//正确
43、 学习处理模板基类内的名称。模板类D<T>继承模板基类B<T>,如果直接在D中使用B的函数,那么编译器可能报错。这时候要显式标明我们使用B<T>的函数:using B<T>::foo;foo()或者B<T>::foo()
44、 将与参数无关的代码抽离template。假设类D有两个模板参数<T,U>,每个模板参数可能有4种选择,那么编译器实作所有的模板类,会产生4*4=16个类。如果类 D<T,U>的实现可以分给类B<T>和C<U>,这样编译器产生的类就有4个B,4个C和4个D,只有12个类。如果模板参数的选择更多,那么这种方式节约的资源会更加明显。
45、 运用成员函数模板接受所有兼容类型。类模板和成员模板可以用两种模板。方法类似44条。
46、 需要类型转换时请为模板定义非成员函数。原因同24。
47、 请使用traits class表现类型信息。Traits技术是;程序会根据类内部的成员类型来调用针对该类的函数。Traits技术代替了if判断语句+typeid函数的传统方法,转而利用编译器来选择相应的处理函数。该类方法广泛用在STL编程中,亦称为type-trait技术。
48、 认识template元编程。指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作,模板元编程常常实现后者。模板元编程的优点有:1将工作从运行期转移到编译期,使得错误更容易发现2可执行文件占用内存低,运行时间短。缺点有1十分冗长的编译时间2使用难度大,被称为“学院派编程”。
49、 了解new-handler的行为。当堆分配函数operator new分配出错,就会调用一个错误处理函数,它可以是用户指定的函数,这就是new-handler,new_handler函数无返回值,没有参数,一般最好抛出一个异常。当operator new函数失败,没有申请到内存,会不断调用new_handler函数,知道找到足够内存。因此一个不做任何事情的new_handler函数会导致程序假死等不正常的行为。正常的new_handler函数可以选择做以下事情:1释放其他内存,提供可用内存2安装另外一个new_handler,根据实际情况做些其他事情3抛出bad_alloc异常4调用abort或者exit。
50、 了解New和Delete的合理替换时机。使用自定义new和delete的情景:1在调试时检测和输出运行的错误2强化效能。典型的就是内存池技术,减少内存碎片,减少申请空间和释放空间的消耗3收集内存运行信息。普通程序猿写出高效通用的内存管理工具很难,一般使用著名的开源工具或者购买商用库。
51、 编写new和delete时需固守常规。New和delete有常规通用的标准模式。
52、 写了placement new也要写placement delete。如果我们编写了自定义的placement new函数,结果出错了,那么系统就会调用placement delete函数来释放空间。如果我们写了placement new而没有编写placement delete函数,那么系统会不会释放空间就不知道了
53、 不要轻易忽视编译器的警告。编译器的警告不一定是简单的问题:如果某些代码按照你的原始意图来说确实是错误的,但是编译器恰恰又可以按照其他语意使其编译通过,这样很难找到错误所在。GCC对 ISO C++标准贯彻的很好,在GCC最高警告级别下,编写没有警告的代码,基本上可以在任何平台的C++编译器上编译通过。
54、 熟悉包括TR1在内的标准程序库。在C++11正式版出来之前,C++11部分特性已经封装在较新的编译器,如vs2008sp1的tr1模块中。其他大部分还未正式纳入C++11标准的模块,存放在boost库中。这些准标准库可以大大减轻工作量。
55、 熟悉boost。如果已经开始使用C++11标准,那么boost库里面很多东西你会觉得眼熟。Boost库里面未纳入标准的模块,可以应用在很多开源环境中;如果是高强度的工业环境中,使用boost库需要慎重。