1. 程式人生 > 其它 >effective c++_Effective C++ 讀書筆記

effective c++_Effective C++ 讀書筆記

技術標籤:effective c++

作者:maxkibble

連結:https://www.jianshu.com/p/67d2b4e2d417

手頭上有一本 Scott Meyes 的 Effective C++(3rd Edition)https://www.amazon.com/gp/product/0321334876,雖然中文的出版時間是感人的2011年(也就是說C++11的那些新特性都沒討論了),但看網上的一些評論,此書還是值得一讀的。(PS:作者針對C++11和14的新特性有本新書 Effective Modern C++http://shop.oreilly.com/product/0636920033707.do。。

Day 1:

Item 1: View c++ as a federation of languages

我果然還是喜歡在開頭說一點總結性的東西。這裡對條款1做一點記錄。。

C++是一個多重範型程式語言,一個簡單理解C++的方法是把C++視為多個次語言(sublanguage)組成的聯邦,次語言之間的守則可能會不一致。次語言共有4個:C,面向物件(Object-Oriented C++),模版 (Template C++),STL。

一個例子:對內建型別(C-like)而言,pass-by-value通常比pass-by-reference高效;對使用者自定義的物件,由於構造和解構函式的存在,pass-by-reference-to-const往往更好;而在傳遞STL迭代器(iterator)和函式物件(function object)時,pass-by-value則更好,因為它們都是基於C指標實現的。

Item 2: Prefer consts, enums and inlines to #define

#define只是預處理階段的文字替換,我們要儘量利用編譯器提供更多資訊。比較常用的就是const代替#define的常量,用inline函式代替#define的巨集函式。這兩個平時都用的很多了。enum只是在下面很特定的例子中有用:

//1.h
classA{
public:
staticconstintx=5;
inty[x];
};

這裡希望宣告一個常量x,它以類成員形式出現,而且所有物件只儲存它的一個副本。於是就有了這麼一個static const的成員變數,但是舊編譯器並不允許你在static變數宣告時賦值,#define又無法體現x的作用域,這時就可以使用enum:

//new_1.h
classA{
public:
enum{x=5};
inty[x];
};

很無語是吧。。是個挺過時的故事。值得一說的是,你如果想要取1.h中x的地址(其實也是挺奇怪的,相當於取常量地址),是需要重新宣告一下這個成員變數的:

//1.cpp
#include"1.h"
#include

constintA::x;//alreadysetvalueto5,ifnot,assignavaluehere

intmain(){
Aa;
std::cout<std::endl;//okwithorwithouttheredeclaration
std::cout<std::endl;//mustredeclareA::x
return0;
}

Day 2:

Item 3: Use const whenever possible

const是一個神奇的關鍵字,它是C++語法的一部分,卻允許你對程式的語義做出限制。應該儘可能在程式中使用const關鍵字,這樣編譯器能夠幫你做更多。

classRational{
constRationaloperator*(constRational&lhs,constRational&rhs){...}
};

intmain(){
Rationala,b,c;
//shouldbe"==",butwillpasscompilationif"const"missed
if((a*b)=c){...}
}

const在指標上有兩種語義:1)指標本身不能指向其他地址;2)指標所指地址的值不能變動。分別對應:

chargreeting[]="hello";
char*constp1=greeting;
constchar*p2=greeting;

Item 1中提到了,STL的迭代器是基於指標的。在STL中這兩類迭代器分別對應:

std::vector<int>vec;
conststd::vector<int>::iteratoriter1=vec.begin();
std::vector<int>::const_iteratoriter2=vec.begin();

const在C++中如此重要的另一個因素是在使用者自定義型別上,pass-by-reference-to-const比pass-by-value高效。因此,應當儘可能多用const關鍵詞修飾成員函式,已方便在const物件上的操作。


關於一個成員函式是否能用const關鍵字,編譯器關心的是bitwise constness,即函式內部沒有對成員變數的賦值;而程式設計師更應當關心程式事實執行中的logical constness,即有些變數可能會被修改,但需要保持const的變數事實上是不會變的,兩個例子:

//aprogramto"beat"bitwiseconstness
//weonlyapplyconstfunctiononconstobject,butthestringvaluechanged

