A person who is virtuous is also courteous.
"有德者必知礼"
书本介绍:《Effective C++:改善程序与设计的55个具体做法》(中文版)(第3版)
一、让自己习惯C++
1、如今的C++是一个多重范型的编程语言,同时支持过程形式(procedural)、面向对象形式(object-oriented)、函数形式(functional)、泛型形式(generic)、元编程形式(metaprogramming);
2、因为或许 #define 不被视为语言的一部分,你所使用的名称可能并未进入记号表(symbol table),所以:
①对于单纯常量,最好以 const 对象或 enums 替换 #defines ;
②对于形似函数的宏(macros),最好用 inline 函数替换 #defines ;
3、 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复;
4、 class 成员的初始化次序总是相同:先 base classes 成员,后 derived classes 成员; class 内的成员按声明次序被初始化;
5、所谓 static 对象,其寿命从被构造出来直到程序结束为止。这种对象包括: global 对象、定义于 namespace 作用域内的对象、在 classes 内、在函数内、以及在file作用域内被声明为 static 的对象;
6、因为C++不保证为内置对象进行默认初始化,所以我们必须进行手工初始化;
7、构造函数最好使用成员成员初值列,而不要在构造函数本体内使用赋值操作; 初值列列出的成员变量,其排列次序应该和他们在 class 中的声明次序相同;
8、为了避免“跨编译单元之初始化次序”问题,应该用 local static 对象替换 non-local static 对象;
二、构造/析构/赋值运算
1、当用户没有声明时,编译器将会自动声明一个构造函数、一个 copy 构造函数、一个 copy assignment 操作符和一个析构函数,但是只有当这些函数被调用的时候才会被编译器创建出来;
2、在以下几种情况下,编译器不会生成默认的 copy assignment 操作符:
① class 内包含 reference 成员;
② class 内包含 const 成员;
③ base classes 将 copy assignment 声明为 private ;
3、为了阻止编译器自动声明的行为,可以将相应的成员函数声明为 private 并且不予实现,或者声明一个空基类,将对应的成员函数声明为 private 并 private 继承它;
4、带多态性质的(polymorphic) base classes 应该声明一个 virtual 析构函数;只要 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数;
5、析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下(不传播)或结束程序;
6、如果要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作;
7、在 derived class 对象的 base class 构造期间,对象的类型是 base class 而不是 derived class , virtual 函数不是 virtual 函数,对象在 derived class 的构造函数执行之前不会成为一个 derived class 对象,所以不会发生动态绑定,此时调用 virtual 函数将得不到正确的版本;
8、重载赋值运算符(=,+=,-=,*=等)时应该返回一个 reference to *this ;
9、为了确保对象自我赋值时 operator= 有良好行为,我们可以通过 比较“来源对象”和“目标对象”的地址、合理安排语句顺序、以及 copy-and-swap 技术来实现 operator= ;
10、必须确保:任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确;
11、当编写一个 copying 函数时,请确保:①复制所有 local 成员变量;②调用所有 base classes 内的适当 copying 函数;
12、如果你发现你的 copy 构造函数和 copy assignment 操作符有相近的代码时,不要尝试以某个 copying 函数实现另一个 copying 函数,应该将相同的功能实现为一个名为 init 的 private 函数以供两个 copying 函数共同调用;
三、资源管理
1、以对象管理资源,又被称为 资源取得时机便是初始化时机(Resource Acquisition Is Initialization; RAII),它有两个关键想法:
①获得资源后立刻放进管理对象内;
②管理对象运用析构函数确保资源被释放;
2、复制RAII对象必须一并复制它所管理的资源,所以资源的 copying 行为决定RAII对象的 copying 行为;
3、APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的方法;
4、成对使用 new 和 delete 时要采取相同形式,最好尽量不要对数组形式做 typedefs 动作;
5、由于编译器对于“跨越语句的各项操作”没有重新排列的*,所以应该以独立语句将newed对象存储于智能指针内,在作为函数参数;如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏;
四、设计与声明
1、让接口容易被正确使用,不易被误用:
①“促进正确使用”的方法包括接口的一致性,以及与内置类型的行为兼容;
②“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;
2、把设计一个 class 当作设计一个 type ,在设计一个 class 的时候,请先考虑以下问题: (重点!!!)
①新 type 的对象应该如何被创建和销毁?(构造和析构)
②对象的初始化和对象的赋值该有什么样的差别?(资源管理)
③新 type 的对象如果被 passed by value (以值传递),意味着什么?(资源管理)
④什么是新 type 的“合法值”?(接口和异常)
⑤你的新 type 需要配合某个继承图系(inheritance graph)吗?(继承和派生)
⑥你的新 type 需要什么样的转换?(类型转换)
⑦什么样的操作符和函数对此新 type 而言是合理的?(运算符)
⑧什么样的标准函数( copy 构造等)应该驳回(禁止编译器自动声明和创建)?(拷贝控制)
⑨谁该取用新 type 的成员?(成员的访问权限)
⑩什么是新 type 的“未声明接口”(underclared interface)?(资源管理、异常和效率)
+1:你的新 type 有多么一般化?(类模板)
+2:你真的需要一个新 type 吗?(继承和派生)
3、对于自定义类型,尽量以 pass-by-reference-to-const 替换 pass-by-value ,这通常比较高效,并且可以避免切割问题;对于内置类型、STL的迭代器和函数对象, pass-by-value 往往比较适当;
4、绝对不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象;
5、切记:将成员变量声明为 private 。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供 class 作者以充分的实现弹性;
6、如果你需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个 non-member ;
7、当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常;
8、如果你提供一个 member swap ,也该提供一个 non-member swap 来调用前者。对于 classes (而非 templates ),也请特化 std::swap ;
9、调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何“命名空间资格修饰”;
五、实现
1、对象在定义时总会伴随着构造函数的执行,所以应该尽可能延后对象或变量定义式的出现,这样可以增加程序的清晰度并改善程序效率;
2、单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如“以Base*指向它”时的地址和“以Derived*指向它”时的地址);
3、如果可以,尽量避免转型;如果转型是必要的,试着将它隐藏于某个函数背后;宁可使用C++-style转型,不要使用旧式转型;
4、避免返回handles(包括referencees、指针、迭代器)指向对象内部,这样可以增加封装性,帮助 const 成员函数的行为像个 const ;
5、异常安全函数应该提供以下三个保证之一:
①基本承诺:如果异常被抛出,程序内的任何事物任然保持在有效状态下;
②强烈保证:如果异常被抛出,程序状态不改变;( copy-and-swap )
③不抛掷保证:承诺绝不抛出异常,因为他们总是能够完成它们原先承诺的功能;
6、将大多数 inlining 限制在小型、被频繁调用的函数身上,这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化;
7、支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes;
8、程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及 templates 都适用;
六、继承与面向对象设计
1、 public inheritance (公开继承)意味着“is-a”(是一种)关系。适用于 base class 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象;
2、世界上并存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来;
3、如果你继承 base class 并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被掩盖的每个名称引入一个 using 声明式;
4、 base class 中为 pure virtual 函数给出一份实现可以将该函数分割为两个基本要素:其声明部分表现的是接口(那是 derived classes 必须使用的),其定义部分则表现出缺省行为(那是 derived classes 可能使用的,但只有在它们明确提出申请时才是);
5、对于接口继承与实现继承:
① pure virtual 函数只具体指定接口继承;
② impure virtual 函数具体指定接口继承及缺省实现继承;
③ non-virtual 函数具体指定接口继承以及强制性实现继承;
6、当你为解决问题而寻找某个设计方法时,不妨考虑 virtual 函数的替代方案:
①使用non-virtual interface(NVI)手法;
②将 virtual 函数替换为“函数指针成员变量”;
③以 tr1::function 成员变量替换 virtual 函数;
④将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数;
7、任何情况下都不应该重新定义一个继承而来的 non-virtual 函数;
8、绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数却是动态绑定;
9、如果 classes 之间的继承关系是 private :
①编译器不会自动将一个 derived class 对象转换为一个 base class 对象;
②由 private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性;
10、多重继承比单一继承复杂,他可能导致新的歧义性,以及对 virtual 继承的需要,而 virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本;只有当 virtual base classes 不带任何数据时,它才最具实用价值;
七、模板与泛型编程
1、 classes 和 template 都支持接口(interfacees)和多态(polymorphism):
①对 classes 而言,接口是显式的(explicit),以函数签名为中心,多态则是通过 virtual 函数发生于运行期;
②对 template 参数而言,接口是隐式的(implicit),奠基于有效表达式,多态则是通过 template 具现化和函数重载解析(function overloading resolution)发生于编译期;
2、声明 template 参数时,前缀关键字 class 和 typename 可互换;
3、使用关键字 typename 标识嵌套从属类型名称,但不得在base class lists(基类列表)或member initialization list(成员初始列表)内以它作为 base class 修饰符;
4、如何使用基类模板中的名称:
①在 base class 函数调用动作之前加上“ this-> ”;
②使用 using 声明式;
③明白指出被调用的函数位于 base class 内;
5、 templatees 生成多个 classes 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系;
6、因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数或 class 成员变量替换 template 参数;
7、因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码;
8、使用member function templatees(成员函数模板)生成“可接受所有兼容类型”的函数;
9、当编写一个 class template ,而它所提供之“与此 template 相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“ class template 内部的 friend 函数”;
10、如何设计并实现一个 traits class :
①确认若干你希望将来可取得的类型相关信息;
②为该信息选择一个名称;
③提供一个 template 和一组特化版本,内含你希望支持的类型相关信息;
11、如何使用一个 traits class :
①建立一组重载函数或函数模板,彼此间的差异只在于各自的 traits 参数,令每个函数实现码与其接受之 traits 信息相应和;
②建立一个控制函数或函数模板,它调用上述函数(模板)并传递 traits class 所提供的信息;
12、template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率;
八、定制new和delete
1、当 operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的 new-handler ;
2、如何设计一个良好的 new-handler :
①让更多内存可被使用;
②安装另一个 new-handler ;
③卸除 new-handler (将 null 指针传给 set_new_handler );
④抛出 bad_alloc (或派生自 bad_alloc )的异常;
⑤不返回(调用 abort 或 exit );
3、 static 成员必须在 class 定义式之外被定义(除非他们是 const 而且是整数型);
4、 set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用;
5、Nothrow new是一个颇为局限的工具,因为它只适用于内存分配,后继的构造函数调用还是可能抛出异常;
6、有许多理由需要写个自定义的 new 和 delete ,包括改善效能、对 heap 运用错误进行调试、收集 heap 使用信息;
7、 operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果他无法满足内存需求,就该调用 new-handler ,它也应该有能力处理 0 byte 申请, class 专属版本则还应该处理“比正确大小更大的(错误)申请”;
8、 operator delete 应该在收到 null 指针时不做任何事, class 专属版本则还应该处理“比正确大小更大的(错误)申请”;
九、杂项讨论
1、严肃对待编译器发出的警告信息,但不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同;
2、C++标准程序库的主要技能由STL、iostreams、locales组成,并包含C99程序库;
3、boost是一个高质量、源码开放、平*立、编译器独立的程序库,应该让自己熟悉boost,首页 http://boost.org
总结:简单将书本过了一遍,前面的部分还相对容易接受,到了 模板与泛型编程 和 定制new和delete 这两章时,感觉像在云里雾里,以后还要多多温习,⊙﹏⊙‖∣。这本书给出了许多对于C++编程的建议,以后写代码的时候可以当一个参考书来参考。