在這篇文章中,我總結了一些C/C++語言中的 const 修飾符的常見用法,供大家參考。 const 的用法,也是技術性面試中常見的基礎問題,希望能夠幫大家梳理一下知識,給大家一點點幫助。作者是菜鳥一枚,難免出錯,還望各位大牛不吝賜教。
首先,來看看const的基本含義。在 C/C++ 語言中,const關鍵字是一種修飾符。所謂“修飾符”,就是在編譯器進行編譯的過程中,給編譯器一些“要求”或“提示”,但修飾符本身,並不產生任何實際代碼。就 const 修飾符而言,它用來告訴編譯器,被修飾的這些東西,具有“只讀”的特點。在編譯的過程中,一旦我們的代碼試圖去改變這些東西,編譯器就應該給出錯誤提示。
所以,const修飾符的作用主要是利用編譯器幫助我們檢查自己代碼的正確性。我們使用const在源碼中標示出“不應該改變”的地方,然后利用編譯器,幫助我們檢查這些地方是否真的沒有被改變過。如果我們不小心去修改了這些地方,編譯器就會報錯,從而幫助我們糾正錯誤。使用const和不使用const,對於最終編譯產生的代碼並沒有影響。
雖然const對於最終代碼沒有影響,但是盡可能使用const,將幫助我們避免很多錯誤,提高程序正確率。
在C/C++中,常見 const 用法有以下幾種:
一、const 變量
const 變量指的是,此變量的值是只讀的,不應該被改變。
- 如果我們在程序中試圖修改 const 變量的值,在編譯的時候,編譯器將給出錯誤提示。
- 正因為 const 變量的值在給定以后不能改變,所以 const 變量必須被初始化。(如果不初始化,之后還怎么賦值呢?)如果我們不初始化 const 變量,編譯時也會有錯誤提示。
const int debugLevel = 10; const int logLevel; // 編譯錯誤:未初始化const變量(這個錯誤是否提示,和所用的編譯器有關) debugLevel = 5; // 編譯錯誤:給只讀變量賦值
在C++中,const 全局變量被用來替換一般常量宏定義。因為雖然 const 變量的值不能改變,但是依然是變量,使用時依然會進行類型檢查,要比宏定義的直接替換方法更嚴格一些(下文還會討論這個問題)。
結構體變量也是一種變量,const 結構體變量是什么含義呢?
1 struct debugInfo 2 { 3 int debugLevel; 4 int line; 5 }; 6 7 int main( int argc, char *argv[]) 8 { 9 const struct debugInfo debug_const1; // 編譯錯誤:未初始化只讀變量(與編譯器實現有關) 10 11 struct debugInfo debug_not_const; 12 13 debug_not_const.debugLevel = 10; 14 debug_not_const.line = 5; 15 16 const struct debugInfo debug_const2 = debug_not_const; 17 18 debug_const2.debugLevel = 10; // 編譯錯誤:不允許修改只讀變量 19 debug_const2.line = 2; // 編譯錯誤:不允許修改只讀變量 20 21 return 0; 22 }
在C中,const 結構體變量表示結構體中任何數據域均不允許改變,且需要另一個結構體變量進行初始化。在C++中,struct與class除了默認訪問權限之外,並無本質區別。在下一節進行討論。
二、const 類對象
const類對象指的是,此類對象不應該被改變。
- const 類對象與 const 變量並無實質不同,只在於類對象的 “改變” 定義。
- 類對象的 “改變” 定義:改變任何成員變量的值,調用任何非const成員函數
class CDebugModule { public: CDebugModule() {}; ~CDebugModule() {}; public: int m_debugLevel; public: void SetDebugLevel(int debugLevel) { m_debugLevel = debugLevel;}; void PrintDebugLevel(void) { cout << m_debugLevel;}; void PrintDebugLevel_const(void) const { cout << m_debugLevel;}; // const 類成員函數 }; int main( int argc, char *argv[]) { const CDebugModule debug; debug.m_debugLevel = 10; // 編譯出錯:不能直接改變成員變量 debug.SetDebugLevel(20); // 編譯出錯:不能通過成員函數改變成員變量 debug.PrintDebugLevel(); // 編譯出錯:不能調用非 const 成員函數 debug.PrintDebugLevel_const(); // 正常 return 0; }
不能改變 const 類對象的任何成員變量,這一點比較好理解,因為 const 本身就帶有不可改變變量取值(內部狀態)的含義。為何const 類成員不能調用非const成員函數呢?我們將在 第九節“const 成員函數” 進行探討。在C++中,struct和class沒有明顯差別,不再贅述。
三、指向 const 變量的指針
指向 const 變量的指針,指的是一個指針,其中保存着一個 const 變量的地址。
- 由於指針指向一個 const 變量,所以通過指針,不能修改這個 const 變量的值。
- 雖然指針指向的值不能改變,但是指針的指向可以改變。
const int debugLevel = 10; const int logLevel = 10; const int *p = &debugLevel; p = &logLevel; // 正常,指針的指向可以改變 *p = 10; // 編譯錯誤,指針指向的位置是只讀的
在 *p = 10, 這一句,編譯器是通過“指針的類型”(const int *)還是通過其“指向變量的類型”(const int )來判斷只讀的呢?我們可以通過下面這個小程序來求證一下:
1 const int debugLevel = 10; // const 變量 2 int logLevel = 10; // 普通變量 3 4 const int *p; 5 int *q; 6 7 p = &logLevel; // 我們讓指向 const 變量的指針指向一個普通變量 8 q = (int*)&debugLevel; // 讓指向普通變量的指針,指向一個 const 變量 9 10 *q = 5; // 編譯正常 11 *p = 5; // 編譯出錯:位置為只讀
通過10、11行程序的編譯結果,我們可以看出,如果指針的類型為“指向const變量的指針”,即使其指向的內容是非const變量,也無法通過這個指針改變數據內容。反之,如果指針的類型是“指向非const變量的指針”,即使指向的是const變量,也可以通過這個指針改變const變量的內容(稍后討論這一點)。所以,編譯器是通過 “指針的類型” 來判斷是否只讀的。
說到這點,我覺得可以這么解釋。因為我們沒有使用面向對象編程,也就不具備動態判斷對象具體類型的能力。所以,編譯器只能夠靜態地判斷對象的類型。這樣,編譯器就只能識別出指針的類型,而不清楚指針指向的內容的具體類型了。當然也就只能通過指針類型來判斷內容是否只讀了。
在上面,我們通過指針,“改變”了const變量的內容,如果我們在上邊的程序中添加上輸出,會是什么結果?
12 printf("debugLevel = %d\n", debugLevel); 13 printf("*q = %d\n", *q); 14 printf("debugLevel address = %x\n", &debugLevel); 15 printf("q = %x\n", q);
從上邊的說明,我們可以想象,debugLevel的值,被我們通過指針改變了,所以輸出應該是:
debugLevel = 5 *q = 5 debugLevel address = 0xbfaff718 q = 0xbfaff718
但是,事實上,這個結果是不確定的!跟您所用的編譯器和優化級別有關。我在g++ 4.1.2上,編譯運行得出了以下結果:
debugLevel = 10 // 直接打印可以發現,const 變量的值未改變 ! *q = 5 // 通過指針訪問,發現 const 變量的值改變了! debugLevel address = 0xa6a65318 q = 0xa6a65318 // 指針的指向並沒有錯誤
乍一看,好像同一個地址的東西,采用變量名訪問和采用指針訪問,得到的結果竟然不一樣。其實,之所以產生這種結果,是由於在編譯器變異的過程中,對 const 變量的訪問進行了優化。編譯器將 const 變量直接替換為對應的內容。也就是說,在編譯的過程中 :
printf("debugLevel = %d\n", debugLevel);
這個語句,被直接替換成了:
printf("debugLevel = %d\n", 10);
所以,才產生了上邊的現象。當然,這種替換不一定會發生,跟編譯器和優化等級相關。
上文已經提到了,C++建議使用 const 全局變量來替換一般常量的宏定義。通過這個例子可以看出,使用 const 全局變量之后,由於編譯器會替換其為具體內容,所以在程序實際運行中,並不會產生一次變量訪問,也就使得 const 全局變量和宏定義具有相同的執行效率。同時,使用 const 全局變量,可以讓編譯器幫助我們進行變量類型檢查,提高正確率。
指針也是變量的一種,所以自然有 const 類型指針。
四、const 指針
const指針指的是,一個指針本身經過 const 修飾,自身內容(指針指向)不應該發生改變。
- 指針的指向一經確定,不能改變。指針指向的內容,可以改變。
int logLevel = 10; int logId = 0; int * const p = &logLevel; int * const q; // 編譯錯誤,未初始化 const 變量(這個錯誤是否報告,和所用的編譯器有關) *p = 5; // 正常,const指針指向內容可以改變 p = &logId // 編譯錯誤,const指針自身內容(指向)不能改變
指針也是一種變量,只不過其內容保存的是地址而已。所以const指針的內容不能改變,也即是它的指向不能改變。
const指針和指向const變量的指針,在寫法上容易使人混淆。給大家介紹一種我自己用着比較順手的區分方法:從右向左,依次結合,const就近結合。
比如,int * const p 可以這樣進行解讀:
1、int * ( const p ):變量p 經過 const 修飾,為只讀變量。
2、int (* (const p)):(const p 現在作為一個整體) 只讀變量p是一個指針。
3、(int (* (const p))):(同樣的 * const p 作為一個整體) 這個只讀的指針p,指向一個int型變量。
於是,可以區分出 int * const p 是一個指向 int 型的const指針。
再比如,const int * p 可以這樣解讀:
1、const int (* p):變量p是一個指針。
2、(const int) (* p):(const與就近的 int 結合)這個指針指向 const int 型變量。
所以,const int * p 是一個指向 const 整形變量的指針。
采用這個方法,相信大家可以自己分辨 int const * p的含義了。
值得注意的是,有的編譯器對重復的 const 不會報錯!所以允許像 const int const *p; 這種寫法。在分析這種“錯誤”的寫法時,只要把重復修飾的const忽略即可。
五、指向 const 變量的 const 指針
這種情況是 const 指針和 指向 const 變量的指針的結合,相信大家已經能夠自己分析,不再贅述。
六、const 變量作為函數參數
在函數調用的過程中,函數的參數是建立在函數的棧上的變量。既然是變量,就可以通過 const 進行修飾。
- 將函數參數聲明為 const 類型,表示對於函數來說,這個參數是一個 const 變量。也就是說,函數內部不能夠改變這個參數的值。
- 將函數參數是一個指針,把它聲明為 “指向 const 變量的指針” ,允許上層使用 ”指向 const 變量的指針“ 或 普通指針 作為參數,調用函數。(如果參數聲明的是普通指針,則不允許使用 指向 const 變量的指針 作為參數調用)(與編譯器有關)
1 // 接收一個int變量,並在函數內部,認為它是只讀的 2 void OutputInt_const( const int a ) 3 { 4 a = 5; // 編譯錯誤:不允許修改只讀變量 5 printf("a = %d\n", a); 6 } 7 8 // 接收一個int變量,在函數內部,認為它是普通的 9 void OutputInt_not_const( int a ) 10 { 11 a = 5; // 正常 12 printf("a = %d\n", a); 13 } 14 15 // 接收一個 指向const型整形數的指針 16 void OutputInt_p_const( const int *a ) 17 { 18 *a = 5; // 編譯錯誤: 19 printf("*a = %d\n", *a); 20 } 21 22 // 接收一個普通指針 23 void OutputInt_p_not_const( int *a ) 24 { 25 *a = 5; 26 printf("*a = %d\n", *a); 27 } 28 29 // 主函數 30 int main( int argc, char *argv[]) 31 { 32 int logLevel = 10; 33 const int debugLevel = 5; 34 35 OutputInt_const(logLevel); 36 OutputInt_const(debugLevel); 37 38 OutputInt_not_const(logLevel); 39 OutputInt_not_const(debugLevel); 40 41 OutputInt_p_const(&logLevel); 42 OutputInt_p_const(&debugLevel); 43 44 OutputInt_p_not_const(&logLevel); 45 OutputInt_p_not_const(&debugLevel); // 編譯錯誤:從 const int * 到 int * 轉換失敗(與編譯器有關) 46 47 return 0; 48 }
為什么對於指針參數的要求特殊?其實我們可以仔細想一下 const 在修飾參數時的作用。
首先,函數參數是函數內部可見的一個變量。在const 修飾函數參數時,僅僅表示此函數內部對於這個變量的限制。對於傳進來的參數,在外邊究竟是什么樣子的,函數內部並不關心。所以,函數 void OutputInt_const( const int a ) 並不會在意傳入的參數在main函數中是否是只讀的。它只會在函數內部,將入參當作只讀變量處理。
既然 const 修飾函數參數時,不會限制入參是否為只讀,為什么 OutputInt_p_not_const( int *a ) 和 OutputInt_p_const( const int *a ) 的調用方式有區別呢(見44、45行)?
其實答案很簡單,const 在此處並不是修飾函數參數的!此處的 const ,與 int * 組合,描述了參數的一種類型。OutputInt_p_const函數要求的參數是:指向只讀整形的指針。所以,只要調用時傳入的參數不是一個指向只讀整形數的指針,就會發生類型不匹配。在示例41行的調用中,使用一個 int * 來調用 OutputInt_p_const 函數,發生類型不匹配,但是 int * 可以隱式轉換為 const int *,所以此處調用可以成功。但在45行中,采用 const int * 來調用需要 int * 的 OutputInt_p_not_const 函數,發生類型不匹配, const int * 不能夠隱式轉換為 int *,所以此處調用失敗。
為什么 int * 可以隱式轉換為 const int *,但是反向就不可以呢?相信各位讀者已經想到了。隱式轉換不放寬對於變量的要求,而 const 型的變量顯然比非 const 型變量要求嚴格,所以不能由 const int * 轉為 int *。
七、const 返回值
const 型的返回值,指的是函數的返回值為一個 const 變量。
- 函數返回const返回值,主要用於函數返回const引用。
1 #include <string> 2 3 using namespace std; 4 5 // 返回 const 引用的函數 6 const string& SetVersion_const(string & versionInfo) 7 { 8 versionInfo = "V0.0.3"; 9 return versionInfo; 10 } 11 12 // 返回普通引用的函數 13 string& SetVersion_not_const(string & versionInfo) 14 { 15 versionInfo = "V0.0.3"; 16 return versionInfo; 17 } 18 19 // 主函數 20 int main( int argc, char *argv[]) 21 { 22 string versionInfo; 23 24 SetVersion_const(versionInfo) = "V0.0.5"; // 編譯錯誤,返回的引用為 const 引用,不允許修改。 25 26 SetVersion_not_const(versionInfo) = "V0.0.5"; // 正常,返回的引用為普通引用,可以修改內容。 27 28 return 0; 29 }
引用是一個對象的別名,相當於 const 指針,其指向一經確定,就不能改變了。而 const 引用,則相當於指向 const 變量的 const 指針,其指向和指向的內容均不允許改變。所以在函數返回 const 引用時,不能夠通過函數返回的引用對實際對象進行任何修改,即便對象本身不是 const 的。在本例中,versionInfo 在 main 函數中不是const的。SetVersion_const 函數返回了一個指向 versionInfo 的 const 引用,不能通過此引用,對 versionInfo 進行修改。
為什么會出現這種現象?相信大家都能理解了。請參考 指向 const 變量指針 的相關內容。
八、const 成員變量
const 成員變量指的是類中的成員變量為只讀,不能夠被修改(包括在類外部和類內部)。
- const 成員變量必須被初始化(在相關構造函數的初始化列表中),初始化后,不能夠被修改。
- 靜態 const 成員變量需要在類外部單獨定義並初始化(可定義在頭文件)
1 class CDebugModule 2 { 3 public: 4 CDebugModule(); 5 ~CDebugModule(); 6 7 public: 8 const int m_debugLevel; 9 static const int m_debugInfo; 10 11 }; 12 13 const int CDebugModule::m_debugInfo = 1; // 靜態常量成員需要在類外進行單獨定義和初始化 14 15 CDebugModule::CDebugModule() 16 : m_debugLevel(10) // const 成員在構造函數初始化列表中初始化 17 { 18 } 19 20 CDebugModule::~CDebugModule() 21 { 22 } 23 24 int main(int argc, char *argv[]) 25 { 26 CDebugModule debugModule; 27 28 debugModule.m_debugLevel = 10; // 編譯錯誤,不能改變只讀成員 29 CDebugModule::m_debugInfo = 10; // 編譯錯誤,不能改變只讀成員 30 31 return 0; 32 }
類對象的實例化過程可以理解為包含以下步驟:首先,開辟整個類對象的內存空間。之后,根據類成員情況,分配各個成員變量的內存空間,並通過構造函數的初始化列表進行初始化。最后,執行構造函數中的代碼。由於 const 成員變量必須在定義(分配內存空間)時,就進行初始化。所以需要在夠在函數的初始化列表中初始化。const成員在初始化之后,其值就不允許改變了,即便在構造內部也是不允許的。
靜態成員變量並不屬於某個類對象,而是整個類共有的。靜態成員變量可以不依附於某個實例化后的類對象進行訪問。那么,靜態成員變量的值,應該在任何實例化操作之前,就能夠進行改變(否則,只有實例化至少一個對象,才能訪問靜態成員)。所以,靜態成員變量不能夠由構造函數進行內存分配,而應該在類外部單獨定義,在實例化任何對象之前,就開辟好空間。又由於 const 成員變量 必須初始化,所以靜態成員變量必須在定義的時候就初始化。
九、const 成員函數
const成員函數指的是,此函數不應該修改任何成員變量。
- 傳給const成員函數的this指針,是指向 const 對象 的 const 指針。
- const成員函數,不能夠修改任何成員變量,除非成員變量被 mutable 修飾符修飾。
1 class CDebugModule 2 { 3 public: 4 CDebugModule() {}; 5 ~CDebugModule(); 6 7 public: 8 int m_debugLevel_not_mutable; // 不帶 mutable 修飾的成員變量 9 mutable int m_debugLevel_mutable; // 帶 mutable 修飾的成員變量 10 11 public: 12 void SetDebugLevel_not_const(int debugLevel); // 非 const 成員函數 13 void SetDebugLevel_const(int debugLevel) const; // const 成員函數 14 }; 15 16 void CDebugModule::SetDebugLevel_not_const(int debugLevel) 17 { 18 m_debugLevel_not_mutable = debugLevel; 19 m_debugLevel_mutable = debugLevel; 20 return; 21 } 22 23 void CDebugModule::SetDebugLevel_const(int debugLevel) const 24 { 25 m_debugLevel_not_mutable = debugLevel; // 編譯錯誤,const 成員函數不能修改一般的成員變量 26 m_debugLevel_mutable = debugLevel; // 正常,當成員變量被 mutable 修飾時,const成員函數就能修改了 27 return; 28 } 29 30 int main(int argc, char *argv[]) 31 { 32 CDebugModule debugModule; 33 34 debugModule.SetDebugLevel_not_const(10); 35 debugModule.SetDebugLevel_const(10); 36 37 return 0; 38 }
在成員函數調用的過程中,都有一個 this 指針被當做參數隱性地傳遞給成員函數(可能通過棧,也可能通過CPU寄存器)。這個this指針,指向調用這個函數的對象(這樣,成員函數才能找到成員變量的地址,從而對其進行操作)。這個this指針,是個 const指針,不能修改其指向(你不希望這個對象的函數,修改了那個對象的成員變量,對吧?)。
傳遞給const成員函數的this指針,指向一個const對象。也就是說,在const成員函數內部,這個this指針是一個指向const對象的const指針。通過第二節的探討,相信大家已經能夠明白,為什么const成員函數不能修改任何成員變量了。
mutable 修飾符使得const函數的行為有了一些靈活性。相當於提醒編譯器,這個成員變量比較特殊,就不要進行任何只讀檢查了。
我們在第二節留下了一個問題 “為什么 const 對象只能夠調用const成員函數呢?”,其實是這樣的。由於對象本身通過 const 修飾,那么指向這個對象的指針也就是指向const對象的const指針了。換句話說,指向這個對象的this指針就是指向const對象的const指針。一般成員函數要求的this指針(別忘了this指針也是一個參數)為:指向對象的const指針。所以此處發生了參數不匹配,無法進行調用。而 const 成員函數要求的this指針,恰恰是 指向const對象的const指針。所以依然能夠調用。
十、總結
const 變量 |
const int a; |
不能修改值,必須初始化 |
const 類對象 |
const MyClass a; |
不能修改成員變量的值,不能調用非 const 函數 |
指向 const 變量的指針 |
const int * a; |
指向內容不可變,指向可變 |
const 指針 |
int * const a; |
指向內容可變,指向不可變 |
指向 const 變量的 const 指針 |
const int * const a; |
指向內容不可變,指向也不可變 const 引用 |
const 變量作為函數參數 |
void myfun(const int a); |
函數內部不能改變此參數 指向 const 變量的指針做參數,允許上層用一般指針調用。(反之不可) |
const 返回值 |
const string& myfun(void); |
用於返回const引用 上層不能使用返回的引用,修改對象 |
const 成員變量 |
const int a; static const int a; |
必須在初始化列表初始化,之后不能改變 static const 成員變量需要單獨定義和初始化 |
const 成員函數 |
void myfun(void) const; |
this指針為 指向const對象的const指針 不能修改 非mutable 的成員變量 |
本文的內容就這么多了,感謝您能夠看到最后,希望對您能夠有一點點幫助 ^_^