本文基础内容来自书籍《Effective C++》,转自博客http://blog.csdn.net/cscmaker/article/details/7553411
条款01 视C++为一个语言联邦
理解C++,必须认识其主要的次语言。共有四个:
C,Object-Orented C++, Template C++, STL
条款02 尽量以const, enum, inline替换#define
如:#define VALUE 1,该记号名称可能没有进入记号表,所以当你在使用该变量产生一个编译错误的时候,编译器的错误信息中会提到 1 这个数值,但是没有VALUE,这使得你无法准确的定位到错误。而是用const声明常量就会解决这个问题。
class GamePlayer{
static const int num = 5;
int scores[num];
};
在旧式编译器中也许不支持上述语法,他们不允许static成员在其声明式上获得初值。如果你的编译器不支持在声明式上赋值,可以再class的实现文件内进行定义。如:
class Test{
static const int num; //在Test的头文件中声明
};
const int Test::num = 5; //在Test实现文件内
但是在GamePlayer类中,你必须要在声明数组scores时,给他一个确定的数值。这种情况下,可以使用“the enum hack“补偿做法。其理论基础是:“一个属于枚举类型的数值可权充ints来使用”,于是GamePlayer可定义如下:
class GamePlayer{
enum{num = 5};
int sorces[num];
};
对于enum hack方法来说,它的行为比较像#define,但是它能够计入记号表,能够避免编译错误不好定位的问题,并且他不像const 常量,取一个const的值是可以的,但是不可以取一个enum的地址。所以当你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮你实现这个约束。
使用#define实现类似于函数的宏,不会招致函数调用带来的额外的开销。但是使用这种宏经常会出现各种陷阱和麻烦。所以使用inline来代替类似于函数的这种宏。
条款03:尽可能使用const
将某些东西声明为const可帮助编译器侦测出错用法,const可被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体
编译器强制实施bitwiseconstness,但是你编写程序时应该使用“概念上的常量性”
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象使用前已先被初始化
读取未初始化的对象会导致不明确的行为,在某些平台上,仅仅只是读取未初始化的值,就可能使得程序终止。
所以在使用任何类型前都应该首先初始化,无论是内置类型还是自定义类型。对于无任何成员的内置类型,我们需要手工完成初始化,对于自定义类型,初始化的任务落在了构造函数中。另外,还需要了解赋值和初始化的区别。例如:
class Test{
int x;
stringy;
Test(int x, string y){}
};
Test:Test(){
this->x= x; //这是赋值操作,而不是初始化操作
this->y = y;
}
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。如Test的成员函数的初始化发生在default构造函数调用之前。Test构造函数的最佳写法是,使用所谓的member initialization list替换赋值动作:
Test::Test(int x,string y):x(x),y(y) //这实现了成员变量的初始化。
{
//do other thing
}
这样使得成员变量在初始化的时候就设置了相对应的值,而不需要在经过一次赋值操作。提高了效率。
C++有着十分固定的“成员初始化次序”。基类总是更早于子类的初始化,而class成员变量的初始化总是以其声明的次序初始化。
C++对于“定义在不同的编译单元内的non-localstatic对象的初始化次序并无明确的定义”。解决这种问题的办法是将每个non-local static对象搬到自己的专属函数内。这些函数返回一个reference,用户调用这些static函数,而不直接涉及到这些对象。这种手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间,首次遇到该对象”时被初始化。单例模式使用的就是这种手法。
《Effective C++ 》第 2节 构造/析构/赋值运算符
条款05: 了解C++默默编写并调用哪些函数
C++编译器会为一个空类声明一个copy构造函数,一个copy assignment操作符和一个析构函数。此外,如果没有声明构造函数,编译器会为你声明一个default构造函数。所有这些函数都是public inline。
default构造函数和析构函数用来存放“隐藏幕后”的代码。copy构造函数和copy assignment操作符只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象。但是要注意三点:
a:如果打算在一个内含reference成员的class支持赋值操作,你必须自己定义copy assignment操作符。因为C++不允许让reference改指向不同的对象
b:若类中使用const成员,因为更改const成员是不合法的。所以也需要自已实现copy assignment操作和copy构造函数
c:如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。
条款06:若不想使用编译器自动生成的函数,就应该明确拒绝
为了驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现,使用像Uncopyable这样的base class也是一种做法。
Uncopyable作为base class方法
class Uncopyable{
protected:
Uncopyable(){};
~Uncopyable(){};
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator= (const Uncopyable&);
};
其他的类private继承Uncopyable
class HomeForSale: private Uncopyable{
............/
};
这样就不能对HomeForSale使用copy构造函数和copy assignment操作符了
条款07:为多态基类声明virtual析构函数
C++明确指出:当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义,实际上执行时通常发生的是对象的derived成分没有被销毁。只执行了其base class 的析构函数,造成了局部销毁资源的状况。从而造成了内存泄露。
解决这个问题的做法很简单:给base class一个virtual析构函数。此后删除derived class对象就会如你想要的那般。它会消除整个对象,包括base class成分。如:
class BaseClass{
BaseClass(){};
virtual ~BaseClass(){ cout<<"~BaseClass()"<<endl;}
};
class DrivedClass: public BaseClass{
DrivedClass(){}
~DrivedClass(){ cout<<"~DrivedClass()"<<endl;}
};
BaseClass* pBaseClass = new DrivedClass();
delete pBaseClass; //现在不会出现局部删除的现象。
如果class不含virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当做base class,令其析构函数为virtual函数往往是个馊主意。这样做会使得对象会携带某些不必要的信息,用来运行期决定哪一个virtual函数该被调用。
classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性质,就不该声明virtual析构函数。
条款08:别让异常逃离析构函数
在析构函数中出现异常,会导致程序过早的结束,造成某些资源的泄漏,例如:应该delete某些对象,但是没有执行到。
a:析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序(abort)
b:如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。
条款09:绝不在构造和析构过程中调用virtual函数
base class在构造函数或者析构函数中如果使用vritual函数,那么该base class调用的也是自身的virtual函数,而不会降级为derived class 相应的virtual函数。
记住这类调用从不下降至derived class。
条款10:令operator= 返回一个reference to *this
注意,这只是一个协议,并不一定必须遵循它。然而这分协议被所有内置类型和标准程序库提供的类型如:string ,vector, complex等共同遵守。
条款11:在operator=中处理“自我赋值”
确保当对象自我赋值时operator=有良好行为,其中技术包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy-and-swap
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
条款12:复制对象时勿忘其每一个成分
copy函数应该确保复制“对象内的所有成员变量”及“所有base class成分”
不要尝试以某个copying函数实现另一个copy函数,应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
《Effective C++ 》第 3节 资源管理
条款13:以对象管理资源
为防止资源泄漏,请使用RAII(Resource Acquisition Is Initialliztion资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的RAII classes分别时tr1::shared_ptr(引用计数型智慧指针RCSP)和auto_ptr(标准库中的智能指针)。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使他指向NULL。
条款14:在资源管理类中小心copying行为
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。(例如:常见的一个错误就是,对象A中存在对另外一个对象B的引用,此时使用默认的copy函数复制该对象A为C,那么在C析构的时候会将B删除掉,A在析构的时候就会出错)
普遍而常见的RAII class copying行为是:抑制copying, 施行引用计数法(shared_ptr类似方法)。不过其他行为也可能被实现。
条款15:在资源管理类中提供对原始资源的访问
APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。(例如:shared_ptr,auto_ptr重载了指针取值操作符( -> 和 *),或者提供一个get()函数,返回一个原始指针)
对原始资源的访问可能经由显示转换或者隐式转换。一般而言显式转换比较安全,但是隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取相同的形式
int* p = new int[5]; delete p; //这样是错误的,只删除了第一个元素的内存空间。
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[],如果你在new表达式中不使用[],也一定不要在delete中使用[]
条款17:以独立语句将newed对象置入智能指针。
例如:函数 void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);在具体调用的时候使用如下方式:
processWidget(std::tr1::shared_ptr<Widget> (new Widget), priority);该函数中在调用该函数之前需要做三件事情:
A :调用 priority B:执行 new Widget C: 调用tr1::shared_ptr构造函数
此时,如果这样对函数进行调用,会有资源泄漏的可能性。因为C++编译器对于这三个事情的执行顺序没有严格的规定。如果 new在第一个位置执行,priority在第二个执行,此时在执行第二个出错的时候,new出来的资源就无法放入智能指针中,也就造成了资源泄漏。
以独立的语句将newed对象存储于智能指针中,如果不这样做一旦抛出异常,就有可能导致资源泄漏。
《Effective C++ 》第 4节 设计与声明
条款18:让接口容易被正确使用,不易被误用
好的接口很容易被正确使用,不容易被误用。
“促使正确使用”的办法包括接口的一致性,以及内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
tr1::shared_ptr支持定制型删除器,这可防范DLL问题(跨DLL之new/delete成对运用),可被用来自动解除互斥锁。但是这种智能指针在防范用户出现错误的同时,增加了系统的开销,boost库中shared_ptr的大小是原始指针的两倍大。所以需要综合考虑其执行成本。
条款19:设计class犹如设计type
如何设计高效的classes,首先需要了解你面对的问题,几乎每一个class都要求你面对以下提问:
新type的对象应该如何被创建和销毁?涉及到构造函数和析构函数以及内存分配函数,释放函数的设计
对象的初始化和对象的赋值该有什么样的差别?这个答案决定你的构造函数和赋值操作符的行为,以及期间的差异。
新type的对象如果被passed by value,意味着什么?copy构造函数用来定义一个type的pass by value该如何实现
什么是新type的“合法值”?对class的成员变量而言,通常只有某些数值集是有效的。
条款20:宁以pass-by-reference-to-const替换pass-by-value
例如:bool validateStudent(Student s);
Student plato;
当调用此函数的时候,把plato当作函数的参数,此时首先Student的构造函数会被调用,以plato为蓝本将s初始化,同样,在函数返回时,会将s销毁。因此对于该函数而言,参数传递的成本是:一次copy构造函数的调用,一次析构函数的调用。当然,当Student类中还存在其他对象的话,那这个代价就不只一次copy,一次析构了。
所以:尽量以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:宁以non-member,non-friend替换member函数
如果某些东西被封装,它就不再可见,愈多东西被封装,愈少人可以看到它。而愈少人看到它,我们就有愈大的弹性去改变它,因为我们的改变仅仅直接影响看到改变的那些人事物。因此,愈多东西被封装,我们改变那些东西的能力也就愈大。这就是我们首先推崇封装的原因:他使我们能够改变事务而只影响有限客户。
现在考虑对象内的数据。愈少代码可以看到数据,愈多的数据可被封装,而我们也就愈能自由的 改变对象数据。
条款这样做可以增加封装性,包裹弹性和机能扩充性。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”-----即this对象------的那个隐喻对象,绝不是隐式转换的合格参数者。
如:const Rational operator* (const Rational& rhs) const; ///成员函数,
result = 2* oneHalf; //result , oneHalf都是Rational对象,此时变成 2.operator*(oneHalf)。错误!2不能隐式转换成Rational对象!
const Rational operator*(const Rational& rhs1, const Rational& rhs2); //non-member函数
此时使用 result = 2* oneHalf // 2可以被隐式转换成Rational对象,前提是构造函数是non-explicit
条款25:考虑写出一个不抛出异常的swap函数
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-memebr swap用来调用前者。对于classes,也请特化std::swap
调用swap时应针对std::swap使用using 声明式,然后调用swap并且不带任何“命名控件资格修饰”
为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
《Effective C++ 》第 5 节 实现
条款26:尽可能延后变量定义式的出现时间
条款27:尽量少做转型动作
C++提供了四种新式的转型
A:const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++ style转型操作符。
B:dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某些对象是否归属于集成体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
C:reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它并不可移植。
D:static_cast用来强迫隐式转换,例如将non-const对象转换成cosnt对象,或者将int转换成double等等。
请记住:
如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts,如果有个设计需要转型,试着发展无需转型的替代设计。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放入自己的代码中。
宁以C++ style转型,不要使用旧式转型,前者很容易辨识出来,而且也比较有着分别门类的职掌。
条款28:避免返回handles指向对象内部成分
不论这所谓的handle是个指针或者迭代器或者reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一关键的是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿”的风险下。
条款29:为“异常安全”而努力是值得的
异常安全函数提供以下三个保证之一:
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会恢复到“调用函数之前”的状态。
不抛掷保证:承诺绝不抛出异常,因为他们总能够完成他们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。
"强烈保证"往往能够以copy-and-swap实现出来,但是“强烈保证”并非对所有函数都可以实现或者具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
条款30:透彻的了街inlining的里里外外
将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调式过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
不要因为funciton templates出现在头文件,就将他们声明为inline
条款31:将文件间的编译依存关系降至最低
Interface classes类似java和.net的interfaces,但是C++的interface classes并不需要负担java和.net的interface所要负担的责任。举个例子:java和.net都不允许在interface内实现成员变量和成员函数,但是C++不禁止这两样东西。C++这种更为巨大的弹性有其用途。
支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和interface classes
程序库文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。
条款32:确定你的public继承塑模出is-a关系
public继承 意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class 对象。
条款33:避免遮掩继承而来的名称
derived classes内的名称会遮掩base classes内的名称,在public继承下从来没有人希望如此。
为了让被遮掩的名称在再见天日,可使用using声明式或者转交函数。
条款34:区分接口继承与实现继承
接口继承与实现继承不同,在public继承之下,derived classes总是继承base classes的接口。
pure virtual函数只具体制定接口继承
简朴的impure virtual函数具体制定接口继承及缺省实现继承
non-virtual函数具体指定接口继承以及强制性实现继承
条款35:考虑virtual函数以外的其他选择
藉由non-virtual interface手法实现 template method模式
这种流派主张virtual函数应该几乎总是private。较好的设计是使用一个non-virtual的public成员函数作为对外的接口,并调用一个private virtual函数进行实际工作。
藉由Function Pointers实现strategy模式
这种方法主张类中接受一个函数指针,指向要执行的动作。从而实现不同的策略。
藉由tr1::function完成strategy模式
这种方法不再使用函数指针,而是改用一个类型为tr1::function的对象。
定义式:typedef std::tr1::function<int (const GameCharacter&)>HealthCalcFunc;
这个签名代表的函数“接受一个reference指向const GameCharacter,并且返回int”。这个tr1::function类型产生的对象可以持有任何与签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换成const GameCharacter&,而其返回类型可被隐式转换为int。(这种方法没有明白,还要继续研究。)
条款36:绝不重新定义继承而来的non-virtual函数
条款37:绝不重新定义继承而来的缺省参数值
virtual函数是动态绑定,而non-virtual则是静态绑定,缺省参数值是静态绑定。
静态绑定就是他在程序中被声明时所采用的类型,动态类型是指“目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。
例如:class Shape{
public:
virtual void draw(ShapeColor color =Red)const = 0;
}
class Rectangle:public Shape{
public:
virtual void draw(ShapeColor color =Green) const ;
}
Shape* pr = new Rectangle();
pr->draw(); //此时默认的参数不是Green而是Red,因为,缺省参数值静态绑定!
条款38:通过复合塑模出has-a或者“根据某物实现出”
复合的意义和public继承完全不同
在应用领域中,复合意味着has-a。在实现领域中,复合意味着is-implemented-in-terms-of(根据某物实现出),例如通过list实现set。
条款39:明智而审慎地使用private继承
Private继承意味is-implemented-in-terms of。它通常比复合的级别低。但是当derived class需要访问protected base class的成员,或者需要重新定义继承而来的virtual函数时,这样设计是合理的。
和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
条款40:明智而审慎地使用多重继承
多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要。
virtual继承会增加大小,速度,初始化复杂度等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况
多重继承的确有正当用途,其中一个情节涉及“public 继承某个Interface class”和“private继承某个协助实现的class”的两相组合。