#include
#include

classText{
char*content;
public:
//...
char&operator[](std::size_tidx)const{
returncontent[idx];
}
};

intmain(){
chargreeting[]="hello";
constTexttext=Text(greeting);
char*p=&text[0];
*p='j';
std::cout<0]<std::endl;
return0;
}

注意,這個例子並不是說編譯器錯了,事實上它的確滿足bitwise constness原則,content指標的值並沒有發生變化。但我們明顯是希望content所指向的字串也保持不變。事實上,這是使用指標的問題,在過載[]時返回const char&或者用string型別作為成員變數,編譯器都能發現這段程式的錯誤行為。

//asnippetexplaining"logicalconstness"
//theconstfunctiongetLength()updatestwovariables
//andtheyaredeclaredasmutable

classText{
char*content;
mutablestd::size_tlength;
mutableboollengthIsValid;
public:
//...
std::size_tgetLength()const{
if(!lengthIsValid){
length=strlen(content);
lengthIsValid=true;
}
returnlength;
}
};

最後,很多時候我們會為const物件實現一個const成員函式,為非const物件實現一個非const成員函式。他們行為幾乎完全一樣,如上述過載[]前要做一系列合法性檢查。那麼為了減少程式碼冗餘,一個常見的操作是用非const函式呼叫const函式,這裡展示一下C++風格的型別轉換寫法:

constchar&operator[](std::size_tidx)const{
//manylinesofcode
returncontent[idx];
}
char&operator[](std::size_tidx){
returnconst_cast<char&>(static_cast<constText&>(*this)[idx]);
}

Day 3:

Item 4: Make sure objects are initialized before they're used

C++中來自C語言的資料型別是不保證初始化的,因此請手動初始化所有的內建型別。

對於使用者定義型別,請使用成員初值列(member initialization list)代替賦值操作,這是出於效率上的考慮:

//constructorinassignmentstyle
A::A(conststring&s,conststd::list<int>l){
str=s;lst=l;
}
//amoreefficientconstructor
A::A(conststring&s,conststd::list<int>l):str(s),lst(l){}

具體來說,對於A類的成員變數str和lst(它們都不是內建型別),它們的初始化時間在呼叫A建構函式之前,也就是說在上面的寫法中,str和lst先用各自的預設建構函式初始化後,又進行了一次賦值;而在下面的寫法中,它們就是用引數值初始化的。

對於跨檔案的static變數,請用local-static物件替換non-local static物件。因為跨檔案的static變數初始化順序是不確定的,而有時我們必須指定特定順序,舉例而言:

//fs.h
classFileSystem{
public:
//linesofcode
voidfoo(){}
};

FileSystem&fs(){
staticFileSystemfsObj;
returnfsObj;
}
//fs.cpp
tfs().foo();

呼叫全域性變數就不太好了:

//fs.cpp
externFileSystemfs;
fs.foo();

當有多個跨檔案的全域性變數時,上面寫法中FileSystem物件的建立時間是可控的,下面則可能造成多個物件建立順序的混亂。

Day 4:

Item 5: Know what functions C++ silently writes and calls

你寫了一個空的類(只聲明瞭一些成員變數),C++編譯器會按需為你生成default建構函式,copy建構函式,解構函式以及過載賦值符號。而一旦你聲明瞭對應的函式,C++就不會再默默為你做這些事。

classEmpty{};
//theabovecodeequalsbelow
classEmpty{
Empty(){...}
Empty(constEmpty&rhs){...}
~Empty(){...}
Empty&operator=(constEmpty&rhs){...}
};

這些預設函式的行為都是naive的,對於基礎型別,copy建構函式和賦值只做簡單的bits拷貝。對於string這樣定義了copy建構函式和賦值的型別,則會呼叫對應函式。


這種naive方法當然有失效的時候,比如你定義了一個const成員變數,或者你的成員變數是一個引用,那麼很明顯bits拷貝的方法是不行的,你必須手動初始化這兩類變數。編譯器會提示你顯式地編寫建構函式,copy賦值函式,過載賦值符。

Day 5:

Item 6: Explicitly disallow the use of compiler-generated functions you do not want

