Effective C++

來源:互聯網
上載者:User

最近又重新看了Effective C+,不過到現在還是有好多地方不懂的,先記下筆記,待用的時候再細細琢磨。

條款1:盡量用const和inline而不用#define

這個條款最好稱為:“盡量用編譯器而不用預先處理”,因為#define經常被認為好象不是語言本身的一部分。

用const的好處是,調試時,可以直接擷取變數,而非定義的數字,這個在使用gdb跟蹤代碼的時候很有用,比如#define NUM 123;如果在gdb中print NUM,會出現NUM找不到符號表的問題,這樣在複雜運算式中出現NUM進行watch的時候要回去找NUM的具體值很鬱悶。

使用const不僅如此,其不允許改動的語義才是其存在的精華, 常量指標和指標常量很容易讓人弄混,來再記一遍,星號在中間,左定內容右定針。
const char *p;    char const* p都表明p指向的內容不能變
char * const p 則表明指標不能變.

拋棄#define使用inline的原因就是類似#define max(a,b) ((a)>(b)?(a):(b)) 這種寫法除了使用無數括弧很bt之外,max(++a,b)這個簡單的運算式便可輕而易舉廢掉程式員的本意,這點足以放棄#define。

條款2:盡量用<iostream>而不用<stdio.h>

EC(Effective c++)給出Item2的理由,cin/cout型別安全,比如Rational r; cout<<r在stdio.h中是無法想象的。當然,Rational要針對<<進行支援,有關寫operator <<又是一個要討論的Item,這裡事先給出正確寫法
class Rational{
    friend ostream& operator<<(ostream&s, const Rational& r)
}

ostream& operator<<(ostream& s, const Rational& r)
{
    s<< r.n << '/' << r.d;
    return s;
}

順便說一句,本條款的標題沒有列印錯;我確實說的是<iostream>而非<iostream.h>。從技術上說,其實沒有<iostream.h>這樣的東西——標準化委員會在簡化非C標準標頭檔時用<iostream>取代了它。他們這樣做的原因在條款49進行瞭解釋。還必須知道的是,如果編譯器同時支援 <iostream>和<iostream.h>,那標頭檔名的使用會很微妙。例如,如果使用了#include <iostream>, 得到的是置於名字空間std(見條款28)下的iostream庫的元素;如果使用#include <iostream.h>,得到的是置於全域空間的同樣的元素。在全域空間擷取元素會導致名字衝突,而設計名字空間的初衷正是用來避免這種名字衝突的發生。還有,打字時<iostream>比<iostream.h>少兩個字,這也是很多人用它的原因。:)

條款3:盡量用new和delete而不用malloc和free

malloc和free(及其變體)會產生問題的原因在於它們太簡單:他們不知道建構函式和解構函式。
new的過程:申請記憶體(也即malloc的作用),調用建構函式,返回對象指標(後面講到operator new ,placement new都是基於這個基本知識)

另外,new和delete對應,malloc和free對應這個也是常識了,但是為啥呢?EC裡面講到,如果混用會導致不可預料的錯誤。

條款4:盡量使用c++風格的注釋

相對/**/這種注釋,多用用//,而//在VS2005以及Eclipse下面都有快速鍵,VS2005是Ctrl+K,C(按住Ctrl,先後按K和C)取消是Ctrl+K,U,Eclipse則方便的多,只用Ctrl+/即可

條款5:對應的new和delete要採用相同的形式

簡單的說,就是單個對象和數組要區分對待。C++使用[]區分這是單個對象還是數組,所以new中有[]的時候,請用delete[]。

條款6:解構函式裡對指標成員調用delete

這條為了防止記憶體泄露,具體說來要做三件事情:

  每個建構函式中將該指標初始化
  每個賦值運算子中將原有記憶體刪除,重新設定一塊
  每個解構函式中,delete這個指標

條款7:預先準備好記憶體不夠的情況

operator new申請記憶體得不到滿足時,在拋出std::bad_alloc之前會調用使用者佈建的handler,該調用找到足夠的記憶體才停止

typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw();

因此,自己定義new handler需要遵循以下原則:

讓更多記憶體可用
自己處理不了的情況下,安裝一個不同的new handler
卸載這個new handler,拋出std::bad_alloc
直接調用abort或者exit
現在標準的operator new的行為時拋出一個std::bad_alloc的異常,其實,現在很少有情況會無法申請到記憶體,按照標準的做法即可。在拋出bad_alloc異常之後,做好log記錄和分析,一旦遇到這種情況,加記憶體就是了。

條款8: 寫operator new和operator delete時要遵循常規

void * operator new(size_t size)

