C++中const用法

const限定符
有時我們希望定義這樣一種變量,它的值不能被改變。例如,用一個變量來表示緩衝區的大小。使用變量的好處是當我們覺得緩衝區大小不再合適時,很容易對其進行調整。另一方面,也應隨時警惕防止程序一不小心改變了這個值。爲了滿足這一要求,可以用關鍵字const對變量的類型加以限定:
const int bufSize = 512;//輸入緩衝區大小
這樣就把bufSize定義成了一個常量。任何試圖爲bufSize賦值的行爲都將引發錯誤:
bufSize = 512; //錯誤:試圖向const對象寫值
因爲const對象一旦創建後其指就不能再改變,所以const對象必須初始化。一如既往,初始值可以是任意複雜的表達式:
const int i = get_size(); //正確:運行時初始化
const int j = 42;//正確:編譯時初始化
const int k; //錯誤

初始化和const
正如之前反覆提到的,對象的類型決定了其上的操作。與非const類型所能參與的操作相比,const類型的對象能完成其中大部分,但也不是所有的操作都適合。主要的限制就是只能在const類型的對象上執行不改變其內容的操作。例如,const int 和普通的int一樣都能參與算術運算,也都能轉換成一個布爾值,等等。
在不改變const對象得操作中還有一種是初始化,如果利用一個對象去初始化另外一個對象,則它們是不是const都無關緊要:
int i = 42;
const int ci = i;
int j = ci;
儘管ci是整型常量,但無論如何ci中的值還是一個整形數。ci的常量特徵僅僅在執行改變ci的操作時纔會發揮作用。當用ci去初始化j時,根本無須在意ci是不是一個常量。拷貝一個對象的值並不會改變它,一旦拷貝完成,新的對象就和原來的對象沒什麼關係了。

默認狀態下,const對象僅在文件內有效
當以編譯時初始化的方式定義一個const對象時,就如對bufSize的定義一樣:
const int bufSize = 512;
編譯器將在編譯過程中把用到該變量的地方都替換成對應的值。也就是說,編譯器會找到代碼中所有用到bufSize的地方,然後用512替換。
爲了執行上述替換,編譯器必須知道變量的初始值。如果程序包含多個文件,則每個用了const對象的文件都必須得能訪問他的初始值才行。要做到這一點,就必須在每一個用到變量的文件中都有對他的定義。爲了支持這一用法,同時避免對同一變量的重複定義,默認情況下,const對象被設定爲僅在文件內有效。當多個文件中出現了同名的const變量時,其實等同於在不同文件中分別定義了獨立的變量。
某些時候有這樣一種const變量,它的初始值不是一個常量表達式,但又確實有必要在文件間共享。這種情況下,我們不希望編譯器爲每個文件分別生成獨立的變量。相反,我們想讓這類const變量像其他(非常量)對象一樣工作,也就是說,只在一個文件中定義const,而在其他多個文件中聲明並使用它。
解決的辦法是,對於const變量不管是聲明還是定義都添加extern關鍵字,這樣只需定義一次就可以了:
//file_1.cc定義並初始化了一個常量,該常量能被其他文件訪問
extern const int bufSize = fcn();
//file_1.h頭文件
extern const int bufSize;//與file_1.cc中定義的bufSize是同一個
如上述程序所示,file_1.cc定義並初始化了bufSize。因爲bufSize是一個常量,必須用extern加以限定使其被其他文件使用。
file_1.h頭文件中的聲明也由extern做了限定,起作用是指明bufSize並非本文件所獨有,他的定義將在別處出現。

const的引用
可以把引用綁定到const對象上,就像綁定到其他對象上一樣,我們稱之爲對常量的引用(reference to const)。與普通引用不同的是,對常量的引用不能被用作修改它所綁定的對象:
const int ci = 1024;
const int &r1 = ci;//正確:引用及其對應的對象都是常量
r1 =42;//錯誤:r1是對常量的引用
int &r2 = ci;//錯誤:試圖讓一個非常量引用指向一個常量對象
因爲不允許直接爲ci賦值,當然也就不能通過引用去改變ci。因此,對r2的初始化是錯誤的。

