《Effective C++》笔记
-
C++由C,object C++,Template C++,STL组成,编程守则视情况而变化,取决于用它的哪一部分。
-
对于单纯变量,最好用const对象或enums替换#defines,即宁可以编译器替换预处理器;对于形似函数的宏,最好用inline函数替换#defines。若实在需要,用#ifdef/#infndef控制编译。
-
尽可能地使用const,可帮助编译器侦测出错误用法【 ?】。编译器强制使用bitwise constness,即const成员函数不可以更改对象内任何non-static成员变量,但编写程序应该使用conceptual constness【 ?】。当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复。
-
确定对象被使用前已被初始化。为内置型对象手工初始化;构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同;为避免跨编译单元之初始化次序问题,用local static对象替换non-local static对象,即将每个non-local static对象搬到自己的专属函数中,并返回一个reference指向它所含的对象。
构造/析构/赋值
-
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
-
若不想使用编译器自动生成的函数,则明确拒绝。包括,可将相应的成员函数声明为private并且不予实现;使用一个专门为了拒绝自动生成函数而设计的base class,并private继承。
-
带多态性质的base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该有一个virtual析构函数。如果class设计目的不是作为base class使用(如标准string和STL容器),或不是为了具备多态性,就不该声明virtual析构函数。
-
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
-
绝不在构造和析构期间调用昔日virtual函数,因为这类调用从不下降至derived class(比起当前执行构造和析构函数的那层)。
-
令赋值操作符返回一个reference to *this。
class Widget{
public:
...
Widget& operator+=(const Widget& rhs)
{
...
return *this;
}
};
- 在operator=中处理“自我赋值”。传统做法是借由最前面的证同测试(比较来源对象和目标对象的地址):
if (this == &rhs) return *this;
考虑到异常安全性,只需注意复制之前别删除(精心周到的语句顺序)。
copy and swap,先制作一份复件再交换。
- 复制对象时勿忘其每一个成分。copying函数应该确保复制对象内的所有成员变量和所有base class成分。不要尝试以某个copying函数实现另一个copying函数(copy构造和copy assignment),应该将共同机能放进第三个函数中,并由两个copying函数共同调用,以消除重复代码。
资源管理
-
以对象管理资源。为防止资源泄露,使用RAII对象(智能指针?)(资源取得时机便是初始化时机;管理对象运用析构函数确保资源被释放),它们在构造函数获得资源并在析构函数释放资源。两个最常用的RAII classes分别是tr1::shared_ptr和auto_ptr。(C++ prime plus提及)
-
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法等。
-
在资源管理类中提供对原始资源的访问(APIs往往要求访问原始资源)。对原始资源的访问可能经由显示转换或隐式转换,一般而言显示转换比较安全,但隐式转换对客户比较方便。
-
成对使用new和delete时要采取相同形式([])。
-
以独立语句将新new的对象存储入智能指针。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
设计与声明
-
让接口容易被正常使用,不易被误用。促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容;阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值以及消除客户的资源管理责任。tr1::shared_ptr支持定制型删除器。
-
设计class犹如设计type。设计规范:新type对象如何被创建和销毁?对象的初始化和赋值该有什么差别?passed by value?什么是type的合法值?新type需要配合某个继承图系(virtual)?新type需要什么样的转换?什么样的操作符和函数对新type而言是合理的?新type各成员的类型?新type的一般化程度(不如直接class template)?真的需要这个新type(non-member函数)?目标:C++内置类型。
-
尽量以pass-by-reference-to-const替换pass-by-value。对内置类型、STL的迭代器和函数对象,还是pass-by-value比较合适。
-
必须返回对象时,别妄想返回其refenrence。绝不要用pointer或reference指向一个local stack对象,或返回一个reference指向heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能需要多个这样的对象。
-
将成员变量声明为private。成员变量的封装性与成员变量改变时所破坏的代码数量成反比。protected并不比public更具封装性。
-
宁以non-member、non-friend替换member函数。因为它们不增加能够访问class之内的private成分的函数数量,使class有更好的封装性。使用namespace,可以跨越多个源文件。
-
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
-
swap...
实现
-
尽可能延后变量定义式出现时间,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。
-
尽量避免转型。试着发展无需转型的替代设计;试着将它隐藏于某个函数背后;宁可使用C++-style转型,不要使用旧式转型。【?】
-
避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数行为像个const、并将发生dangling handles的可能性降至最低。
-
为异常安全而努力。异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数区分为三种可能的保证:
- 基本型:如果异常被抛出,程序内的任何事物仍然保持在有效状态下,但程序状态不可预料。
- 强烈型:如果异常被抛出,程序状态不改变。如果函数成功,就是完全成功,如果函数失败,程序回到调用函数之前的状态。一般用copy and swap实现:为你打算修改的对象做出一份副本,然后在副本身上做一切必要修改,所有改变成功,则swap。
- 不抛掷保证。
函数提供的异常安全保证通常最高只等于其所调用各个函数的异常安全保证中的最弱者。
-
透彻了解inlining。inline函数背后的整体观念是,对此函数的每一个调用都用函数本体替换之。故,将大多数inlining限制在小型、被频繁调用的函数身上,可使调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序速度提升。(平均而言,一个程序往往将80%的执行时间花在20%的代码上头)。如果没有理由要求template具现的每一个函数都为inlined,就避免将这个template声明为inline。inline函数通常一定被置于头文件内。
定义于class内的函数都暗自成为inline。
-
将文件间的编译依存关系降至最低。程序头文件应该以完全且仅有声明式的形式存在。基于“编译依存最小化”的一般构想是:相依于声明式,而不是定义式。将定义与实现分离【?】。两个方法:
- handles classes:为声明式和定义式提供不同的头文件。如果能够,尽量以class声明式替换定义式,并不需要定义时。如果使用object references或object pointers可以完成任务,就不要使用object。
- interface class:抽象基类,描述derived class的接口,通常不带成员变量,也没有构造函数,只有一个virtual析构函数和一组pure virtual函数,用来描述整个接口。调用factory函数为derived class创建新对象,,声明为static,返回指针,指向动态分配所得对象,而该对象支持interface class接口。【?】
继承与面向对象设计
-
public继承意味着is-a。适用于base class身上的每一件事情一定也适用于derived class身上,好的接口可以防止无效的代码通过编译。
-
避免遮掩继承而来的名称。derived classes内的名称会遮掩base classes内的名称,为了让被遮掩的名称重见天日(继承并重新定义或覆写),可使用using声明或转交函数(forwarding function)。
-
接口继承和实现继承不同。在public继承下,derived class总是继承base class的接口。pure virtual函数只具体指定接口继承;impure virtual函数具体指定接口继承及缺省实现继承;non-virtual函数具体指定接口继承及强制性实现继承,不变性凌驾特异性。
-
virtual函数的替代方案。
-
NVI手法,一个特殊形式的Template Method设计模式,以public non-virtual成员函数包裹较低访问性(private或pretected)的virtual函数。
-
函数指针成员变量。将机能从成员函数移到class外部函数,带来的一个缺点是非成员函数无法访问class的non-public成员。
-
tr1::function对象的行为就像一般函数指针,可接纳“与给定之目标签名式兼容”的所以可调用物。
-
古典strategy设计模式。将继承体系内的virtual函数替换为另一个继承体系的virtual函数。
-
绝对不要重新定义继承而来的non-virtual函数。
-
绝对不要重新定义一个继承而来的缺省参数值。non-virtual、缺省参数值都是静态绑定,而virtual函数确是动态绑定的。
对象所谓静态类型,就是它在程序中被声明时所采用的类型,而动态类型则是指目前所指对象的类型。
-
通过复合塑模出has-a或根据某物实现出。在应用域,复合意味着has-a;在实现域,复合意味着is-implemented-in-terms-of。
-
private继承意味着is-implemented-in-terms-of,通常比复合级别低,只要可以,应该选择复合。但是当derived class需要访问protected base class的成员或需要重新定义继承而来的virtual函数时,这么设计是合理的。另外,和复合不同,private继承可以造成empty base最优化,这对致力于“对象尺寸最小化”的程序开发者而言,可能很重要。
-
多重继承可能导致新的歧义性(先找出最佳匹配函数再检验其可用性),以及对virtual继承的需要。virtual继承会增加大小、速度、初始化赋值复杂度的等成本,如果virtual base class不带任何数据,将是最具实用价值的情况。“public继承某个interface class”和“private继承某个协助实现的class”的结合,可用多重继承。
模板与泛型编程
- classes和templates都支持接口和多态。对classe而言,接口是显式的,以函数签名为中心,多态则是通过virtual函数发生在运行期;对template而言,接口是隐式的,奠基于有效表达式,多态则是通过template具现化和函数重载解析发生在编译期。
- 了解typename的双重意义。声明template参数时,前缀关键词class和typename可互换。请使用关键词typename标识嵌套从属类型名称,但不得在base class list(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
- 可在derived class template内通过“this->”指涉base class template内的成员名称,或藉由一个明白写出的“base class 资格修饰符”(using)完成。
- 将与参数无关的代码抽离template。任何template代码都不该与某个造成膨胀的template参数产生相依关系。因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。因类型模板参数而造成的代码膨胀,往往可降低,做法是让完全相同二进制表述的具现类型共享实现码。
- 使用成员函数模板生成“可接受所有兼容类型”(类型转换的筛选)的函数。如果声明member template用于“泛化copy构造”或“泛化assignment操作”,你还需要声明正常的copy构造函数和copy assignment操作符。
- 需要类型转换时请为模板定义非成员函数。当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转gk换”时,请将那些函数定义为“class template内部的friend函数”(令friend函数调用辅助函数)。
- Traits classes使得“类型相关信息”在编译期可用,它们以template和templates特化完成实现。整合重载技术后,traits classes有可能在编译期对类型执行if-else操作。
- TMP(模板元编程)可将工作由运行期移往编译期,得以实现早期错误侦测和更高的执行效率。TMP可用来生成“base on combination of policy choice”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
定制new和delete
-
了解new-handler的行为。set_new_handler允许客户指定一个函数,在内存分配无法满足时被调用。Nothrow new只适用于内存分配,后继的构造函数调用还是可能抛出异常。
-
了解new和delete的合理替换时机。定制版之operator new和operator delete胜过缺省版本(中庸之道)。有许多理由需要写个自定的new和delete,包括改善性能(速度,非最佳齐位,集中)、对heap运用错误进行调试、收集heap使用信息。
-
operator new应该内含一个无穷循环,并在其中尝试分配内存。如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请,operator delete应该在收到null指针时不做任何事。class专属版本还应该处理“比正确大小更大的(错误)申请”(令标准的operator new来处理)。
-
写了placement operator new(operator new接收的参数除了一定会有的那个size_t之外还有其他),请确定也写出了对应的placement operator delete,如果没有这么做,可能发生内存泄漏。当你声明placement new和placement delete,请确定不要无意识地遮掩了它们的正常版本(class中)。
缺省情况下C++在global作用域提供以下形式的operator new:
void* operator new(std::size_t) throw(std::bad_alloc);//normal new void* operator new(std::size_t, void*) throw();//placement new void* operator new(std::size_t, const std::nothrow_t&) throw();//nothrow new
杂项讨论
- 严肃对待编译器发出的警告信息(warning),努力在你的编译器的最严苛警告级下争取无任何警告的荣誉。但也不要过度倚赖编译器的报警能力,毕竟不同编译器态度不同。
- 让自己熟悉包括TR1在内的标准程序库。C++标准程序库的主要机能由STL、iostreams、locales组成,并包含C99标准程序库。TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式、以及另外10个组件的支持。TR1自身只是一份规范,为获得TR1提供的好处,你需要从Boost取得实现。
- Boost是一个社群,也是一个网站。提供了许多TR1组件实现品,以及其他许多程序库。