有些類不應當存在拷貝操作,讓編譯器幫助你的方法是將這個類的copy建構函式和賦值過載宣告為private

classUnCopyable{
UnCopyable(constUnCopyable&);//parameternamecanbeskipped
UnCopyable&operator=(constUnCopyable&);
public:
//linesofcode
};

為了進一步防止成員函式和友元函式呼叫賦值,你需要使這兩個private函式的函式體為空。這樣如果你在成員函式或友元函式中使用了賦值,聯結器(linker)會報錯。

Item 7: Declare destructors virtual in polymorphic base classes

先看一份程式碼:

#include

usingnamespacestd;

classA{
public:
intx;
virtual~A(){
cout<"DestructorAiscalled"<endl;
}
};

classB:publicA{
public:
inty;
~B(){
cout<"DestructorBiscalled"<endl;
}
};

classC{
public:
intx;
~C(){
cout<"DestructorCiscalled"<endl;
}
};

intmain(){
A*ptr=newB();
deleteptr;
cout<"sizeA:"<sizeof(A)<endl;
cout<"sizeC:"<sizeof(C)<endl;
return0;
}

執行結果:

Compiler:GNUG++146.4.0
Ouput:
DestructorBiscalledDestructorAiscalledsizeA:8
sizeC:4

這份程式碼告訴我們兩件事:


1)不要無端的將一個類的某些函式宣告為virtual,這會使你的類體積變大,其原因是有虛擬函式的類會有一個虛指標(virtual table pointer),用於指向執行時實際呼叫的函式。
2)如果一個類是作為基類使用,並且會設計多型,那麼一定要將建構函式宣告為virtual,否則示例程式碼中只會呼叫A類的解構函式,從而B類在堆中為y申請的記憶體會發生洩漏。

Day 6:

Item 8: Prevent exceptions from leaving destructors

C++中當有多個異常被丟擲時,程式不是結束執行就是導致不明確行為。而如果在解構函式中丟擲異常,那麼這樣的情況非常容易發生,比如銷燬一個vector!因此,絕對不應該讓解構函式丟擲異常,而是應該在解構函式中就對其捕捉並處理(考慮剛才提到的危害,在解構函式中發現異常後直接退出程式也是可以接受的)。


另一個做法是將可能丟擲異常的部分程式碼獨立出來作為一個成員函式,讓使用者手動去呼叫並處理可能帶來的異常。類本身也可以設定一個變數記錄進入解構函式時這個可能丟擲異常的程式是否已經被呼叫。如果沒有,那麼捕獲並處理掉這個異常,如果已經被呼叫,那麼就可以擺脫異常處理這一煩惱了!這相當於是將部分風險責任分擔給了使用者從而獲得的“雙保險”做法。

Item 9: Never call virtual functions during constructor or destructor

虛擬函式的作用是在多型環境下呼叫正確類的函式。而在建構函式和解構函式中使用虛擬函式則會無法保證這一點,這可能會帶來困惑。究其原因是一個派生類在呼叫自己的建構函式前,會先呼叫基類的建構函式,此時這個派生類物件的執行時型別(runtime type information)就會是基類!如果基類的建構函式中呼叫了虛擬函式,這個虛擬函式是沒有辦法指向派生類中的實現的!解構函式的道理類似。看完這段,不如猜一猜下面程式碼的輸出吧!

#include

classA{
intx;
public:
A(){
std::cout<"ConstructA"<std::endl;
foo();
}
virtualvoidfoo(){
std::cout<"A::foo()invocation"<std::endl;
}
~A(){
std::cout<"DestructA"<std::endl;
foo();
}
};

classB:A{
inty;
public:
B(){
std::cout<"ConstructB"<std::endl;
}
voidfoo(){
std::cout<"B::foo()invocation"<std::endl;
}
~B(){
std::cout<"DestructB"<std::endl;
}
};

intmain(){
Bb;
return0;
}

正確答案:

ConstructA
A::foo()invocation
ConstructB
DestructB
DestructA
A::foo()invocation

●編號431,輸入編號直達本文

●輸入m獲取文章目錄

C語言與C++程式設計

0648ddd51447f1b38c4403385b435fbb.png

分享C/C++技術文章