初始化和對const的引用
引用的類型必須與其引用對象的類型一致,但是有兩個例外。第一種例外情況就是在初始化常量引用時允許用任意表達式作爲初始值,只要該表達式的結果能轉換成引用的類型即可。尤其,允許爲一個常量引用綁定非常量的對象、字面值,甚至是個一般表達式:
int i = 42;
const int &r1 = i;//允許將const int&綁定到一個普通int對象上
const int &r2 = 42;//正確:r1是一個常量引用
const int &r3 = r1 * 2;//正確:r3是一個常量引用
int &r4 = r1 * 2;//錯誤:r4是一個普通的非常量引用
要想理解這種例外情況的原因,最簡單的辦法是弄清楚當一個常量引用被綁定到另外一種類型上時到底發生了什麼:
double dval = 3.14;
const int &ri = dval;
此處ri引用了一個int型的數。對ri的操作應該是整數運算,但dval卻是一個雙精度浮點數而非整數。因此未了確保讓ri綁定一個整數,編譯器把上述代碼變成了如下形式:
const int temp = dval;// 由雙精度浮點數生成一個臨時的整型常量
const int &ri = temp;//讓ri綁定這個臨時量
在這種情況下,ri綁定了一個臨時量(temporary)對象。所謂臨時量對象就是當編譯器需要一個空間來暫存表達式的求值結果時臨時創建的一個未命名的對象。C++程序員們常常把臨時量對象簡稱爲臨時量。
接下來探討當ri不是常量時,如果執行了類似於上面的初始化過程將帶來什麼樣的後果,如果ri不是常量,就允許對ri賦值,這樣就會改變ri所引用對象的值。注意,此時綁定的對象是一個臨時量而非dval,就肯定想通過ri改變dval的值,否則幹什麼要給ri賦值呢?

對const的引用可能引用一個並非const的對象
必須認識到,常量引用僅對引用可參與的操作做出了限定,對於引用的對象本身是不是一個常量未作限定。因爲對象也可能是個非常量,所以允許通過其他途徑改變它的值:
int i = 42;
int &r1 = i;//引用ri綁定對象i
const int &r2 = i;//r2也綁定對象i,但是不允許通過r2修改i的值
r1 = 0;//r1並非常量,i的值修改爲0
r2 = 0;//錯誤:r2是一個常量引用
r2綁定(非常量)整數i是合法的行爲。然而,不允許通過r2修改i的值。儘管如此,i的值仍然允許通過其他途徑修改,既可以直接給i賦值,也可以通過像r1一樣綁定到i的其他引用來修改。

指針和const
與引用一樣,也可以令指針指向常量或非常量。類似於常量引用,指向常量的指針不能用於改變其所指對象的值。要想存放常量對象的地址,只能使用指向常量的指針:
const double pi = 3.14;//pi是個常量,它的值不能改變
double *ptr = π//錯誤:ptr是一個普通指針
const double *cptr = π//cptr可以指向一個雙精度常量
cptr = 42; //錯誤:不能給cptr賦值
指針的類型必須與其所致對象的類型一致,但是有兩個例外。第一種例外情況是允許令一個指向常量的指針指向一個非常量對象:
double dval = 3.14;//dval是一個雙精度浮點數,它的值可以改變
cptr = &dval; //正確:但是不能通過cptr改變dval的值
和常量引用一樣,指向常量的指針也沒有規定其所指的對象必須是一個常量。所謂指向常量的指針僅僅要求不能通過該指針改變對象的值,而沒有規定那個對象的值不能通過其他途徑改變。

const指針
指針是對象而引用不是,因此就像其他對象類型一樣,允許把指針本身定位常量。常量指針(const pointer)必須初始化,而且一旦初始化完成,則它的值(也就是存放指針中放入那個地址)就不能再改變了。把*放在const關鍵字之前以說明指針是一個常量,這樣的書寫形式隱含着一層意味,即不變的是指針本身的值而非指向的那個值:
int errNumb = 0;
int *const curErr = &errNumb;//curErr將一直指向errNumb
const double pi = 3.14159;
const double const pip = π//pip是一個指向常量對象的常量指針
想要弄清楚這些聲明的含義最行之有效的辦法是從右向左閱讀。此例中,離curErr最近的符號是const,意味着curErr本身是一個常量對象,對象的類型由聲明符的其餘部分確定。聲明符中的下一個符號是
,意思是curErr是一個常量指針。最後,該聲明的基本數據類型部分確定了常量指針指向的是一個int 對象。與之相似我們也能推斷出,pip是一個常量指針,它指向的對象是一個雙精度浮點型常量。
指針本身是一個常量並不意味着不能通過指針修改其所指對象的值,能否這樣做完全依賴於所指對象的類型。例如,pip是一個指向常量的常量指針,則無論是pip所指的對象值還是pip自己存儲的那個地址都不能去改變。相反的,curErr指向的是一個一般的非常量整數,那麼久完全可以用curErr去修改errNumb的值:
*pip = 2.72; //錯誤:pip是一個指向常量的指針
//如果curErr所指的對象(也就是errNumb)的值不爲0
if (*curErr){
errorHandler();
*curErr = 0; //正確:把curErr所指的對象的值重置
}