{

if(size==0){ //1.大小為0的new也可以成功

size=1;

}

while(true){ //2.不斷迴圈,嘗試申請記憶體

if(alloc success) return *pointer //3.成功返回指標

//4.不成功,處理錯誤

new_handler globalHandler = set_new_handler(0);

set_new_handler(globalHandler);

if(globalHandler) (*globalHandler)();

else throw std::bad_alloc(); //5.沒有處理函數,則拋異常

}

}

delete應該遵循的則很簡單,即刪除一個null指標永遠是安全的

void operator delete(void * rawMemory)

{

if(rawMemory ==0 ) return;

否則再刪除記憶體

}

條款9: 避免隱藏標準形式的new

因為內部範圍聲明的名稱會隱藏掉外部範圍的相同的名稱,所以對於分別在類的內部和全域聲明的兩個相同名字的函數f來說,類的成員函數會隱藏掉全域函數

條款10: 如果寫了operator new就要同時寫operator delete

讓我們回過頭去看看這樣一個基本問題:為什麼有必要寫自己的operator new和operator delete?

答案通常是:為了效率。預設的operator new和operator delete具有非常好的通用性,它的這種靈活性也使得在某些特定的場合下,可以進一步改善它的效能。尤其在那些需要動態分配大量的但很小的對象的應用程式裡,情況更是如此。

預設的operator new需要在返回指標的前方使用一點空間記錄該指標佔用的大小(該空間稱作cookie),用於delete的正常運行。自己重載operator new,則可以自己進行管理這個區塊,減少記憶體使用量

實現記憶體池,每次從記憶體池中申請,若記憶體池也不夠的話,則擴張之

所以,寫了一個operator new之後,要對應寫一個operator delete,因為只有自己才知道到底是如何申請記憶體的。

條款11: 為需要動態分配記憶體的類聲明一個拷貝建構函式和一個賦值操作符

也就是說,class內有一個指標,使用new來動態申請記憶體的情況下,預設的copy constructor和assignment運算子是淺拷貝(bitwise copy),也即直接拷貝指標的值,可能會有記憶體泄露的危險
String a("hello");{String b("hello");b=a;}當b=a,b原來的內容變成野指標,當b結束範圍後a的內容也被刪除。這真是災難
所以條款11告訴我們:class內有指標需要申請記憶體,則自己撰寫拷貝構造和賦值函數,避免記憶體泄露和異常。

條款12: 盡量使用初始化而不要在建構函式裡賦值

原因:
1,const、reference只能通過初始化列表進行初始化
2,從效率角度。對象的構造分成兩階段:初始化data member(可以根據初始化列表進行,無則初始化為0等預設值),執行被調用的建構函式。所以執行assignment實際執行了兩次賦值。
3,基本類型的non-const, non-reference對象,初始化和賦值之間沒有2所說的區別

條款13: 初始化列表中成員列出的順序和它們在類中聲明的順序相同

編譯器構造和析構的順序是相反的,編譯器不可能針對初始化列表中的順序進行初始化,否則重載不同初始化順序的建構函式會讓編譯器頭暈的。編譯器內部確定是按照class內的聲明次序,如果初始化列表不同,很可能初始化列表的資料會錯誤。
核心:先按class內聲明成員預設賦值,然後調用初始化參數列表進行初始化。

條款14: 確定基類有虛解構函式

基類指標指向具體衍生類別,delete基類指標的時候,需要虛函數進行多態。
小tip:如果解構函式不是虛的,那麼基類和衍生類別的析構都要調用 ,先調用派生,再調用基類
tip2:虛函數要佔用class空間,要綜合考量

條款15: 讓operator=返回*this的引用

原因:objA = objB = objC這種連續的重載=號行為
e.g.

String& String::operator =(const String&rhs)
{

return *this;
}

條款16: 在operator=中對所有資料成員賦值

原因:編譯器會預設為你產生一個operator=,採用bitwise,所以最好都是自己寫一個
注意點:繼承機制的引入,Base中的私人成員Derived對象無法訪問
要麼 Base::operator=(rhs),要麼staqtic_cast<Base&>(*this) = rhs兩種方式解決

條款17: 在operator=中檢查給自己賦值的情況

一般採用的方法:

C& C::operator=(const C&rhs)
{
if(this==&rhs) return * this;
}

這主要是針對如何判斷對象相等的問題,這裡採用的是地址相等的方法

條款18: 爭取使類的介面完整並且最小

條款19: 分清成員函數,非成員函數和友元函數

