前景小故事
??最近重溫《C++ Primer》時被一位朋友看見。也許因為這位朋友太閑了策泣,他強行湊過來要與我討論一些零零散散的C++內容,其中就有這篇文章的主角——C++中的const關鍵字抬吟。
??起初我內心是拒絕的:const是什么值得討論的東西嗎萨咕?不過最后我還是被他的熱情打敗,一番交流后得出了下面的內容火本。
(希望有緣點進來看到這里的讀者可以花一點時間看正文內容并給筆者一些反饋任洞,這是筆者第一次發(fā)布文章,需要各位的建議讓以后的文章比現(xiàn)在的能看)
正題
??在C++中发侵,const絕大多數地出現(xiàn)是為了作為限制符限制變量的行為。被const修飾的變量稱為常量(總覺得這句話有語病)妆偏,最集中的一點體現(xiàn)是:常量需要在定義時初始化刃鳄,至于是否必須顯式初始化這個問題,筆者本機g++測試的結果是:內置類型(如int)在函數體外定義(此時內置類型會被默認初始化)會被編譯器報錯钱骂,而自定義的數據結構(擁有默認構造函數)則可以被編譯通過叔锐。這個問題筆者認為不必深究,在使用常量時都用顯式初始化即可见秽,因為實在是想像不到常量使用默認初始化的必要情況愉烙。
??最簡單的常量定義(以內置類型int為例)如下:
const int ivar = 777;
??定義后,ivar將在其生命周期內永遠地(如果沒有什么意外的話)作為777的代言解取,編譯器會檢測在ivar生命周期內對其所代表的內存進行非法改動的行為步责。試圖對它直接賦值的語句將會被報錯:
ivar = 888;
//error: cannot assign to variable 'ivar' with const-qualified type 'const int'
??對于const的用處,最常被擺上臺面的例子之一是:定義一個名為buf_size的常量用于表示需要定義的緩沖區(qū)大匈骺唷:
const size_t buf_size = 1024;
/* 其他代碼 */
//緩沖區(qū)定義
char *buf[buf_size] = {0};
??之后對緩沖區(qū)的操作如果出現(xiàn)需要使用緩沖區(qū)大小這一信息的情況時可以直接使用buf_size常量蔓肯,保證需要修改緩沖區(qū)大小時不需要修改多個地方。人們稱以上技巧為非硬編碼振乏。
與預處理變量的區(qū)別
朋友的朋友指出蔗包,C++11中有用于定義常量表達式的關鍵字constexpr,不僅完全替代了預處理變量的功能慧邮,還比后者更安全调限,所以宏定義永遠不再應該被用來定義預處理變量了,故以下內容僅作了解误澳。
??在C語言中經常能看見大量的宏定義耻矮。宏定義經常會被用來定義一些預處理變量,如:
#define BUF_SIZE 1024
??源程序添加以上語句后忆谓,在編譯前的預處理階段該語句以下的BUF_SIZE都被替換為1024淘钟。比如:
//預處理前源程序
char *buf[BUF_SIZE] = {0};
//預處理后等價于
char *buf[1024] = {0};
??C++繼承了C語言的預處理機制,也就是說C++也可以使用宏定義定義預處理變量。在C++11前米母,為了表示空指針經常會使用到定義在<cstdlib>中的NULL正是預處理變量勾扭。
??如上面說的,宏定義參與的是編譯前的預處理過程铁瞒,看起來與文本替換有點相似妙色。于是預處理變量與const修飾的變量最大差別出現(xiàn)了:聚焦上面的例子,預處理變量BUF_SIZE在編譯前完成了替換的工作慧耍,到了編譯階段編譯器根本不知道1024曾經是一個預處理變量而把它當作一個字面值處理身辨;而const修飾的變量buf_size是會參與編譯過程并分配到作為變量應有的內存的。拋開繁瑣的細節(jié)簡單地說:預處理變量參與預處理過程而const修飾的變量參與編譯過程芍碧;預處理變量并不是真正意義上的變量而const修飾的變量是煌珊。
??根據上面的總結,預處理變量似乎比const修飾的變量好用:沒有多余定義變量的指令泌豆。想下定論前筆者認為還有兩點需要考慮到:首先是前者僅是一個替換的過程定庵,比起有指定類型且參與編譯過程的后者可能會有著更多的安全問題;如果常量不是簡單的內置類型(如字符串)且在多處被使用踪危,前者每次都需要申請臨時空間而后者只需要申請一次即可蔬浙。
??筆者的見解是,在所需常量為內置類型且有足夠把握不會出現(xiàn)安全問題的情況下使用預處理變量贞远,而其他情況使用const修飾的變量比較好畴博;甚至如果閑麻煩可以全部都使用后者。
術語解釋:頂層const與底層const
??按照《C++Primer》里的說法蓝仲,頂層const(top-level const)指變量本身是不可改變的俱病,前文例子中buf_size即是頂層const;而底層const(low-level const)與以指針為代表的復合類型所指向的值有關袱结,如:
const int *pi = &ivar;
??此時可以說pi是底層const庶艾。
指針與const
??以下通過例子列舉了const在指針中的使用:
const int cval = 7;
int val = 777, val2 = 7777;
const int *pi1 = &cval; //pi1是一個指向int常量的指針,且確實指向了常量
const int *pi2 = &val; //pi2是一個指向int常量的指針擎勘,但其實指向的并不是常量
*pi1 = 9; //錯誤咱揍,不允許更改指向了const的指針所指向的值
pi1 = &val; //正確,允許更改pi1的指向
*pi2 = 888; //錯誤棚饵,不允許更改指向了const的指針所指向的值
int *const pi3 = &cval; //錯誤:pi3是一個指向int的常量指針煤裙,但卻指向了常量
int *const pi4 = &val; //pi4是一個指向int的常量指針,且確實指向了int變量
pi4 = &val2; //錯誤噪漾,不允許更改常量指針的指向
*pi4 = 999; //正確硼砰,允許更改指向非const的指針所指向的值
const int *const pi5 = &cval; //pi5是一個指向int常量的常量指針
??通過上面可以看出,本身是常量時欣硼,指針不允許修改自身的指向题翰;而當所指向的值類型被const所修飾時,不能修改指針所指向的值。前者即為頂層const的含義豹障,后者為底層const的含義冯事。最后一個例子指出指針可以同時是頂層const以及底層const。
引用與const
??引用在普通情況是不可以與字面值血公、表達式等綁定的昵仅,但如果引用加上const后就可以達到這一目的了。下面是const引用的一些例子:
int i = 7;
const int ci = 7;
const int &ival1 = i; //ival1綁定了一個int變量累魔,但不能通過ival改變i的值
const int &ival2 = ci; //ival2綁定了一個int常量
const int &ival3 = 777; //ival3綁定了一個字面值
const int &ival4 = ci + 1; //ival4綁定了一個表達式
int &ival5 = 777; //錯誤摔笤,引用不能綁定字面值
int &ival6 = 777; //錯誤,引用不能綁定表達式
??上述例子ival1垦写、ival2的情況都是可以預見的吕世,至于ival3與ival4是因為編譯器會將其做如下變換(ival3為例):
const int temp = 777;
const int &ival3 = temp;
??也就是說,最終ival3綁定的是一個存放了777的臨時變量梯投。
??由于頂層const是針對變量而言的命辖,所以不能被稱為變量的引用并不存在頂層const。
??上面的例子也表明了晚伙,無論是指針也好引用也好,身為底層const時它們會認為與之綁定或其指向的變量為頂層const俭茧,所以對變量的值進行修改是非法的咆疗。而實際上那個變量并沒有一定被const修飾,只是它們自以為的罷了母债。
??以上就是我們討論的const的內容了午磁。事實上const還有其他用處和比較特殊的用法在這里并沒有提到,主要還是對變量定義的const作用展開了一些討論和記錄毡们。如果有機會其他的用法會穿插在其他的C++討論中迅皇。