[原創] 基礎中的基礎(二):C/C++ 中 const 修飾符用法總結

  在這篇文章中,我總結了一些C/C++語言中的 const 修飾符的常見用法,供你們參考。 const 的用法,也是技術性面試中常見的基礎問題,但願可以幫你們梳理一下知識,給你們一點點幫助。做者是菜鳥一枚,不免出錯,還望各位大牛不吝賜教。面試

  首先,來看看const的基本含義。在 C/C++ 語言中,const關鍵字是一種修飾符。所謂「修飾符」,就是在編譯器進行編譯的過程當中,給編譯器一些「要求」或「提示」,但修飾符自己,並不產生任何實際代碼。就 const 修飾符而言,它用來告訴編譯器,被修飾的這些東西,具備「只讀」的特色。在編譯的過程當中,一旦咱們的代碼試圖去改變這些東西,編譯器就應該給出錯誤提示。編程

  因此,const修飾符的做用主要是利用編譯器幫助咱們檢查本身代碼的正確性。咱們使用const在源碼中標示出「不該該改變」的地方,而後利用編譯器,幫助咱們檢查這些地方是否真的沒有被改變過。若是咱們不當心去修改了這些地方,編譯器就會報錯,從而幫助咱們糾正錯誤。使用const和不使用const,對於最終編譯產生的代碼並無影響。小程序

  雖然const對於最終代碼沒有影響,可是儘量使用const,將幫助咱們避免不少錯誤,提升程序正確率。函數

 

  在C/C++中,常見 const 用法有如下幾種:優化

 

1、const 變量this

  const 變量指的是,此變量的值是隻讀的,不該該被改變。spa

  • 若是咱們在程序中試圖修改 const 變量的值,在編譯的時候,編譯器將給出錯誤提示。
  • 正由於 const 變量的值在給定之後不能改變,因此 const 變量必須被初始化(若是不初始化,以後還怎麼賦值呢?)若是咱們不初始化 const 變量,編譯時也會有錯誤提示。
const int debugLevel = 10;
const int logLevel;                // 編譯錯誤:未初始化const變量(這個錯誤是否提示,和所用的編譯器有關)

debugLevel = 5;                    // 編譯錯誤:給只讀變量賦值

 

  在C++中,const 全局變量被用來替換通常常量宏定義。由於雖然 const 變量的值不能改變,可是依然是變量,使用時依然會進行類型檢查,要比宏定義的直接替換方法更嚴格一些(下文還會討論這個問題)。debug

  結構體變量也是一種變量,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除了默認訪問權限以外,並沒有本質區別。在下一節進行討論。code

 

2、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沒有明顯差異,再也不贅述。

 

3、指向 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;  // 編譯出錯:位置爲只讀

  經過十、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 類型指針。

 

4、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 能夠這樣進行解讀:

  一、int * ( const p ):變量p 通過 const 修飾,爲只讀變量。

  二、int (* (const p)):(const p 如今做爲一個總體) 只讀變量p是一個指針。

  三、(int (* (const p))):(一樣的 * const p 做爲一個總體) 這個只讀的指針p,指向一個int型變量。

  因而,能夠區分出 int * const p 是一個指向 int 型的const指針。

 

  再好比,const int * p 能夠這樣解讀:

  一、const int (* p):變量p是一個指針。

  二、(const int) (* p):(const與就近的 int 結合)這個指針指向 const int 型變量。

  因此,const int * p 是一個指向 const 整形變量的指針。

 

  採用這個方法,相信你們能夠本身分辨 int const * p的含義了。

  值得注意的是,有的編譯器對重複的 const 不會報錯!因此容許像 const int const *p; 這種寫法。在分析這種「錯誤」的寫法時,只要把重複修飾的const忽略便可。

 

5、指向 const 變量的 const 指針

  這種狀況是 const 指針和 指向 const 變量的指針的結合,相信你們已經可以本身分析,再也不贅述。

 

6、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 ) 的調用方式有區別呢(見4四、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 *。

 

7、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 變量指針 的相關內容。

  

8、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 成員變量 必須初始化,因此靜態成員變量必須在定義的時候就初始化。

 

 9、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指針。因此依然可以調用。

  

 10、總結  

 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 的成員變量

 

  本文的內容就這麼多了,感謝您可以看到最後,但願對您可以有一點點幫助 ^_^

相關文章
相關標籤/搜索