【c++】條款3:儘可能使用const
1、 const的一件奇妙事情是,它允許你指定一個語義約束(也就是指定一個“不該被改動”的物件),而編譯器會強制實施這項約束。它允許你告訴編譯器和其他程式設計師某值應該保持不變,只要這是事實,你就該確實說出來,因為說出來可以獲得編譯器的相助,確保這條約束不被違反。
2、關鍵字const多才多藝,你可以在classes外部修飾global或namespace作用域中的常量、或修飾檔案、函式、或區塊作用域中被宣告為static的物件。你也可以用它修飾classes內部的static和non-static成員變數。面對指標,你也可以指出指標自身、指標所指物,或兩者都是const
char greeting[] = "Hello" ;
char* p = greeting;
const char* p = greeting;
char* const p = greeting;
const char* const p = greeting;
3、const語法雖然變化多端,但並不莫測高深。如果關鍵字const出現在星號左邊,表示被指物是常量;如果出現在星號右邊,表示指標自身是常量;如果出現在星號兩邊,表示被指物和指標兩者都是常量。如果被指物和指標兩者都是常量。如果被指物是常量,有些程式設計師會將關鍵字const寫在型別之前,有些人會把它寫在型別之後、星號之前。兩種寫法的意義相同,所以下列兩個函式接受的引數型別是一樣的。
void f1(const Widget* pw);
void f2(Widget const * pw);
兩種形式都有人用,你應該試著去習慣它們。
4、STL迭代器系以指標為根據塑模出來,所以迭代器的作用就像個T*指標,宣告
迭代器為const就像宣告指標為const一樣(即宣告為一個T* const指標),表示這個迭代器不得不指向不同的東西,但它所指的東西的值是可以改動的。如果你希望迭代器所指的東西不可被改動,所以你需要的是const_iterator
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
*iter = 10 ;
++iter;
std::vector<int>::const_iterator citer=vec.begin();
*citer = 10;
++citer;
5、const 最具威力的用法是面對函式宣告時的應用。在一個函式宣告式內,const可以和函式返回值、各引數、函式自身。(如果是成員函式)產生關聯。令函式返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而又不至於放棄安全性和高效性。舉個例子,考慮有理數的operator*宣告式:
class Rational{...};
const Rational operator*(const Rational& lns,const Rational& rhs);
這裡為什麼返回一個const物件,原因是如果不這樣客戶就能實現這樣的暴行:
Rational a,b,c;
...
(a*b)=c;
這樣的錯誤也許是無意中的,但這明顯是不符合正常語法的。
if(a*b=c)
如果a和b都是內建型別,這樣的程式碼直截了當就是不合法。而一個“良好的使用者自定義型別”的特徵是它們避免無端的與內建型別不相容,因此允許對兩值乘積作賦值動作也就沒有意思了。將operator*的回傳值宣告為const可以預防那個“沒意思的賦值動作”,那就是該那麼做的原因了。至於const引數,沒有什麼特別新穎的概念,它們不過就像local const物件一樣,你應該在必要的使用它們的時候使用它們。除非你有需要改動引數或local物件,否則請將它們宣告為const。只不過多打六個字元,卻可以避免惱人的錯誤,像是“==”寫成“=”的錯誤,一如稍早所述。
6、const成員函式:
將const實施於成員函式的目的,是為了確認該成員函式可作用於const物件身上,這一類成員函式之所以重要,基於兩個理由,第一,它們使class介面比較容易被理解。這是因為,得知哪個函式可以改動物件內容而那個函式不行,很是重要。第二,它們使“操作const物件”稱為可能。這對編寫高效程式碼是個關鍵,改變c++程式效率的一個根本方法就是以引用方式傳遞物件,而此技術可行的前提是,我們有const成員物件可用來處理取得的const物件。
許多人漠視一個事實:兩個成員函式如果只是常量性不同,可以被過載。這實在是一個重要的c++特性。考慮以下class,用來表現一大塊文字,
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;
};
TextBlock的operator[]S可被這樣使用:
TextBlock tb["Hello"];
std::cout<<tb[0];
const TextBlock std("world");
std::cout<<ctb[0];
附帶一提,真實程式中const物件大多用於passed by pointer-to-const 或 passed by reference-to-const的傳遞結果,上述的ctb例子顯得太過造作,下面這個比較真實
void print(const TextBlock& ctb)
{
std::cout<<ctb[0];
...
}
只要過載operator[]並對不同的版本給予不同的返回型別,就可以令const和non-const TextBlocks獲得不同的處理
std::cout<<tb[0];
tb[0]='x';
std::cout<<ctb[0];
ctb[0]='x';
注意上述錯誤只因operator[]呼叫動作自身沒問題,錯誤起因於企圖對一個“由const版之operator[]”返回的const char&型別實施賦值動作。也請注意,non-const operator[]的返回型別是個reference to char,不是char,下面這樣的句子就無法通過編譯tb[0] = ‘x’;那是因為,如果函式的返回型別是個內建型別,那麼改動函式返回值從來就不合法,縱使合法,c++以by value返回物件這一事實意味著被改動的·其實是tb.text[0]的一個副本,不是tb.text[0]自身,那不會是你想要的行為。
7、讓我們為哲學思辨喊一次暫停。成員函式如果是const意味著什麼?這有兩個流行概念,bitwise constness和logical constness
bitwise const陣營的人相信,成員函式只有在不更改物件之任何成員變數時才可以說是const。也就是它不更改物件中的任何一個bit這樣的好處是很容易偵測違反點:編譯器只需要尋找成員變數的賦值動作即可。bitwise constness正是c++對常量性的定義,因此成員函式不可以更改物件內任何non-static成員變數。
不幸得是許多成員函式雖然不十分具備const性質卻能通過bitwise測試。更具體的說,一個更改了“指標所指物”的成員函式雖然不能算是const,但如果只有指標隸屬於物件,那麼稱此函式為bitwise const不會引發編譯器異議。這導致反直觀結果,假設我們有一個TextBlock-like class ,它將資料儲存為char*而不是string,因為它需要和一個認識string物件的C API溝通:
class CTextBlock
{
public:
...
char& operator[](std::size_t position) const
{
return pText[position];
}
private:
char* pText;
};
這個class不適當地將operator[]宣告為const成員函式,而該函式卻返回一個reference指向物件內部值。假設暫時不管這個事實,請注意,operator[]實現程式碼並不更改pText.於是編譯器很開心的為operator[]產出目標碼。它是bitwise const,所有編譯器都這麼認定。但是看看它允許發生什麼事。
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';
這其中當然不該有任何錯誤:你建立一個常量物件並設以某值,而且只對它呼叫
const成員函式。但你終究還是改變了它的值。
這種情況匯出所謂的logical constness.這一派擁護者主張,一個const成員函式可以修改它所處理的物件內的某些bits,但只有在客戶端偵測不出的情況下才的如此。例如你的CTextBlock class有可能快取記憶體(cache)文字區塊的長度以便應付詢問:
class CTextBlock
{
public:
...
std::size_t length(0 const;
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length()const
{
if(!lengthIsValid)
{
textLength=std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
8、length的實現當然不是bitwise const ,因為textLength和lengthIsValid都可能被
修改。這兩筆資料被修改對const CTextBlock物件而言雖然可接受,但編譯器不同意。它們堅持bitwise constness.。怎麼辦?
解決方法很簡單:利用c++的一個與const相關的擺動場:mutable(可變的)。mutable釋放掉
non-static成員變數的bitwise constness約束
class CTextBlock
{
public:
...
std::size_t length()const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length()const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
在const和non—const成員函式中避免重複:
對於“bitwise-constness非我所欲”的問題,mutable是個解決辦法,但它不能解決所有
的const相關難題。舉個例子,假設TextBlock內的operator[]不單只是返回一個reference指向某字元,也執行邊界檢驗、志記訪問資訊、甚至可能進行資料完善性檢驗,把所有這些同時放進const和non-const operator[]中,導致這樣的怪物:
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;
}
你能說出其中發生的程式碼重複以及伴隨的編譯時間、維護、程式碼膨脹等令人頭疼的問題嗎?當然啦。將邊界檢驗……等所有程式碼移到另一個成員函式(往往是個private)並令兩個版本的operator[]呼叫它,是可能的,但你還是重複了一些程式碼,例如函式呼叫、兩次return語句等等。
你真正該做的是實現operator[]的機能一次並使用它兩次。也就是說,你必須
令其中一個呼叫另一個,這促使我們將常量性排除就一般守則而言,轉型是一個糟糕的想法,我將貢獻一整個條款來談這碼事,告訴你不要這樣做,然而程式碼重複也不是什麼令人愉快的經驗。本例中const operator[]完全做掉了non-const版本該做的一切,唯一的不同是其返回型別多了一個const資格修飾。這種情況下如果將返回值的const轉除是安全的,因為不論誰呼叫
non-const operator[]都一定首先有個non-const物件,否則就不能呼叫non-const函式。所以令non-const operator[]呼叫其const兄弟是一個避免程式碼重複的安全做法-即使過程中需要一個轉型動作。下面是程式碼,稍後有更詳細的解釋:
class TextBlock
{
public:
...
const char& operator[](atd::size_t position) const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>
{
static_cast<const TextBlock>(*this)[position]);
}
...
}
};
如你所見,這份程式碼有兩個轉型動作,而不是一個。我們打算讓non-const operator[]呼叫其const兄弟,但non-const operator[]內部若只是單純呼叫operator[],
會遞迴呼叫自己。那會大概進行一百萬次,為了避免無窮遞迴,我們必須明確指出呼叫得是const operator[],但c++缺乏直接的語法可以這麼做。因此這裡將*this從其原始型別TextBlock&轉型為const TextBlock.是的,我們使用轉型為它加上const!所以這樣共有兩次轉型,第一次用來為*this新增const(這使接下來呼叫operator[])
時得以呼叫const版本),第二次則是從const operator[]的返回值中移除const新增const的那一次轉型強迫進行了一次安全轉型,所以我們使用static_cast.移除const的那個動作只可以藉由const_cast完成,沒有其他選擇(就技術而言其實是有的:一個C-style轉型也行的通,那種轉型很少是正確的抉擇。如果你不熟悉static_cast或const_cast)至於其他動作,由於本例呼叫的是操作符,所以語法有一點點奇特,恐怕無法贏得選美大賽,但卻有我們渴望的“避免程式碼重複”效果,因為它運用const operator[]實現出non-const 版本。為了到達那個目標而寫出如此難看的語法
是否值得,只有你能決定,但“運用const成員函式實現出其non-const 孿生兄弟”的技術是值得了解的。更值的瞭解的是,反向做法—-令const版本呼叫non-const版本以避免重複—並不是你該做的事。記住,const成員函式承諾絕不改變其物件的邏輯狀態,non-const成員函式卻沒有這般承諾。如果在const函式內呼叫mon-const_cast函式,就是冒了這樣的風險,你曾經承諾不改動的那個物件被改動了。這就是為什麼“const成員函式呼叫non-const成員函式”是一種錯誤行為:因為物件有可能因此被改動。實際上若要令這樣的程式碼通過編譯,你必須使用一個const_cast將*this身上的const性質解放掉,這是烏雲罩頂的清洗前兆,反向呼叫才是安全的,non-const成員函式本來就可以對其物件作任何動作,
所以在其中呼叫一個const成員函式並不會帶來風險。這就是為什麼本例以static_cast作用於*this的原因:這裡並不存在const相關風險。
9、本條款一開始就提醒你,const是個奇妙而非比尋常的東西。在指標和迭代器上
;在指標、迭代器及references指涉的物件身上:在函式引數和返回型別身上;
在local變數身上;在成員函式身上,林林總總不一而足。const是個威力強大的
助手。儘可能使用它。你會對你的作為感到高興。
請記住:
(1)將某些東西宣告為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的物件,
函式引數、函式返回型別、成員函式本體。
(2)編譯器強制實施bitwise constness,但你編寫程式時應該使用“概念上的常量性”
(3)當const和non-const成員函式有著實質等價的實現時,令non-const 版本呼叫const版本
可避免程式碼重複。