成員函數和非成員函數最大的區別在於成員函數可以是虛擬而非成員函數不行。所以,如果有個函數必須進行動態綁定(見條款38),就要採用虛擬函數,而虛擬函數必定是某個類的成員函數。
1,如果要實現虛函數,必須是member function
2,讓operator<<和operator>>成為non-members,如果還需要擷取類的非公用成員變數,聲明為friend。原因,如果是func為member,那麼以後書寫順序應該是obj>>cin,obj<<cout,這樣不符合習慣
3,只有non-member才能在最左參數身上實施型別轉換。如果需要對函數f的最左側參數進行型別轉換,那麼f為non-function,如果還需要擷取類的非公用成員變數,聲明為frind。
舉例,operator *(Class &lhs, Class &rhs)這種聲明,2*obj2的調用,需要對2進行型別轉換(建構函式聲明為explicit可以阻止隱式型別轉換),這樣就必須為non-member

條款20: 避免public介面出現資料成員

Effective中舉了三個原因,說明為什麼不要放在公開介面中

一致性,以後對類對象的所有操作,均需要帶(),也就是只能調用函數,不能擷取變數
擷取控制性,比如唯讀、可讀可寫、不處理,通過不同的函數實現
函數抽象性,提供一個借口,底層如何?上層使用者不用關心
不過在實際編程中,很少人能夠完全做到這點,畢竟需要自己花些時間來寫get和set,暫時我也沒找到自動產生get、set函數的方法,所以魚與熊掌不可兼得,若想獲得好處,就得費力寫get、set了。

條款21: 儘可能使用const

使用const的好處在於它允許指定一種語意上的約束——某種對象不能被修改——編譯器具體來實施這種約束。通過const,你可以通知編譯器和其他程式員某個值要保持不變。只要是這種情況,你就要明確地使用const ,因為這樣做就可以藉助編譯器的協助確保這種約束不被破壞。

const關鍵字實在是神通廣大。在類的外面,它可以用於全域或名字空間常量(見條款1和47),以及靜態對象(某一檔案或程式區塊範圍內的局部對象)。在類的內部,它可以用於靜態和非靜態成員(見條款12)。

1,*號在中間,前定內容後定針
2,傳回值用const修飾,說明傳回值是唯讀,不能修改
3,函數後面用const修飾,說明該函數不能修改任何變數。函數可以據此進行重載,有const的函數被const對象調用,沒有const的函數被非const對象調用
const的真正意義是什嗎?不變性,具體的體現有兩種說法:A, bitwise. B, conceptual,A說法對位進行比較,如果沒有修改則認為是不變的。B從概念層面進行判斷,即使底層有修改,但對上層概念來講是不變的,那就是不變的。但是C++語言只支援A,所以為了應付B,引入mutable修飾詞,用來修飾上層概念不變,但是底層要修改的底層變數。

條款22: 盡量用“傳引用”而不用“傳值”

c語言中,什麼都是通過傳值來實現的,c++繼承了這一傳統並將它作為預設。除非明確指定,函數的形參總是通過“實參的拷貝”來初始化的,函數的調用者得到的也是函數傳回值的拷貝。

正如我在本書的導言中所指出的,“通過值來傳遞一個對象”的具體含義是由這個對象的類的拷貝建構函式定義的。這使得傳值成為一種非常昂貴的操作。

void printnameanddisplay(const window& w)

C語言裡面都是傳值
傳值成本比較大,會調用對象的拷貝構造,如果類比較複雜,則會建立和析構更多的對象
傳引用會避免切割問題。Func(base&) 和Func(base)兩種函式宣告,內部調用f()虛函數,如果傳遞個derived對象,則傳引用會調用derived.f(),而傳值則會切割而調用base.f()

條款23: 必須返回一個對象時不要試圖返回一個引用

用重載乘法舉例

Inline const Rational Operator*( const Rational& lhs, const Rational & rhs)

{

return Rational(lhs.n*rhs.n, lhs.d*rhs.d);

}

傳回的是value,如果傳回reference的話,內部變數析構之後,引用沒有真正的對象

寫一個必須返回一個新對象的函數的正確方法就是讓這個函數返回一個新對象。

條款24: 在函數重載和設定參數預設值間謹慎選擇

void g(int x=0);

g();

g(10);

void f(); void f(int x);

f();

f(10);

兩種方式要謹慎選擇,避免出現模稜兩可的情況

條款25: 避免對指標和數字類型重載

void f(int x);

void f(string *ps);

f(0)

0的存在會對指標和數值造成模稜兩可,所以要堅決避免針對指標和數值進行重載

條款26: 當心潛在的二義性

C++有一個哲學信仰,它相信潛在的模稜兩可狀態不是一種錯誤,但是對程式員來講,將所有問題放到運行後發現就是一種災難。所以程式員應該避免模稜兩可。
類的轉換,一是拷貝構造方式可以隱式轉換,一是operator Class()方式,當需要型別轉換時,就會有模稜兩可