頂層const
指針本身是一個對象,它又可以指向另外一個對象。因此,指針本身是不是常量以及指針所指的是不是常量就是兩個相互獨立的問題。用名詞頂層const(top-level const)表示指針本身是個常量,而用名詞底層const(low-level const)表示指針所指的對象是一個常量。
更一般的,頂層const可以表示任意的對象時常量,這一點對任何數據類型都適用,如算術類型、類、指針等。底層const則與指針和引用等複合類型的基本類型部分有關。比較特殊的是,指針類型既可以是頂層const也可以是底層const,這一點和其他類型相比區別明顯:
int i = 0;
int *const p1 = &i;//不能改變p1的值,這是一個頂層const
const int ci = 42;//不能改變ci的值,這是一個頂層const
const int *p2 = &ci;//允許改變p2的值,這是一個底層const
const int *const p3 = p2;// 靠右的const是頂層const,靠左的是底層const
const int &r = ci; //用於聲明引用的const都是底層const
當執行對象的拷貝操作時,常量是頂層const還是底層const區別明顯。其中,頂層const不受聲明影響:
i = ci; //正確:拷貝ci的值,ci是一個頂層const,對此操作無影響
p2 = p3; //正確:p2和p3指向的對象類型相同,p3頂層const的部分不影響
執行拷貝操作並不會改變被拷貝對象的值,因此,拷入和拷出的對象是否是常量都沒什麼影響。
另一方面,底層const的限制卻不能忽視。當執行對象的拷貝操作時,拷入和拷出的對象必須具有相同的底層const資格,或者兩個對象的數據類型必須能夠轉換。一般來說,非常量可以轉換成常量,反之則不行:
int *p = p3; //錯誤:p3包含底層const的定義,而p沒有
p2 = p3; //正確:p2和p3都是底層const
p2 = &i; //正確:int 能轉換成const int
int &r = ci; //錯誤
const int &r2 = i; //正確:const int&可以綁定到一個普通int上

constexpr和常量表達式
常量表達式(const expression)是指值不會改變並且在編譯過程就能得到計算結果的表達式。顯然,字面值屬於常量表達式,用常量表達式初始化的const對象也是常量表達式。後面就會提到,C++語言中有幾種情況下是要用到常量表達式的。
一個對象(或表達式)是不是常量表達式由它的數據類型和初始值共同決定,例如:
const int max_files =20; //max_files是常量表達式
const int limit = max_files + 1; //limit是常量表達式
int staff_size = 27; //staff_size不是常量表達式
const int sz = get_size(); //sz不是常量表達式
儘管staff_size的初始值是個字面值常量,但由於它的數據類型只是一個普通的int而非const int,所以它不屬於常量表達式。另一方面儘管sz本身是一個常量,但它的具體指知道運行時才能獲取到,所以也不是常量表達式。

constexpr變量
在一個複雜系統中,很難分辨一個初始值到底是不是常量表達式。當然可以定義一個const變量並把它的初始值設爲我們人爲地某個常量表達式,但在實際使用時,儘管要求如此卻常常發現初始值並非常量表達式的情況。可以這麼說,在此種情況,對象的定義和使用根本是兩回事兒。
C++11新標準規定,允許將變量聲明爲constexpr類型以便編譯器來驗證變量的值是否是一個常量表達式。聲明爲constexpr的變量一定是一個常量,而且必須用常量表達式初始化:
constexpr int mf = 20; //20是常量表達式
constexpr int limit = mf + 1; //mf + 1是常量表達式
constexpr int sz = size(); //只有當size是一個constexpr函數時纔是一條正確的聲明語句
儘管不能使用普通函數作爲constexpr變量的初始值,但是新標準允許定義一種特殊的constexpr函數。這種函數應該足夠簡單以使得編譯時就可以計算其結果,這樣就能用constexpr函數去初始化constexpr變量了。
(一般來說,如果你認定變量是一個常量表達式,那就把它聲明成constexpr類型)

字面值類型
常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因爲這些類型一般比較簡單,值也顯而易見、容易得到,就把它們成爲“字面值類型”(literal type)
到目前爲止接觸過的數據類型中,算術類型、引用和指針都屬於字面值類型。
儘管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲在某個固定地址中的對象。
函數體內定義的變量一般來說並非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義於所有函數體之外的對象其地址固定不變,能用來初始化constexpr指針。允許函數定義一類有效範圍超出函數本身的變量,這類變量和定義在函數體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr指針也能指向這樣的變量。

指針和constexpr
必須明確一點,在constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針所指的對象無關:
const int *p = nullptr;//p是一個指向整型常量的指針
constexpr int *q = nullptr; //q是一個指向整數的常量指針
p和q的類型相差甚遠,p是一個指向常量的指針,而q是一個常量指針,其中的關鍵在於constexpr把它定義的對象置爲了頂層const。
與其他常量指針,constexpr指針既可以指向常量也可以指向一個非常量:
constexpr int *np = nullptr; // np是一個指向整數的常量指針,其指爲空
int j = 0;
constexpr int i =42; //i的類型是整型常量
//i 和j都必須定義在函數體之外
constexpr const int *p = &i; //p是常量指針,指向整型常量i
constexpr int *p1 = &j; //p1是常量指針,指向整數j

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章