《Effective C++》 阅读小结 (笔记)

时间:2021-12-15 06:25:47

  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++编程的建议,以后写代码的时候可以当一个参考书来参考。