語言標準轉換,6.02可以轉換成int也可以轉換成char
多繼承也是如此
當遇到模稜兩可情況時,程式員應該顯式的說明採用哪種方式。

條款27: 如果不想使用隱式產生的函數就要顯式地禁止它
使用private修飾防止公開調用
不定義防止friend等調用
private:

Array& operator=(const Array &rhs);(注意這裡;表示不定義)

條款28: 劃分全域名字空間

namespace name1{

}

using namespace name1;

最好每個人都以自己姓名為name,進行分割,這樣可以類似Java中的包的概念

條款29: 避免返回內部資料的控制代碼

傳回handle之後,打破了抽象性,所以要避免
對於non-const member functions而言,傳回內部handle也會導致麻煩,當涉及暫時對象,Handle可能變成懸空的(dangling)

條款30: 避免這樣的成員函數:其傳回值是指向成員的非const指標或引用,但成員的訪問級比這個函數要低

條款31: 千萬不要返回局部對象的引用,也不要返回函數內部用new初始化的指標的引用

條款32: 儘可能地延遲變數的定義

延遲變數定義可以提高程式的效率,增強程式的條理性,還可以減少對變數含義的注釋。看來是該和那些開放式模組的變數定義吻別了。
需要的時候再定義,延緩定義式的出現,當出錯時就會減少記憶體的使用。

條款33: 明智地使用內聯

內嵌函式------多妙的主意啊!它們看起來象函數,運作起來象函數,比宏(macro)要好得多(參見條款1),使用時還不需要承擔函數調用的開銷。你還能對它們要求更多嗎?

然而,你從它們得到的確實比你想象的要多,因為避免函數調用的開銷僅僅是問題的一個方面。為了處理那些沒有函數調用的代碼,編譯器最佳化程式本身進行了專門的設計。所以當內聯一個函數時,編譯器可以對函數體執行特定環境下的最佳化工作。這樣的最佳化對"正常"的函數調用是不可能的。
1,好處:直接用代碼替換,減少函數調用成本
2,壞處:造成代碼膨脹現象,可能會導致病態的換頁現象
3,大部分編譯器會拒絕將複雜的(內有迴圈或遞迴調用)函數inline,而所有虛擬函數都不能inline
4,建構函式和解構函式最好不要inline,即使inline,編譯器也會產生出out-of-line副本,以方便擷取函數指標

條款34: 將檔案間的編譯依賴性降至最低

條款35: 使公有繼承體現 "是一個" 的含義

條款36: 區分介面繼承和實現繼承

聲明一個純虛函數的目的是讓子類只繼承其介面
聲明一般(非純)虛函數的目的,是為了讓子類繼承該函數的介面和預設行為
聲明非虛函數的目的是為了讓子類繼承函數的介面和實現。且"不變性"淩駕於"變異性"之上,我們不應該在子類重新定義它。

條款37: 決不要重新定義繼承而來的非虛函數

條款38: 決不要重新定義繼承而來的預設參數值

條款39: 避免 "向下轉換" 繼承層次

條款40: 通過分層來體現 "有一個" 或 "用...來實現"

條款41: 區分繼承和模板

模板用來產生一群class,其中對象性別不會影響class的函數行為

繼承應用於一群class身上,其中對象性別會影響class的函數行為

條款42: 明智地使用私人繼承

私人繼承意味著 "用...來實現"。如果類D私人繼承於類B,類型D的對象只不過是用類型B的對象來實現而已;類型B和類型D的對象之間不存在概念上的關係

如果是私人繼承,編譯器不會隱式的將子類對象轉化成基類對象
私人繼承,基類所有函數在子類都變成私人屬性
私人繼承意味著根據某物實現,與layering相比,當protected members和虛擬函數牽扯進來會有很大的優越性。
私人繼承,子類僅僅是使用了父類中的代碼,他們沒有任何概念上的關係。

條款43: 明智地使用多繼承

多繼承會產生模稜兩可,子類調用方法如何兩個父類都有,則必須指明使用的是哪個父類
多繼承會產生鑽石型繼承體現,為了使得祖先類只有一份,請在兩個父類繼承祖先的時候採用虛繼承(而這在設計祖先類的時候一般是無法預料到的)
可以通過public繼承方式繼承介面,private繼承方式繼承實現,來完成目的

條款44: 說你想說的;理解你所說的

條款45: 弄清C++在幕後為你所寫、所調用的函數

條款46: 寧可編譯和連結時出錯,也不要運行時出錯

條款47: 確保非局部靜態對象在使用前被初始化

條款48: 重視編譯器警告

條款49: 熟悉標準庫

條款50: 提高對C++的認識

 

參考:  http://www.cnblogs.com/liuchen/archive/2009/10/21/1587278.html

           《Effective c++》

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.