摘自《effective c++》、《more effective c++》、网上的经验
参考:http://blog.csdn.net/KangRoger/article/details/44706403
关于C++程序的设计原则,以后还会再补充。
习惯使用C++
条款1:C++有哪些内容?
C、C with Classes、模板、STL。
条款2:尽量以const、enum、inline替换#define
这句话也可以说:以编译器代替预处理。
专属于类作用域的常量。专属于类的常量将这个常量限定在类的作用域内,而#define定义的常量没有作用域的限制,一旦在某一处有个宏定义,在其后面都有效(除非#undef)。
对于单纯常量,最好以const对象或enums替换#defines;
对于形似函数的宏,最好改用inline函数替换#defines。
条款3:尽可能使用const
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
条款4:确定对象使用前已先被初始化
为了避免使用未初始化的对象,要做三件事1、手动初始化非成员对象。2、使用初始化列表初始化成员对象。3、消除初始化次序的不确定性。
不是这些书的条款1:struct与class
“struct那个关键词,其实没什么用"。总的来讲,C++中struct和class是基本上完全相同的,仅在细节上有些不同。
构造/析构/赋值
条款5:了解C++默认编写并调用哪些函数
编译器可以暗自为类创建默认构造函数、拷贝构造函数、拷贝赋值操作符,以及析构函数。
条款6:若不想使用编译器自动生成的函数,就该明确拒绝
为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。继承像noncopyable这样不能复制的基类也是一种做法。这条款对应于单例模式。
(more effective条款4):避免无用的缺省构造函数
没有缺省构造函数造成的问题:通常不可能建立对象数组,对于使用非堆数组,可以在定义时提供必要的参数另一种方法是使用指针数组,但是必须删除数组里的每个指针指向的对象,而且还增加了内存分配量提供无意义的缺省构造函数会影响类的工作效率,成员函数必须测试所有的部分是否都被正确的初始化。
条款7:为多态基类声明虚析构函数
带有多态性质的基类应该声明一个虚析构函数。如果一个类带有任何虚函数,它就应该拥有一个虚析构函数。
如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。
一个类的设计目的不是作为基类使用,或不是为了具备多态性,就不该声明虚析构函数。
条款9:绝不在构造和析构过程中调用virtual函数
在构造和析构函数期间不要调用虚函数,因为这类调用从不下降至派生类。
条款10:令operator=返回一个reference to *this
如果operator=不返回一个引用,返回一个临时对象,照样可以实现连锁赋值。但这个临时对象的构建会调用拷贝构造函数。多出了一步,浪费资源。
operator=是改变左侧操作数的,与其类似的operator+=、operator-=等改变左侧操作符的运算,都应该返回引用。这是一个协议,应该去遵守。
条款11:在operator=中实现“自我赋值”
确保当对象自我赋值时operator =有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一部分
在一个类中,有两个函数可以给复制对象:复制构造函数和赋值操作符,统称为copying函数。
如果我们自己不编写者两个函数,编译器会帮我们实现这两个函数,编译器生成的版本会将对象的所有成员变量做一份拷贝。编译器生成的copying函数的做法通常是浅拷贝。可以参考这里。
如果我们自己实现了copying函数,编译器就不再帮我们实现。但是编译器不会帮我们检查copying函数是否给对象的每一个变量都赋值。
可以发现复制构造函数和赋值操作符有类似的代码。但是者两个函数不能相互调用。复制构造函数是构造一个不存在的对象,而赋值操作符是给一个存在的对象重新赋值。
消除重复代码的方法编写一个private方法,例如void Init()。在这个函数中操作重复代码。
不是这些书的条款2:注意深拷贝和浅拷贝
普通类型对象的拷贝很简单,就是值的复制而已。
类对象的拷贝相比于普通类型的拷贝就复杂多了,它存在着各种成员变量。在对进行对象拷贝时,当对象包含对其他资源的引用,如果需要拷贝这个对象所引用的对象,那就是深拷贝,否则即是浅拷贝。
资源管理
条款13:以对象管理资源
为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的RAII类分别是auto_ptr和tr1::shared_ptr。后者通常是较佳选择,因为其拷贝行为比较直观。若选择auto_ptr,复制动作会使他(被复制物)指向NULL。
注:RAII,也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。它保证在任何情况下,使用对象时先构造对象,最后析构对象。
条款14:在资源管理类中小心coping行为
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
普遍而常见的RAII类拷贝行为是:抑制拷贝,施行引用计数法。不过其它行为也可能被实现。
条款15:在资源管理类中提供对原始资源的访问
APIs往往要求访问原始资源,所以每一个RAII类应该提供一个“取得其所管理之资源”的方法。
对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取相同形式
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。
条款17:以独立语句将newed对象置入智能指针
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常抛出,有可能导致难以察觉的资源泄漏。设计与声明
条款18:让接口容易被正确使用,不容易被误用
好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
tr1::shared_ptr支持定制删除器。这可防范DLL问题,可被用来自动解除互斥量等等。
条款19:设计class犹如设计type
Class的设计就是type的设计。
在定义一个新的type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。
条款20:宁以pass-by-reference-to-const替换pass-by-value
尽量以pass-by-reference-to-const替代pass-by-value。前者通常比较高效,并可避免切割问题。
以上规则并不使用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。
条款21:必须返回对象时,别妄想返回其reference
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明为private
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保护,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
条款23:宁以非成员函数、非友元函数替换成员函数
这样做可以增加封装性、包裹弹性和机能扩充性。
条款24:若所有参数皆需要类型转换,请为此采用non-member函数
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个非成员函数。
条款25:考虑写出一个不抛出异常的swap函数
swap是STL中的标准函数,用于交换两个对象的数值。后来swap成为异常安全编程(exception-safe programming,条款29)的脊柱,也是实现自我赋值(条款11)的一个常见机制。
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap。
调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
实现
条款26:尽可能延后变量定义式的出现时间
尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
(effective条款27、more effective条款2)尽量少做转型动作、尽量使用C++风格的转换
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的执掌。
(more effective条款5)谨慎定义类型转换函数
让编译器进行隐式类型转换所造成的弊端要大于它所带来的好处,所以除非你确实需要,不要定义类型转换函数。
条款28:避免返回handles指向对象内部成分
这里的handles指的是引用、指针、迭代器等可以修改对象的传递方法。
避免返回handles指向对象内部。
遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
注:dangling handles(空悬的号码牌):handles所指对象不存在。
条款30:透彻了解inlining的里里外外
inline函数是特殊的函数,它有宏的优点,却克服了宏的缺点。
将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。
不要只因为function templates出现在头文件,就将它们声明为inline。
Templates通常也放置在头文件,因为它一旦被使用,编译器为了将它具体化,也需要知道它长什么样子。(有些编译环境可以在链接期间才执行template具体化,但是编译期间完成的更常见)。
对于template,inlining除了引起代码膨胀,还有其他成本。
条款31:将文件间的编译依存关系降至最低
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。
基于此构想的两个手段是Handle classed和Interface classes。
程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。
继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
“public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。
条款33:避免遮掩继承而来的名称
derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。
条款34:区分接口继承和实现继承
接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
pure virtual函数只具体制定接口继承。
简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
non-virtual函数具体制定接口继承以及强制性实现继承。
条款35:考虑virtual函数以外的其他选择
使用non-virtual interfance (NVI)手法,NVI手法自身是一个特殊形式的Template Method设计模式。
将virtual函数替换为“函数指针成员变量”,这是strategy设计模式的一种分解表现方式。
将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。
条款36:绝不重新定义继承而来的non-virtual函数
绝对不要重新定义继承而来的non-virtual函数。
条款37:绝不要重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。
条款38:通过复合塑模树has-a 或“根据某物实现出”
复合(composition)的意义和public继承完全不同。
在应用域(application domain),复合意味has-a;在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。
条款39:明智而审慎的使用private继承
private继承意味着is-implemented-in-terms-of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,使用private是合理的。
和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象占用空间最小化”的程序库开发者而言,可能很重要。
条款40:明智而审慎的使用多重继承
多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
virtual继承会在对象大小、速度、初始化、赋值造成成本增加。如果virtual base class不带任何数据,那么将是最具有使用价值情况。
多重继承的确有正当用途。其中一个情节设计public继承某个Interface class和private继承某个协助实现的class。
(more effective条款3)不要对数组使用多态
多态和指针算法不能混合在一起来用,所以数组与多态也不能用在一起。
值得注意的是如果你不从一个具体类派生出另一个具体类,那么你就不太可能犯这种使用多态性数组的错误。
正如条款more effective条款33 所解释的,不从具体类派生出具体类有很多好处。
模板与泛型编程
条款41:了解隐式接口和编译期多态
class和templates都支持接口与多态;
对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
条款42:了解typename的双重意义
当声明template参数时,typename与class可以互换
使用typename标识嵌套从属类型名称,但是在基类列表和成员初始哈列表中,不能使用typename来指明从属名称。
条款43:学习处理模板化基类内的名称
可在derived class templates内通过this->指涉base class templates内的成员名称,或藉由一个明白写出base class资格修饰符完成。
条款44:将与参数无关的代码抽离templates
template 生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述(binary representation)的具现类型共享实现码。
条款45:运用成员函数模板接受所有兼容类型
请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
如果声明member templates用于泛化copy构造函数或泛化assignment操作,还是要声明正常的copy构造函数和copy assignment操作符。
条款46:需要类型转换时请为模板定义非成员函数
当编写一个class template时,它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为class template内部的friend函数。
条款47:请使用traits class表现类型信息
traits是一种技术,是C++程序员共同遵守的协议。这个技术要求之一就是,它对内置类型和自定义类型表现的一样好。traits必须能够施行于内置类型,意味着“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原值指针内。所以类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一个或多个特化版本中。这样的templates在STL中有若干个,迭代器的为iterator_traits:
条款48:认识template元编程
Template metaprogramming(TMP,模板元编程)可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率。
TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
定制new和delete
条款49:了解new-handler的行为
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是有可能抛出异常。
条款50:了解new和delete的合理替换时机
有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
条款51:编写new和delete时需固守常规
operator new应该内涵死循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属版本的还应该处理“比正确大小更大的(错误)申请”。
operator delete应该在收到指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
条款52:写了placement new也要写placement delete
当编写一个placement operator new时,也要编写对应版本的placement operator delete。否则就可能造成隐蔽的内存泄露。
当声明了placement new和placement delete时,就会掩盖正常版本。
杂项讨论
条款53:不要轻忽编译器的警告
严肃对待编译器发出的警告信息。努力在你的编译器最高警告级别下做到无任何警告。
不要过度依赖编译器的报警能力,因为不同编译器对待事情的态度不相同。一段有警告的代码,移植到另一个编译器上,可能没有任何警告。
条款54:让自己熟悉包括TR1在内的标准程序库
C++的标准程序的主要机能由STL、iostream、locales组成。并包含C99标准程序库。
TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式以及另外10个组件的支持。
TR1只是一份规范,为获得TR1提供的好处,需要一个实物,例如Boost。
条款55:让自己熟悉Boost
Boost是个社群,也是一个网址。致力于免费、源码开发、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响里的角色。
Boost提供许多TR1组件实现品,以及其他许多程序库。
(more effective条款1)区分指针与引用
当你知道你必须指向一个对象并且不想改变其指向时, 或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针。
运算符
(more effective条款6)自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别
不论是自增或自减的前缀还是后缀都只有一个参数。为了解决这个语言问题, C++规定后缀形式有一个 int 类型参数,当函数被调用时,编译器传递一个 0 做为 int 参数的值给该函数。
从你开始做 C 程序员那天开始,你就记住自增 的前缀形式有时叫做“增加然后取回”,后缀形式叫做“取回然后增加”。这两句话非常重要,因为它们是自增 前缀与后缀的形式上的规范。而且自增运算符的 前置比后置效率高。
(more effective条款7)不要重载“&&”,“||”, 或“,”
与 C 一样,C++使用布尔表达式短路求值法(short-circuit evaluation)。这表示一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。如果你想重载这种方法,你必须知道你正在极大地改变游戏规则。因为你以函数调用法替代了短路求值法。
同样的理由也适用于逗号操作符,你不能重载下面的操作符
new delete sizeof typeid
static_cast dynamic_cast const_cast reinterpret_cast
(more effective条款8)理解各种不同含义的 new 和 delete
new 和 delete 操作符是内置的,其行为不受你的控制,凡是它们调用的内存分配和释放函数则可以控制。当你想定制 new 和 delete 操作符的行为时,请记住你不能真的做到这一点。你只能改变它们为完成它们的功能所采取的方法,而它们所完成的功能则被语言固定下来,不能改变。
异常
(more effective条款10):在构造函数中防止资源泄漏
你用对应的 auto_ptr 对象替代指针成员变量,就可以防止构造函数在存在异常时发生资源泄漏,你也不用手工在析构函数中释放资源,并且你还能象以前使用非const 指针一样使用 const 指针,给其赋值。在对象构造中,处理各种抛出异常的可能,是一个棘手的问题,但是 auto_ptr(或者类似于 auto_ptr 的类)能化繁为简。它不仅把令人不好理解的代码隐藏起来,而且使得程序在面对异常的情况下也能保持正常运行。
(more effective条款9,more effective条款11)条款8:别让异常逃离析构函数
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作。
我们知道禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止 terminate 被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。
条款29:为“异常安全”而努力是值得的
异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。
这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
C 程序员能够仅通过 setjmp 和 longjmp 来完成与异常处理相似的功能。但是当 longjmp在 C++中使用时,它存在一些缺陷,当它调整堆栈时不能对局部对象调用析构函数。而大多数 C++程序员依赖于这些析构函数的调用,所以 setjmp 和 longjmp 不能够替换异常处理。
(more effective条款12):理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。
第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。对象做为参数传递给函数时不一定需要被拷贝。
第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。最后一点,catch 子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的 catch 将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。
(more effective条款13):理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异