學習《Effective C++》

學習《Effective C++》

#@date:			2014-06-16
#@author: 		gerui
#@email:		forgerui@gmail.com

  前幾天買了好幾本書,其中有一本是《Effective C++》,準備好好學習一下C++.書中提出了55條應該遵循的條款,下面將逐一學習。點擊查看Evernote原文c++

1、讓本身習慣C++

1. 視C++爲一個語言聯邦

  將C++分爲4個次語言。即:C, Objective-Oriented C++, Template C++, STL.數據庫

2. 儘可能以 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

3. 儘量使用 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]);
		} 
};

4. 肯定對象使用前先被初始化

  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 對象會在"該函數被調用期間","首次趕上該對象之定義式"時被初始化,這樣就保證了對象被初始化。這樣作的好處是不調用函數時,不會產生對象的構造和析構。但對多線程這樣的方法會有問題。

2、構造/析構/賦值運算

5. 瞭解C++默默編寫並調用哪些函數

  1) 編譯器會自動爲class建立default構造函數、copy構造函數、copy assignment操做符、以及析構函數。

  2) 若是用戶聲明瞭一個構造函數,則編譯器則不會再爲它聲明default構造函數。

  3) 拷貝構造函數能夠經過=()實現。默認的拷貝構造函數對指針進行地址的複製,這樣會產生多個對象共用一塊地址,會產生問題,能夠本身實現拷貝構造函數,實現值的複製。

6. 若不想使用編譯器自動生成的函數,就該明確拒絕

  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&);
};

7. 爲多態基類聲明virtual析構函數

  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。

8. 別讓異常逃離析構函數

  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函數,則能夠在析構函數中自動調用。因此,在寫程序時,必定要將會發生異常的函數做爲一個普通函數,這樣能夠提供更多的選擇。

9. 毫不在構造和析構過程當中調用virtual函數

  1) 在構造和析構函數期間不要調用virtual函數,由於這類調用從不降低到derived class(子類)。父類的構造函數先於子類執行,因此父類的自身成分早於子類構造,子類的virtual函數尚未生成,因此即便調用virtual函數,也只會調用父類的virtual函數,即這個被聲明爲virtual的函數在構造函數中毫無心義。

10. 令operator= 返回一個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;
		}
}

11. 在operator=中處理「自我賦值」

  1) 確保當對象自我賦值時,operator=有良好行爲。其中技術包括比較「來源對象」和「目標對象」的地址、精心周到的語句順序、以及copy-and-swap。

  2) 肯定任何函數若是操做一個以上的對象,而其中多個對象是同一個對象時,其行爲仍然正確。

12. 複製對象時勿忘其每個成分

  1) Copying函數應該確保複製「對象內的全部成員變量」及「全部base成分」。

  2) 不要嘗試以某個copying函數實現另外一個copying函數。應該將共同機能放進第三個函數中,並由兩個copying函數共同調用。

3、資源管理

13. 以對象管理資源

  1) 防止資源泄漏,請使用RAII(Resource Acquisition is Initialization;資源取得時機即是初始化時機)對象,它們在構造函數中得到資源並在析構函數中釋放資源。

  2) 兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者一般是較佳選擇,由於其copy行爲比較直觀。若選擇auto_ptr,複製動做會使它(被複制物)指向null,即只有一個對象指向這個資源。

14. 在資源管理類中當心copying行爲

  1) 複製RAII對象必須一併複製它所管理的資源,因此資源的copying行爲決定RAII對象的copying行爲。      2) 廣泛而常見的RAII class copying行爲是:抑制copying、施行引用計數法。不過其餘行爲也均可能被實現。

15. 在資源管理類中提供對原始資源的訪問

  1) APIs每每要求訪問原始資源,因此每個RAII class應該提供一個「取得其所管理的資源」的方法。      2) 對原始資源的訪問可能經由顯式轉換或隱式轉換。顯式轉換更安全,隱式轉換更方便。   

16. 成對使用new和delete時要採起相同形式

  1) 若是new數組時使用[],那麼釋放資源時就要用delete[],這會調用多個析構函數去釋放資源;若是使用new對象不使用[],釋放時必定不要使用[]。保持二者一致

std::string str = new std::string;
std::string strArr = new std::string[20];
//釋放資源
delete str;
delete[] strArr;

17. 以獨立語句將newed對象轉入智能指針

  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_ptr先執行,但對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);

4、設計與聲明

18. 讓接口容易被正確使用,不易被誤用

  1) 好的接口容易被正確使用。   2) 保持接口的一致性,與內置類型行爲兼容。   3) 爲阻止誤用,能夠採用創建新類型、限制類型上的操做,束縛對象值,消除客戶的資源管理責任。   4) tr1::shared_ptr支持定製型刪除器。這可防範DLL問題,可被用來自動解除互斥鎖。

19. 設計class猶如設計type

  1) class的設計就是type的設計。在定義一個新type以前,請肯定符合一些規範。

20. 寧以pass-by-reference-to-const替換pass-by-value

  1) 儘可能以pass-by-reference-to-const替換pass-by-value。前者一般比較高效,並可避免切割問題。   2) 以上規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value每每比較適當。

21. 必須返回對象時,別妄想返回其reference

  1) 毫不要返回pointer或reference指向一個local stack對象,或返回reference指向一個heap-allocated對象,或返回pointer或reference指向local static對象而有可能同時須要多個這樣的對象。

22. 將成員變量聲明爲private

  1) 切記將成員變量聲明爲private。這樣能夠保證數據的一致性、可細微劃分方向控制、允諾條件得到保證,並提供class做者以充分的實現彈性。   2) protected並不比public更具封裝性。

23. 寧以non-member、no-friend替換member函數

  1) 寧肯拿non-member non-friend函數替換member函數。這樣能夠增長封裝性、包裹彈性和機能擴充性。

24. 若全部參數皆需類型轉換,請爲此採用non-member函數

  1) 若是你須要爲某個函數的全部參數(包括被this指針所指的那個隱喻參數)進行類型轉換,那麼這個函數必須是個non-member。

25. 考慮寫出一個不拋異常的swap函數

  1) 當std::swap效率不高時,能夠提供本身版本的swap,但要保證這個swap不會拋出異常。   2) 若是提供一個member swap,也應該提供一個non-member swap來調用前者。   3) 調用swap時,使用using std::swap;聲明std::swap,而後調用swap而且不加任何「命名空間修飾符」。   4) 爲「用戶定義類型」進行std template特化是好的,但不要嘗試在std內加入某些對std而言是全新的東西。

5、實現


26. 儘量延後變量定義式的出現時間

  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;
}

27. 儘可能少作轉型動做

  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)轉型,不要使用舊式轉型。前者更明確,更容易查找。  

28. 避免返回handles指向對象內部成分

  1) 避免返回handles(包括指針、reference、迭代器)指向對象內部。這能夠增長封裝性,幫助const成員函數的行爲像個const,並將發生「虛吊號碼」的可能性降至最低。   

29. 爲「異常安全」而努力是值得的

  1) 異常安全函數(Exception-safe functions)即便發生異常也不會泄漏資源或容許任何數據結構破壞。這樣的函數區分爲三種可能的保證:基本型、強烈型、不拋異常型。   2) 「強烈保證」每每可以以copy-and-swap實現,但要考慮資源消耗和效率問題,不是全部狀況都有必要的。   3) 函數提供的「異常安全保證」一般最高只等於其所調用之各個函數的「異常安全保證」中的最弱者。  

相關文章
相關標籤/搜索