條款03:盡可能使用const
Use const whenever possible.
const允許你告訴編譯器和其他程序員某值應該保持不變。
如果關鍵字const出現在星號左邊,表示被指物是常量;如果出現在星號右邊,表示指針自身是常量;如果出現在星號兩邊,表示被指物和指針兩者都是常量。
1. char greeting[] = "Hello";
2. char* p = greeting; //non-const pointer, non-const data
3. const char* p = greeting; //non-const pointer, const data
4. char* const p = greeting; //const pointer, non-const data
5. const char* const p = greeting; //const pointer, const data
如果被指物是常量,既可以關鍵字const寫在類型之前,又可以把它寫在類型之後、星號之前。兩種寫法的意義等價:
1. void f1(const Widget* pw); //f1獲得一個指針,指向一個常量Widget對象..
2. void f2(Widget const * pw); //f2也是
STL迭代器系以指針為底層塑模出來,所以迭代器的作用就像個T*指針。聲明迭代器為const就像聲明指針為const一樣(即聲明一個T* const 指針),表示這個迭代器不得指向不同的東西,但它所指的東西的值是可以改動的。如果希望迭代器所指的東西(數據)不可被改動(即希望STL模擬一個const T* 指針),則用const_iterator:
1. std::vector<int> vec;
2. ...
3. const std::vector<int>::iterator iter = vec.begin( ); //T* const
4. *iter = 10; //沒問題,改變iter所指物
5. ++iter; //錯誤!iter是const
6. std::vector<int>::const_iterator cIter = vec.begin( );// const T*
7. *cIter = 10; //錯誤! *cIter是const
8. ++cIter; //沒問題,改變cIter。
令函數返回一個常量值,往往可以降低因客戶錯誤而造成的意外,而又不至於放棄安全性和高效性。
1. class Rational { ... };
2. const Rational operator* (const Rational& lhs,const Rational& rhs);
為什麼返回一個const對象?原因是如果不這樣客戶就能實現這樣的行為:
1. Rational a, b, c;
2. ...
3. (a * b) = c; //在a * b的成果上調用operator=
如果a和b都是內置類型,這樣的代碼直截了當就是不合法。而一個"良好的用戶自定義類型"的特征是它們避免無端地與內置類型不兼容。將operator* 的回傳值聲明為const可以預防那個荒唐的賦值動作。
const成員函數
將const實施於成員函數的目的,是為了確認該成員函數可作用於const對象身上。這類成員函數可以得知哪個函數可以改動對象內容而哪個函數不行,很是重要。
兩個成員函數如果只是常量性不同,可以被重載。這實在是一個重要的C++特性(前幾天的面試剛碰到過):
1. class TextBlock {
2. public:
3. ...
4. const char& operator[](std::size_t position) const
5. { return text[position]; } // operator[] for const對象.
6. char& operator[](std::size_t position)
7. { return text[position]; } // operator[] for non-const對象.
8. private:
9. std::string text;
10. };
11.
12. TextBlock tb("Hello");
13. std::cout << tb[0]; //調用non-const TextBlock::operator[]
14. const TextBlock ctb("World");
15. std::cout << ctb[0]; //調用const TextBlock::operator[]
只要重載operator[]並對不同的版本給予不同的返回類型,就可以令const和non-const TextBlocks獲得不同的處理:
1. std::cout << tb[0];//沒問題 - 讀一個non-const TextBlock
2. tb[0] = 'x'; //沒問題 - 寫一個non-const TextBlock
3. std::cout << ctb[0];//沒問題 - 讀一個const TextBlock
4. ctb[0] = 'x'; //錯誤! - 寫一個const TextBlock
上述錯誤只因operator[] 的返回類型以致,至於operator[] 調用動作自身沒問題。
請注意,non-const operator[] 的返回類型是個reference tochar,不是char。如果operator[]只是返回一個char,下面這樣的句子就無法通過編譯:
1. tb[0] = 'x';
返回類型是內置類型的函數,改動函數返回值不合法。縱使合法,C++以值傳遞意味被改動的其實是tb.text[0]的一個副本,不是tb.text[0]自身。
l 將某些東西聲明為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體。
成員函數如果是const意味什麼?
bitwise const:成員函數只有在不更改對象之任何成員變量(static除外)時才可以說是const,也就是說它不更改對象內的任何一個bit。bitwise constness正是C++ 對常量性的定義,因此const成員函數不可以更改對象內任何non-static成員變量。
不幸的是許多成員函數雖然不十足具備const性質卻能通過bitwise測試:
1. class CTextBlock {
2. public:
3. ...
4. char& operator[](std::size_t position) const // bitwise const聲明,
5. { return pText[position]; } // 但其實不適當.跟之前相比少了一個const!
6. private:
7. char* pText;
8. };
operator[]實現代碼並不更改私有變量pText,於是編譯器為operator[]產出目標碼,並認定它是bitwiseconst。
1. const CTextBlock cctb("Hello");//聲明一個常量對象。
2. char* pc = &cctb[0];//調用const operator[]取得一個指針, 指向cctb的數據。
3. *pc = 'J'; //cctb現在有了 "Jello" 這樣的內容。
以上代碼沒有任何錯誤:創建一個常量對象並設以某值,而且只對它調用const成員函數。但終究還是改變了它的值。
這種情況導出所謂的logical constness:一個const成員函數可以修改它所處理的對象內的某些bits,但只有在客戶端偵測不出的情況下才得如此,也就是自己可以改用戶不能改。
1. class CTextBlock {
2. public:
3. ...
4. std::size_t length() const;
5. private:
6. char* pText;
7. std::size_t textLength; //最近一次計算的文本區塊長度。
8. bool lengthIsValid; //目前的長度是否有效。
9. };
10. std::size_t CTextBlock::length() const
11. {
12. if (!lengthIsValid) {
13. textLength = std::strlen(pText);
14. //錯誤!在const成員函數內不能對私有變量textLength進行修改
15. lengthIsValid = true;
16. //錯誤!在const成員函數內不能對私有變量lengthIsValid進行修改
17. }
18. return textLength; }
解決辦法很簡單:利用C++ 中的mutable(可變的)修飾符,mutable釋放掉non-static成員變量的bitwiseconstness約束:
1. class CTextBlock {
2. public:
3. ...
4. std::size_t length() const;
5. private:
6. char* pText;
7. mutable std::size_t textLength; //這些成員變量可能總是會被更改,
8. mutable bool lengthIsValid; //即使在const成員函數內。
9. }; //現在,剛才的length函數就可以了~
l 編譯器強制實施bitwise constness,但你編寫程序時應該使用"概念上的常量性"(conceptual constness)。
在const和non-const成員函數中避免重復
對於"bitwise-constness非我所欲"的問題,mutable是個解決辦法,但它不能解決所有的const相關難題。舉個例子,假設TextBlock(和 CTextBlock)內的operator[] 不單只是返回一個reference指向某字符,也執行邊界檢驗(boundschecking)、志記訪問信息(loggedaccess info.)、甚至可能進行數據完善性檢驗。把所有這些同時放進const和non-const operator[] 中,導致兩個版本的operator[]及大量的代碼重復。
真正該做的是實現operator[]的機能一次並使用它兩次,也就是說,令其中一個調用另一個。本例中constoperator[]完全做掉了non-const版本該做的一切,唯一的不同是其返回類型多了一個const資格修飾。
1. class TextBlock {
2. public:
3. ...
4. const char& operator[](std::size_t position) const //一如既往
5. {
6. ...
7. return text[position];
8. }
9. char& operator[](std::size_t position) //現在只調用const op[]
10. {
11. return
12. const_cast<char&>( //將op[]返回值的const轉除
13. static_cast<const TextBlock&>(*this)//為*this加上const
14. [position] //調用const op[]
15. );
16. }
17. ...
18. };
這裡共有兩次轉型:第一次用來為 *this添加const(這使接下來調用operator[]時得以調用const版本),第二次則是從constoperator[]的返回值中移除const。
添加const的那一次轉型強迫進行了一次安全轉型(將non-const對象轉為const對象),所以我們使用static_cast。移除const的那個動作只可以藉由const_cast完成,沒有其他選擇。
簡單來說就是non-const版本為了調用const版本先轉換常量性,應用const版本功能完畢後,為了符合non-const的返回值,再去除常量性。
const成員函數承諾絕不改變其對象的邏輯狀態(logicalstate),non-const成員函數卻沒有這般承諾。如果在const函數內調用non-const函數,就是冒了這樣的風險:你曾經承諾不改動的那個對象被改動了。這就是為什麼"const成員函數調用non-const成員函數"是一種錯誤行為:因為對象有可能因此被改動。
l 當const和non-const成員函數有著實質等價的實現時,令non-const版本調用const版本可避免代碼重復。
摘自 pandawuwyj的專欄