# [18] const正確性
## FAQs in section [18]:
* [18.1] 什么是“const正確性”?
* [18.2] “const正確性”是如何與普通的類型安全有何聯系?
* [18.3] 我應該“盡早”還是“推遲”確定const正確性?
* [18.4] “const Fred* p”是什么意思?
* [18.5] “const Fred* p”、“Fred* const p”和“const Fred* const p”有什么不同?
* [18.6] “const Fred& x”是什么意思?
* [18.7] “Fred& const x”有意義嗎?
* [18.8] “Fred const& x”是什么意思?
* [18.9] “Fred const* x”是什么意思?
* [18.10] 什么是“const成員函數”?
* [18.11] 返回引用的成員函數和const成員函數之間有什么聯系?
* [18.12] “const重載”是做什么用的?
* [18.13] 如果我想讓一個const成員函數對數據成員做“不可見”的修改,應該怎么辦?
* [18.14] const_cast會導致無法優化么?
* [18.15] 當我用const int*指向一個int后,為什么編譯器還允許我修改這個int?
* [18.16] “const Fred* p”的意思是*p不會改變么?
* [18.17] 當把Foo**轉換成const Foo**時為什么會出錯?
## 18.1 什么是“`const`正確性”?
這是個好東西。意思是用`const`關鍵字來阻止`const`對象被修改。
例如,如果你要編寫一個函數`f()`,它接收`std::string`類型的參數,并且想要對調用者保證不會修改調用者傳過來的`std::string`參數,可以按以下方法聲明`f()`
* `void f1(const std::string& s); ????//傳const引用`
* `void f2(const std::string* sptr); ?//傳const指針`
* `void f3(std::string s); ???????????//傳值`
在_傳`const`引用_和_傳`const`指針_的情況下,任何試圖在`f()`內部修改`std::string`的行為都會在編譯時被編譯器標記為錯誤。這完全是在編譯時做的,所以使用`const`沒有運行時的空間或速度損失。在_傳值_時(`f3()`),被調用函數獲得了調用者`std::string`的一份拷貝。也就是說,`f3()`可以修改這個拷貝,但返回時這個拷貝會被銷毀。尤其是`f3()`無法修改調用者的`std::string`對象。
舉個反例,如果想要編寫一個函數`g()`,也是接收`std::string`,但想要告知調用者g()有可能會修改調用者的`std::string`對象。這時,可以按以下方法聲明`g()`:
* `void g1(std::string& s); ??????//傳非const引用`
* `void g2(std::string* sptr); ???//傳非const指針`
在這些函數中省去`const`,就是告訴編譯器允許(但不強制)它們修改調用者的`std::string`對象。因此,這些`g()`函數可以把它們的`std::string`傳遞給任何`f()`函數,但只有`f3()`(通過傳值接收參數)能夠將其參數傳遞給`g1()`或`g2()`。如果`f1()`或`f2()`需要調用`g()`函數,必須給`g()`傳遞一份`std::string`的本地拷貝。`f1()`或`f2()`的參數不能直接傳遞給`g()`函數。例如:
```
?void?g1(std::string&?s);
?void?f1(const?std::string&?s)
?{
???g1(s);??????????//?編譯錯誤,因為s是const的
???std::string?localCopy?=?s;
???g1(localCopy);??//?正確,因為localCopy不是const的
?}
```
當然,在上面的例子中,任何`g1()`所做的修改都會反映到`f1()`函數內的`localCopy`對象。特別是,通過`const`引用傳遞給`f1()`的參數不會被修改。
## 18.2 “`const`正確性”是如何與普通的類型安全有何聯系?
將參數聲明為`const`正是另外一種形式的類型安全。這就好像`const std::string`是與`std::string`不同的類一樣。因為`const`變量缺少一些非`const`變量所具有的一些變更性操作(例如,可以想象以下,`const std::string`沒有賦值操作符)。
如果你發現普通的類型安全有助于構建正確的系統(的確有幫助,尤其是對于大型系統來說),你會發現`const`正確性也有幫助。
## 18.3 我應該“盡早”還是“推遲”確定`const`正確性?
應該在最最最開始。
事后保證`const`正確性會導致一種滾雪球效應:每次你在一個地方添加了`const`會需要在四個更多的地方也添加`const`。
## 18.4 “`const Fred* p`”是什么意思?
意思是`p`是一個指向`Fred`類的指針,但不能通過`p`來修改`Fred`對象(當然`p`也可以是`NULL`指針)。
例如,假設`Fred`類有一個叫做`inspect()`的`const`成員函數,那么寫`p->inspect()`是可以的。但如果`Fred`類有一個非`const`成員函數`mutate()`,那么寫`p->mutate()`就是個錯誤(編譯器會捕獲這種錯誤;不會在運行時測試;因此`const`不會降低運行速度)。
## 18.5 “`const Fred* p`”、“`Fred* const p`”和“`const Fred* const p`”有什么不同?
應該從右往左讀指針聲明。
* `const Fred* p`表明`p`指向一個`const`的`Fred`對象——`Fred`對象不能通過`p`修改。
* `Fred* const p`表明`p`是一個指向`Fred`對象的`const`指針——可以通過`p`修改`Fred`對象,但不能修改`p`本身。
* `cosnt Fred* const p`表明“`p`是一個指向`const Fred`對象的`const`指針”——不能修改`p`,也不能通過`p`修改`Fred`對象。
## 18.6 “`const Fred& x`”是什么意思?
意思是`x`是`Fred`對象的一個別名,但不能通過`x`來修改`Fred`對象。
例如,假設`Fred`類有一個叫做`inspect()`的`const`成員函數,那么寫`x.inspect()`是可以的。但如果`Fred`類有一個非`const`成員函數`mutate()`,那么寫`x.mutate()`就是個錯誤(編譯器會捕獲這種錯誤;不會在運行時檢查;因此`const`不會降低運行速度)。
## 18.7 “`Fred& const x`”有意義嗎?
沒意義。
為了理解這個聲明,需要從右往左讀這個聲明。因此“`Fred& const x`”的意思是“`x`是一個指向`Fred`的`const`引用”。但這是多余的,因為引用本來就是`const`的。你不能重新綁定一個引用。不管有沒有`const`,都不行。
換句話說,“`Fred& cosnt x`”在功能上與“`Fred& x`”是一樣的。因為在`&`后面加上`const`沒什么用,因此為了避免迷惑就不應該多此一舉。有人可能會認為這里加上`const`后指向的`Fred`對象就是`const`的了,就好像“`const Fred& x`”一樣。
## 18.8 “`Fred const& x`”是什么意思?
“`Fred cosnt& x`”在功能上與`const Fred& x`相同。然而,真正的問題是_應該_用哪一種。
答案:絕_沒有任何人_能夠為你所在的機構做決定,除非他們了解你的機構。沒有放之四海而皆準的規則。沒有對所有機構都“正確”的答案。所以不要讓_任何人_做倉促的選擇。“思考(Think)”并非一個四字母的單詞。
例如,一些機構看重的是一致性,并且已經有大量代碼使用“`const Fred&`”了。對于他們來說,不管是否有優點,“`Fred const&`”都不是個好選擇。還有很多其它的商業環境,一些傾向于“`Fred const&`”,其它則傾向于“`const Fred&`”。
采用適合你機構中_普通維護程序員_的寫法。不是專家,不是傻瓜,而是維護代碼的普通程序員。除非你決定解雇他們并雇傭新人,否則就要確保_他們_能夠理解你的代碼。根據實際情況做商業決定,而不是根據其它什么人的假設。
使用“`Fred const&`”需要克服一些慣性。現在大多數C++書籍都使用`const Fred&`,大多數程序員學C++時接觸的就是這種語法,并且仍然這么用。這并非是說`const Fred&`一定對你的機構好。但在更改(這種風格)期間,和/或在招收新人時,的確可能會引起一些混亂。一些機構認為用`Fred const&`帶來的好處更大,其它機構則不這么認為。
另一個警告:如果決定用`Fred const&`,確保采取措施使人們不會誤寫成沒意義的“`Fred& const x`”。
## 18.9 “`Fred const* x`”是什么意思?
“`Fred cosnt* x`”在功能上與`const Fred* x`相同。然而,真正的問題是_應該_用哪一種。
答案:絕_沒有任何人_能夠為你所在的機構做決定,除非他們了解你的機構。沒有放之四海而皆準的規則。沒有對所有機構都“正確”的答案。所以不要讓_任何人_做倉促的選擇。“思考(Think)”并非一個四字母的單詞。
例如,一些機構看重的是一致性,并且已經有大量代碼使用“`const Fred*`”了。對于他們來說,不管是否有優點,“`Fred const*`”都不是個好選擇。還有很多其它的商業環境,一些傾向于“`Fred const*`”,其它則傾向于“`const Fred*`”。
采用適合你機構中_普通維護程序員_的寫法。不是專家,不是傻瓜,而是維護代碼的普通程序員。除非你決定解雇他們并雇傭新人,否則就要確保_他們_能夠理解你的代碼。根據實際情況做商業決定,而不是根據其它什么人的假設。
使用“`Fred const*`”需要克服一些慣性。現在大多數C++書籍都使用`const Fred*`,大多數程序員學C++時接觸的就是這種語法,并且仍然這么用。這并非是說`const Fred*`一定對你的機構好。但在更改(這種風格)期間,和/或在招收新人時,的確可能會引起一些混亂。一些機構認為用`Fred const*`帶來的好處更大,其它機構則不這么認為。
另一個警告:如果決定用`Fred const*`,確保采取措施使人們不會誤寫成語義不同但語法相似的“`Fred* const x`”。這兩者雖然第一眼看上去非常相似,但含義完全不同。
## 18.10 什么是“`const`成員函數”?
是指僅查看(而不改變)對象的成員函數。
`const`成員函數會在緊跟函數參數列表的后面跟一個`const`關鍵字。有`const`后綴的成員函數被稱作“`const`成員函數”或者是“查看函數”(inspector)。沒有`const`后綴的成員函數被稱作“非`const`函數”或“變更函數”(mutator)。
```
?class?Fred?{
?public:
???void?inspect()?const;???//?該成員保證不修改*this
???void?mutate();??????????//?該成員可能會修改*this
?};
?void?userCode(Fred&?changeable,?const?Fred&?unchangeable)
?{
???changeable.inspect();???//?正確:沒有修改一個可修改對象
???changeable.mutate();????//?正確:修改一個可修改對象
???unchangeable.inspect();?//?正確:沒有修改一個不可修改對象。
???unchangeable.mutate();??//?錯誤:試圖修改一個不可修改對象。
?}
```
`unchangeable.mutate()`這個錯誤在編譯期被發現。`const`不會有運行時的時空效率損失。
在`inspect()`成員函數后面的`const`后綴表示不會改變對象的(調用方可見的)_抽象_狀態。這并非保證不改變對象的“底層二進制位”。C++編譯器不允許將其解釋為“按位”(不變),除非能解決別名問題,而別名問題一般無法解決(即可能存在會修改對象狀態的非`const`別名)。另外一個對這種別名問題的(重要)認識是:用一根“指向`const`對象的指針”并不能保證對象不改變,它只是保證對象不會_通過該指針_被改變。
## 18.11 返回引用的成員函數和`const`成員函數之間有什么聯系?
如果想要從一個查看函數中返回`this`對象的引用,那么應該返回指向`cosnst`對象的引用,即`const X&`。
```
?class?Person?{
?public:
???const?std::string&?name_good()?const;??←?正確:調用者不能修改name
???std::string&?name_evil()?const;????????←?錯誤:調用者能夠修改name...
?};
?void?myCode(const?Person&?p)??←?這里保證不會修改Person?對象...
?{
???p.name_evil()?=?"Igor";?????←?...但還是修改了!!
?}
```
好消息是當你犯這種錯誤時,編譯器_通常_能夠發現。尤其是如果不小心返回了`this`對象的非`const`引用,例如上面的`Person::name_evil()`,編譯器在編譯這個成員函數時,_通常_能夠發現并給出一條編譯錯誤。
壞消息是編譯器并不能發現所有這種錯誤:在一些情況下編譯器無法產生一條錯誤消息。
最后:你需要思考,并記住本FAQ所述的原則。如果你通過引用返回的對象在_邏輯上_是`this`對象的一部分,而不管其是否在物理上放在了`this` 對象內,那么`const`方法應該返回`const`引用或直接按值返回。(`this`對象的“邏輯”部分與對象的“抽象狀態”相關。請參閱前一個FAQ。)
## 18.12 “`const`重載”是做什么用的?
當一個查看函數和一個變更函數名字相同,且參數個數與類型也相同時就有用了——即兩者的不同之處僅在于一個有`const`另一個沒有`const`。
`const`重載的一個常見應用是下標運算符。通常應該盡量使用標準模板容器,例如`std::vector`,但有時會需要在自己的類中支持下標運算符。一個經驗法則是:**下標運算符通常成對出現**。
```
?class?Fred?{?...?};
?class?MyFredList?{
?public:
???const?Fred&?operator[]?(unsigned?index)?const;??←下標運算符通常成對出現
???Fred&???????operator[]?(unsigned?index);????????←下標運算符通常成對出現...
?};
```
當對一個非`const`的`MyFredList`對象使用下標運算符時,編譯器會調用非`const`的下表運算符。因為返回的是一個普通`Fred&`,所以能夠查看或修改對應的`Fred`對象。例如,假設`Fred`類有一個查看函數`Fred::inspect() const`和一個變更函數`Fred::mutate()`:
```
?void?f(MyFredList&?a)??←?MyFredList是非const的
?{
???//?可以調用不修改a[3]處Fred對象的方法:
???Fred?x?=?a[3];
???a[3].inspect();
???//?可以調用修改a[3]處Fred對象的方法:
???Fred?y;
???a[3]?=?y;
???a[3].mutate();
?}
```
但是,當對一個`const`的`MyFredList`對象使用下標運算符時,編譯器會調用`const`的下標運算符。因為會返回`const Fred&`,所以可以查看對應的`Fred`對象而不能修改它。
```
?void?f(const?MyFredList&?a)??←?MyFredList是const的
?{
???//?可以調用不修改a[3]處Fred對象的方法:
???Fred?x?=?a[3];
???a[3].inspect();
???//?錯誤(很幸運!):試圖改變a[3]出的Fred對象:
???Fred?y;
???a[3]?=?y;???????←?幸運的是編譯器在編譯時發現了這個錯誤。
???a[3].mutate();??←?幸運的是編譯器在編譯時發現了這個錯誤。
?}
```
在以下FAQ中演示了針對下標運算符和函數調用運算符的`const`重載:[13.10], [16.17], [16.18], [16.19]和[35.2]
當然除了下標運算符,其它函數也可以進行`const`重載。
## 18.13 如果我想讓一個`const`成員函數對數據成員做“不可見”的修改,應該怎么辦?
用`mutable`(或者實在沒辦法了,用最后一招`const_cast`)
少數查看函數需要對數據成員做適當的修改(例如,一個`Set`對象可能想要緩存上一次查看的內容,以便下一次查看時能夠提高性能)。這里“適當”的意思是,所做的修改不會從對象的接口上反映到外部(否則該成員函數就應該是一個變更函數,而不是查看函數了)。
這時,需要修改的數據成員應標記為`mutable`(把`mutable`關鍵字放在數據成員的聲明前;即和`const`的位置一樣)。這就通知編譯器說這個數據成員允許在`const`成員函數中被修改。如果編譯器不支持`mutable`關鍵字,那么可以通過`const_cast`去除掉`this`的`const`(但是記著讀下面的**注意事項**)。例如在`Set::lookup() const`中,可以這么寫:
```
?Set*?self?=?const_cast<Set*>(this);
???//?在這么做之前,記著讀下面的**注意事項**
```
然后,`self`和`this`內容一樣(即`self == this`為真),但`self`類型是`Set*`而不是`const Set*`(技術上來講,是`const Set* const`,不過最右邊的`const`與這里的問題無關)。因此可以使用`self`來修改`this`所指向的對象。
**注意:**`const_cast`可能會導致一種非常罕見的錯誤。這個錯誤僅在三件很少見的事情同時發生時出現:數據成員本應該是`mutable`(例如上面所說的),編譯器不支持`mutable`,并且對象原本就定義為`const`(不是通過一根指向`const`對象的指針來訪問的普通`const`對象)。雖然這種組合非常罕見,甚至永遠不會發生,但如果真的發生了,那么這種代碼可能就不能正常運行(標準說這種行為是未定義的)。
如果想要用`const_cast`,那么應該用`mutable`替代。換句話說,如果需要修改一個對象的成員,而又是通過指向`const`對象的指針來訪問這個對象,那么最安全和最簡單的做法就是給該成員的聲明前加上`mutable`。如果你確信實際對象不是`const`的(例如能夠確定對象是像這樣聲明的:`Set s;`),那么也可以用`const_cast`。但如果對象本身就是`const`的(例如可能聲明為:`const Set s;`),那么就應該用`mutable`而不是`const_cast`。
請不要告訴我說Y編譯器的X版本在Z機器上允許修改`const`對象的非`mutable`成員。我不管這個——根據標準這是錯誤的,如果換一個編譯器,甚至是同一編譯器的不同版本(升級版),你的代碼就可能會失敗。不要這么做。用`mutable`吧。
## 18.14 `const_cast`會導致無法優化么?
在理論上是的;在實際中不會。
即使語言本身禁止了`const_cast`,要想在調用`const`成員函數時避免讀寫寄存器的唯一辦法是解決別名問題(即要證明沒有其它指向該對象的非`const`指針)。這只有在極少數情況下才能辦到(當在調用`const`成員函數時構造對象,所有在構造對象和調用`const`成員函數之間調用的非`const`成員函數是靜態綁定的,并且所有這些調用包括構造函數都是內聯的,同時構造函數調用的任何成員函數也要是內聯的)。
## 18.15 當我用`const int*`指向一個`int`后,為什么編譯器還允許我修改這個`int`?
因為“`const int* p`”意思是“`p`保證不會修改`*p`”,而_不是說_“`*p`保證不變”。
用`const int*`指向一個`int`,不會使這個`int`變為`const`。`int`不會通過`const int*`被修改,但如果有另外一個`int*`(注意沒有`const`)指向該`int`(這就是“別名”),那么這個`int*`指針可以用來修改`int`。例如:
```
?void?f(const?int*?p1,?int*?p2)
?{
???int?i?=?*p1;?????????//?獲得*p1的(原始)值*p1
???*p2?=?7;?????????????//?如果p1 == p2,這就會修改*p1。
???int?j?=?*p1;?????????//?獲取*p1(可能是更新后)的值。
???if?(i?!=?j)?{
?????std::cout?<<?"*p1?changed,?but?it?didn't?change?via?pointer?p1!\n";
?????assert(p1?==?p2);??//?這是*p1可能會變化的唯一辦法。
???}
?}
?int?main()
?{
???int?x?=?5;
???f(&x,?&x);???????????//?這完全合法(甚至是合情理的!)...
?}
```
注意`main()`和`f(const int*, int*)`可能是在不同的編譯單元中,并且不是在同一天編譯的。因此,編譯器就無法在編譯時發現別名。因此無法在語言中禁止這種事情。實際上,我們甚至不想添加這樣一個規則。因為一般來說,允許很多指針指向同一個對象,這是一個功能。當指針保證說不去修改所指內容時,這只是該_指針_作出的保證,而_不是_內容所做的保證。
## 18.16 “`const Fred* p`”的意思是`*p`不會改變么?
不是!(這個與int指針的別名問題相關)
“`const Fred* p`”意思是不能通過指針`p`來修改`Fred`,但有可能不經過`const`(例如一個非`const`指針`Fred*`),而是通過其它途徑來訪問`object`。例如,如果有兩根指針“`const Fred* p`”和“`Fred* q`”都指向同一個`Fred`對象(別名),那么指針`q`可以用來修改`Fred`對象,但指針`p`不能。
```
?class?Fred?{
?public:
???void?inspect()?const;???//?const成員函數
???void?mutate();??????????//?非const成員函數
?};
?int?main()
?{
???Fred?f;
???const?Fred*?p?=?&f;
?????????Fred*?q?=?&f;
???p->inspect();????//?可以:不改變*p_
???p->mutate();?????//?錯誤:不能通過p來修改*p
???q->inspect();????//?可以:允許使用q來查看對象
???q->mutate();?????//?可以:允許使用q來修改對象
???f.inspect();?????//?可以:允許使用f來查看對象
???f.mutate();??????_//?可以:允許使用f來修改對象...
?
```
## 18.17 當把`Foo**`轉換成`const Foo**`時為什么會出錯?
因為把`Foo**`轉換成`const Foo**`是非法且危險的。
C++允許`Foo*`到`const Foo*`的轉換(這是安全的)。但如果想要將`Foo**`隱式轉換成`const Foo**`則會報錯。
這么做的原因如下所示。但首先,這里有個最普通的解決辦法:只要把`const Foo**`改成`const Foo* const*`就可以了。
```
?class?Foo?{?/*?...?*/?};
?void?f(const?Foo**?p);
?void?g(const?Foo*?const*?p);
?int?main()
?{
???Foo**?p?=?/*...*/;
???...
???f(p);??//?錯誤:將Foo**轉換成const Foo**是非法且邪惡的
???g(p);??//?可以:將Foo**轉換成const Foo* const*是合法且合理的...
?}
```
之所以`Foo**`到`const Foo**`的轉換是危險的,是因為這會使你沒有經過轉換就在不經意間修改了`const Foo`對象。
```
?class?Foo?{
?public:
???void?modify();??//?修改this對象
?};
?int?main()
?{
???const?Foo?x;
???Foo*?p;
???const?Foo**?q?=?&p;??//?這時q指向p;(幸虧)這是個錯誤。
???*q?=?&x;?????????????//?這時p指向x
???p->modify();?????????//?啊:修改了const Foo!!...
?}
```
記住:_請不要_用指針轉換繞過這里。別這么做就是了!
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 &lt;iostream&gt; 和 &lt;cstdio&gt;輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