#@date: 2014-06-16 #@author: gerui #@email: forgerui@gmail.com
前几天买了好几本书,其中有一本是《Effective C++》,准备好好学习一下C++.书中提出了55条应该遵循的条款,下面将逐一学习。点击查看Evernote原文。c++
将C++分为4个次语言。即:C, Objective-Oriented C++, Template C++, STL.数据库
const,enum,inline
替换 #define
宁肯以编译器替换预处理器:
1) 预处理器`#define N 1.653' 将全部出现N的地方替换为1.653,当出现错误报的是1.653致使目标有问题,而不是N。若是使用变量,则可轻易地判断。此外,替换会形成代码在多处出现,增长代码量。因此尽可能定义为常量,const double N = 1.653;
2) 若是在数组初始化的时候,编译器须要知道数组的大小,这样,不可使用变量进行数组初始化,这时#define能够,但咱们最好使用enum{N=3;}来替代define.
3) 使用#define定义一个三目运算符也会产生问题,若是你想得到高效,建议使用inline内联函数。
但#include,以及#ifdef/#ifndef都是必需的,但咱们要尽可能限制预处理器的使用。express
const
1) const
表示不能够改变,若是修饰变量,则表示这个变量不可变,如(a);若是修饰指针,表示指针指向的位置不可改变,如(b)。数组
const char * p = "hello"; //(a) *p的hello不可变, 与char const * p = "hello"等价 char * const p = "hello"; //(b) 表示p的值不可变,即p不能指向其它位置
2) STL迭代器的const安全
std::vector<int> vec; const std::vector<int>::iterator iter = vec.begin(); //相似T* const *iter = 10; //没问题,改变iter所指物 ++iter; //错误!iter是const std::vector<int>const_iterator cIter = vec.begin(); //相似const T* *iter = 10; //错误,*iter是const iter++; //没问题,能够改变iter
3) 使函数返回一个常量值,能够避免意外错误。以下代码,错把==
写成=
,通常程序对*
号以后进行赋值会报错,但在自定义操做符面前不会(由于自定义*
号后返回的是Rational对象实例的引用,能够拿来赋值,不会报错)。若是*
不写成const
,则下面的程序彻底能够经过,但写成const以后,再对const进行赋值就出现问题了。数据结构
class Rational { ... }; const Rational operator* (const Rational& lhs, const Rational& rhs); Rational a, b, c; if(a * b = c); //把==错写成=,比较变成了赋值
4) 函数的参数,若是无需改变其值,尽可能使用const
,这样能够避免函数中错误地将==
等于符号误写为=
赋值符号,而没法察觉。多线程
5) const
做用于成员函数,两个做用,a)能够知道哪些函数能够改变成员变量,哪些函数不能够;b)改善C++效率,经过reference_to_const(即const对象的引用)方式传递对象。下面是常量函数与很是量函数的形式:app
class TextBlock{ public: ... const char& operator[] (std:size_t position) const{ return text[position]; } char& operator[] (std:size_t position) { return text[position]; } private: std::string text; }; /** *使用operator[] */ TextBlock tb("hello"); //non-const 对象 cout<<tb[0]<<endl; //调用的是non-const TextBlock::operator[] tb[0] = 'x'; //没问题,写一个non-const对象 const TextBlock cTb("hello"); //const 对象 cout<<cTb[0]<<endl; //调用的是const TextBlock:operator[] cTb[0] = 'x'; //错误,写一个const对象
6) bitwise const主张const成员函数不能够改变对象内任何non-static成员变量;logical const主张成员函数能够修改它所处理的对象内的某些bits,但要在客户端侦测不出的状况下才得如此。编译器默认执行bitwise。若是想要在const函数中修改non-static变量,需将变量声明为mutable
(可变的)。函数
class TextBlock{ private: char* pText; mutable std::size_t textLength; mutable bool lengthIsValid; public: ... std::size_t length() const; }; std::size_t TextBlock::length() const{ if (!lengthIsValid){ textLength = std::strlen(pText); //加上mutable修饰后,即可以修改其值 lengthIsValid = true; } }
7) 避免const和non-const成员函数重复学习
思想很简单,若是const和non-const成员函数功能至关时,就用non-const函数去调用const函数(不能反过来...o_O
)。
class TextBlock{ public: const char& operator[](std:size_t position) constP ... return text[position]; } char& operator[] (std:size_t position){ return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]); } };
1) 对内置类型(基本类型)手动进行初始化。
int x = 0; const char* p = "Hello World"; double d; std:cin >> d;
2) 内置类型之外的类型,初始化要靠构造函数。类的构造函数使用成员初值列(member initialization list),而不是在构造函数中进行赋值操做。初值列成员变量的排列顺序与其声明顺序相同。
class PhoneNumber { ... }; class ABEntry { public: ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones); private: std::string theName; std::string theAddress; std::list<PhoneNumber> thePhones; int numberTimesConsulted; }; ABEntry::ABEntry(const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones){ theName = name; //这些都是赋值,而非初始化 theAddress = address; thePhones = phones; numberTimesConsulted = 0; } /** *使用成员初值列,效率更高 */ ABEntry::ABEntry(const std:string& name, const std::string& address, const std:list<PhoneNumber>& phones) :theName(name), theAddress(address), thePones(phones), numberTimesConsulted(0) //成员初值列 { ... }
3) 为避免"跨编译单元之初始化次序"问题,以local static对象替换non-local static对象。
//FileSystem源文件 class FileSystem{ public: ... std::size_t numDisks() const; };
extern FileSystem tfs; //Directory源文件,与FileSystem处于不一样的编译单元 class Directory{ public: Directory(params); ... }; Directory::Directory(params){ ... //调用未初始化的tfs会出现错误 std::size_t disks = tfs.numDisks(); }
这样的话,Directory类会调用一个non-local的tfs,而这个tfs未必经历了初始化处理。咱们要有效避免这个状况,使获取的tfs对象保证是初始的,可使用以下的一个函数获取,这就像Singleton(单例)模式同样。
class FileSystem { ... }; FileSystem& tfs(){ static FileSystem fs; return fs; } class Directory { ... }; Directory::Directory(params){ std::size_t disks = tfs().numberDisks(); } Directory& tempDir(){ static Directory td; return td; }
通过上面的处理,将non-local转换了local对象,这样作的原理是:函数内的local static 对象会在"该函数被调用期间","首次赶上该对象之定义式"时被初始化,这样就保证了对象被初始化。这样作的好处是不调用函数时,不会产生对象的构造和析构。但对多线程这样的方法会有问题。
1) 编译器会自动为class建立default构造函数、copy构造函数、copy assignment操做符、以及析构函数。
2) 若是用户声明了一个构造函数,则编译器则不会再为它声明default构造函数。
3) 拷贝构造函数能够经过=
或()
实现。默认的拷贝构造函数对指针进行地址的复制,这样会产生多个对象共用一块地址,会产生问题,能够本身实现拷贝构造函数,实现值的复制。
1) 不容许用户进行对象的拷贝。通常编译器会提供默认拷贝,可将相应的成员函数声明为private而且不予以实现。但这是有个问题,member(成员)函数和friend(友元)函数仍然能够调用。
2) 在不想实现的函数中不写函数参数的名称。
class HomeForSale{ public: ... private: HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&); };
3) 将错误移至编译期,更早地发现错误每每更好。定义一个Uncopyable的基类,其它类继承该类,当执行拷贝时,要调用基类拷贝构造函数,就会出现问题。
class Uncopyable{ protected: Uncopyable(); ~Uncopyable(); private: Uncopyable(const Uncopyable&); Uncopyable& operator=(const Uncopyable&); };
1) polymorphic(带多态性质的)base classes 应该声明一个virtual析构函数。这样,每一个派生类都要实现析构函数,防止指向derived classes的对象没有析构函数。
2) 若是class带有任何virtual函数,它就应该拥有一个virtual析构函数。
3) Classes的设计目的若是不是做为base classes使用,或不是为了具有多态性,就不应声明virtual析构函数。
4) 含有纯虚函数的类是抽象类。
class AWOV{ public: virtual ~AWOV() = 0; //纯虚函数 };
5) 不是全部类都是被设计做为基类来使用的。如string类和STL容器类,因此这些类不须要声明为virtual。
1) 析构函数绝对不要吐出异常。若是析构函数调用的函数可能抛出异常,析构函数应该捕捉异常,而后吞下它们或结束程序。
class DBConnection{ public: static DBConnection create(); void close(); }; class DBManager{ public: ~DBManager(){ db.close(); //析构函数关闭数据库链接 } private: DBConnection db; }; //调用析构函数时可能会发生异常 DBManager dbM(DBConnection::create());
上面的代码为了帮助忘记关闭数据库链接的客户关闭链接,在析构函数中调用了close函数,但这个函数可能出现异常,这种必须调用可能产生异常的函数时,须要进行异常捕获。以下:
DBManager::~DBManager(){ try { db.close(); } catch(...){ //能够记录错误后退出程序 std::abort(); } }
2) 上面这个问题还不是完善的方案,即便析构函数捕获到异常,客户也没法处理异常,客户须要对某个函数运行期间抛出的异常进行反应,那么class应该提供一个普通函数来执行该操做。
class DBManager(){ public: void close(){ db.close(); closed = true; } ~DBManager(){ if (!closed){ try { db.close(); } catch(...) { //错误日志... } } } private: bool closed; DBConnection db; };
这里面加了一个close函数,客户能够本身调用close函数,当发生异常时,进行异常处理。若是客户没有调用close函数,则能够在析构函数中自动调用。因此,在写程序时,必定要将会发生异常的函数做为一个普通函数,这样能够提供更多的选择。
1) 在构造和析构函数期间不要调用virtual函数,由于这类调用从不降低到derived class(子类)。父类的构造函数先于子类执行,因此父类的自身成分早于子类构造,子类的virtual函数尚未生成,因此即便调用virtual函数,也只会调用父类的virtual函数,即这个被声明为virtual的函数在构造函数中毫无心义。
reference to *this
1) 令赋值(assignment)操做符返回一个reference to *this
。这样就能够像基本式同样连续赋值,如基本式的连续赋值:int a,b,c; a=b=c=1
。
class Widget{ public: Widget& operator+=(const Widget& src){ ... return *this; } Widget& operator=(const Widget& src){ ... return *this; } }
1) 确保当对象自我赋值时,operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
2) 肯定任何函数若是操做一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
1) Copying函数应该确保复制“对象内的全部成员变量”及“全部base成分”。
2) 不要尝试以某个copying函数实现另外一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
1) 防止资源泄漏,请使用RAII(Resource Acquisition is Initialization;资源取得时机即是初始化时机)对象,它们在构造函数中得到资源并在析构函数中释放资源。
2) 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者一般是较佳选择,由于其copy行为比较直观。若选择auto_ptr,复制动做会使它(被复制物)指向null,即只有一个对象指向这个资源。
1) 复制RAII对象必须一并复制它所管理的资源,因此资源的copying行为决定RAII对象的copying行为。 2) 广泛而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其余行为也均可能被实现。
1) APIs每每要求访问原始资源,因此每个RAII class应该提供一个“取得其所管理的资源”的方法。 2) 对原始资源的访问可能经由显式转换或隐式转换。显式转换更安全,隐式转换更方便。
1) 若是new数组时使用[],那么释放资源时就要用delete[],这会调用多个析构函数去释放资源;若是使用new对象不使用[],释放时必定不要使用[]。保持二者一致。
std::string str = new std::string; std::string strArr = new std::string[20]; //释放资源 delete str; delete[] strArr;
1) 以独立语句将newed对象存储于(置入)智能指针内。若是不这样作,一旦异常被抛出,很难察觉到资源泄漏。
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
上面的代码存在须要三个步骤:
priority()
new Widget
tr1::shared_ptr
构造函数但C++的编译器对这三个执行的次序并不固定,而Java和C#则以特定的顺序完成。但C++中能够肯定的是,new Widget
必定比tr1::shared_pt
r先执行,但对priority()函数的调用却没有限定。若是如下面的顺序:
new Widget
priority()
函数tr1::shared_ptr
构造函数这就会引起一个问题,若是第二步priority()
函数发生异常,那么new Widget就没法放入shared_ptr
中,这样就会形成资源泄漏(shared_ptr
用来进行资源管理)。正确的作法是将语句分离,先建立资源并放到资源管理器后,再进行下步操做。
//先建立对象并置入资源管理器中 std::tr1::shared_ptr<Widget> pw(new Widget); //再进行下步操做 processWidget(pw, priority);
1) 好的接口容易被正确使用。 2) 保持接口的一致性,与内置类型行为兼容。 3) 为阻止误用,能够采用创建新类型、限制类型上的操做,束缚对象值,消除客户的资源管理责任。 4) tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁。
1) class的设计就是type的设计。在定义一个新type以前,请肯定符合一些规范。
1) 尽可能以pass-by-reference-to-const替换pass-by-value。前者一般比较高效,并可避免切割问题。 2) 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value每每比较适当。
1) 毫不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向local static对象而有可能同时须要多个这样的对象。
1) 切记将成员变量声明为private。这样能够保证数据的一致性、可细微划分方向控制、允诺条件得到保证,并提供class做者以充分的实现弹性。 2) protected并不比public更具封装性。
1) 宁肯拿non-member non-friend函数替换member函数。这样能够增长封装性、包裹弹性和机能扩充性。
1) 若是你须要为某个函数的全部参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
1) 当std::swap效率不高时,能够提供本身版本的swap,但要保证这个swap不会抛出异常。 2) 若是提供一个member swap,也应该提供一个non-member swap来调用前者。 3) 调用swap时,使用using std::swap;
声明std::swap,而后调用swap而且不加任何“命名空间修饰符”。 4) 为“用户定义类型”进行std template特化是好的,但不要尝试在std内加入某些对std而言是全新的东西。
1) 尽量延后变量定义的时间。这样能够增长程序的清晰度并改善程序效率。
std::string encryptPass(string& pass){ using namespace std; //在抛出异常前定义,若是抛出了异常,则没有必要定义这个变量 string encrypted; if (pass.length() < MinLenth){ throw login_error("Password is too short"); } //应该把变量移动这里 //string encrypted; ... return encrypted; }
1) 两个旧式转型。 1. (T)expression 2. T(expression) 2) 四个新式转型。 1. const_cast
: 将const转为non-cast。 2. dynamic_cast
: 将父类转为子类(耗费重大,循环中尽可能不要用)。 3. reinterpret_cast
: 执行低级转型,根据编译器不一样有所改变,不能够移植。(不多用)。 4. static_cast
: 作上面三个转型的逆操做。 3) 若是能够,尽可能避免转型,特别是在注重效率的代码避免使用dynamic_casts
。可使用virtual的继承去实现-_1!
。 4) @^@
若是能够将转型放在函数背后,客户能够调用该函数,而不须要进行转型操做。 5) 宁肯使用新式(C++-style)转型,不要使用旧式转型。前者更明确,更容易查找。
1) 避免返回handles(包括指针、reference、迭代器)指向对象内部。这能够增长封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码”的可能性降至最低。
1) 异常安全函数(Exception-safe functions)即便发生异常也不会泄漏资源或容许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。 2) “强烈保证”每每可以以copy-and-swap
实现,但要考虑资源消耗和效率问题,不是全部状况都有必要的。 3) 函数提供的“异常安全保证”一般最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。