一、const是什麼
$$E=mc^2$$
$$\sum_{i=0}^N\frac{i^2}{i+1}$$
在 C/C++ 語言中,const關鍵字是一種修飾符。所謂“修飾符”,就是在編譯器進行編譯的過程中,給編譯器一些“要求”或“提示”,但修飾符本身,並不產生任何實際代碼。就 const 修飾符而言,它用來告訴編譯器,被修飾的這些東西,具有“只讀”的特點。在編譯的過程中,一旦我們的代碼試圖去改變這些東西,編譯器就應該給出錯誤提示。
所以,const修飾符的作用主要是利用編譯器幫助我們檢查自己代碼的正確性。我們使用const在源碼中標示出“不應該改變”的地方,然後利用編譯器,幫助我們檢查這些地方是否真的沒有被改變過。如果我們不小心去修改了這些地方,編譯器就會報錯,從而幫助我們糾正錯誤。使用const和不使用const,對於最終編譯產生的代碼並沒有影響。
雖然const對於最終代碼沒有影響,但是儘可能使用const,將幫助我們避免很多錯誤,提高程序正確率。
二、const可以修飾哪些對象
在上面已經提到過了,const是一種修飾符,那它可以作爲哪些對象的修飾符呢?下面列舉了一些C/C++中用到const的地方。
1,const變量
2,const指針
3,const引用
4,const類
5,類的const成員變量
6,類的const成員函數
7,const修飾函數的形參與返回值
下面我們分別討論上面幾種情況下,const的用法。
三、const與變量
當一個變量被const修飾後,具有以下幾個特點:
1)該變量只能讀取不能修改。(編譯器進行檢查)
2)定義時必須初始化。
3)C++中喜歡用const來定義常量,取代原來C風格的預編譯指令define。
1 const int var; // Error:常量 變量"var"需要初始化設定項 2 const int var1 = 42; 3 var1 = 43; // Error:表達式必須是可以修改的左值
上面代碼中第一行和第三行都有錯誤,註釋便是編譯器給出的錯誤提示。
另外注意,在使用const變量作爲數組的下標時,變量的值一定要是一個常量表達式(在編譯階段就能計算得到結果)。
1 const int sz = 42; 2 int iAr[sz]; 3 const int sz1 = size(); // size()必須是一個返回常量的函數 4 int iAr1[sz1]; 5 6 int var = 42; 7 const int sz2 = var; 8 int iAr2[sz2]; // error:sz2只有運行時才知道值
四、const與引用
我們知道,引用必須在定義的時候賦值,這樣就會所引用的變量綁定在一起並作爲它的一個別名,在程序中的其他地方,是不能讓引用再與其他對象綁定。這個特性,讓引用看起來就像是const對象一樣,一旦定義後將不能更改。所以並不存在const的引用。
但是我們卻可以引用一個const的對象(變量),我們稱之爲對常量的引用,與普通的引用不同的時,對常量的引用不能被用作修改它所綁定的對象。
1 const int ci = 1024; 2 const int &r1 = ci; 3 r1 = 42; // Error:r1是對常量的引用 4 int & r2 = ci; //Error:不能將一個非常量引用指向一個常量的對象
我們知道,引用的類型必須與其所引用對象的類型一致,如下面的代碼:
double dval = 3.14; int& ri = dval; // Error:無法用double類型的值初始化int&類型的引用(非常量限定)
上述代碼爲何不行?
此處ri引用了一個int型的整數。對於ri的操作數應該是整數運算,但是dval卻是一個雙精度的浮點數而非整數。因此爲了確保讓ri綁定一個整數,編譯器把上述代碼變成了如下形式:
double dval = 3.14; int temp = dval; int& ri = temp;
其中temp是一個臨時變量,而ri綁定了一個臨時量,所以當ri改變時,並沒有改變davl的值,所以這種引用是無效的。
也許你注意到了,當我們把double變量綁定在一個int&類型上時,編譯器提示後有個括號:非常量限定。這說明如果是一個常量的引用,則有可能是通過的,顯然下面的代碼就沒有任何問題:
double dval = 3.14; const int& ri = dval;
因爲在這裏,ri是一個常量引用,我們並不想通過ri改變dval的值,只要能讀到dval對應的int型的值就行。
五、const與指針
我們知道,指針與引用不同,指針本身是一個對象,所以存在常量指針,這種指針在定義並初始化後,便不能再指向其他變量。用來修飾這種常量指針的const,我們稱之爲"頂層const"。
與頂層指針對應的是底層指針,這種指針指向一個const修改的對象,這一點上就有點像是常量的引用。
對於指向常量的指針或引用,都有以下規則:
1)可以將一個非const對象的地址賦給一個指向const對象的指針
2)可以將一個非const對象的地址賦給一個指向非const對象的指針
3)可以將一個const對象的地址賦給一個指向const對象的指針
4)不可以將一個const對象的地址賦給一個指向非const對象的指針。
1 int var; 2 const int ci = 42; 3 4 int *p1 =& var; 5 int *p2 = &ci; // Error,const int* 不能用於初始化int* 6 const int *p3 = &var; //ok 7 const int *p4 = &ci; // ok
還有一種指向const對象的const指針,這種指針首先表明,本身是一個const指針,一旦初始化後不能指向其他對象;其次,它本身所指向的對象也是一個常量,即不能通過指針修改對象的值。
const int var = 42; const int* const p = &var;
這裏再強調一點,const只是給編譯器看的,我們可以很輕鬆的騙過編譯器,並看看編譯器都做了什麼:
1 const int var = 42; 2 int* p = (int*)&var; 3 *p = 20; 4 cout << var << endl; //42 5 cout << *p << endl; //20
我們在代碼的第2行,用一個類型轉換強制的,把一個非const指針指向了一個const對象。
但是後面我們通過這個指針來修改這個值,卻沒有生效,原因呢?
那是因爲編譯器在編譯階段發現var是一個常量,所以在編譯目標代碼時已經將var的地方都用42進行了替換。
六、const與類
其實類定義的對象,與普通的變量是一樣的,用const修飾時,說明這個類是一個常量類對象,這個對象有下面2個特點:
1)不能改變其成員變量(非mutalbe成員)
2)不能調用其非const成員函數
1 class AClass{ 2 public: 3 int m_var; 4 mutable int m_mutable_var; 5 void setVar(int var){ m_var = var; } 6 void printVar(){ cout << m_var; } 7 void printVar_const()const { cout << m_var; } 8 }; 9 10 const AClass ac; 11 ac.m_var = 20; // Error:ac是一個const類,不能修改成員變量 12 ac.m_mutable_var = 42; // ok 可以修改mutable修飾的變量 13 ac.setVar(20); // Error: ac不能調用非const成員函數,而且這個成員函數還修改了成員變量的值 14 ac.printVar();// Error:ac不能調用非const成員函數 15 ac.printVar_const(); // ok
七、const與類的成員
1,const成員變量
const 成員變量指的是類中的成員變量爲只讀,不能夠被修改(包括在類外部和類內部)。
1)const 成員變量必須在類的構造函數初始化表達式中被初始化,即使在構造函數體內也不可以。
2)靜態 const 成員變量需要在類外部單獨定義並初始化(可定義在頭文件)
1 class constTestClass 2 { 3 public: 4 const int var; 5 static const int sci; 6 public: 7 constTestClass() :var(42){} // const成員變量必須在類的構造函數初始化列表中初始化 8 }; 9 const int constTestClass::sci = 42; // static const成員變量需要在類外單獨進行定義和初始化
類對象的實例化過程可以理解爲包含以下步驟:首先,開闢整個類對象的內存空間。之後,根據類成員情況,分配各個成員變量的內存空間,並通過構造函數的初始化列表進行初始化。最後,執行構造函數中的代碼。由於 const 成員變量必須在定義(分配內存空間)時,就進行初始化。所以需要在夠在函數的初始化列表中初始化。const成員在初始化之後,其值就不允許改變了,即便在構造內部也是不允許的。
靜態成員變量並不屬於某個類對象,而是整個類共有的。靜態成員變量可以不依附於某個實例化後的類對象進行訪問。那麼,靜態成員變量的值,應該在任何實例化操作之前,就能夠進行改變(否則,只有實例化至少一個對象,才能訪問靜態成員)。所以,靜態成員變量不能夠由構造函數進行內存分配,而應該在類外部單獨定義,在實例化任何對象之前,就開闢好空間。又由於 const 成員變量 必須初始化,所以靜態成員變量必須在定義的時候就初始化。
2,const成員函數
const成員函數指的是,此函數不應該修改任何成員變量。
1)傳給const成員函數的this指針,是指向 const 對象 的 const 指針。
2)const成員函數,不能夠修改任何成員變量,除非成員變量被 mutable 修飾符修飾。
1 class constTestClass 2 { 3 public: 4 int var; 5 const int ci; 6 mutable int mci; 7 public: 8 void setVar(int i); 9 void setMci(int i)const; 10 }; 11 void constTestClass::setVar(int i) 12 { 13 var = i; // ok 14 mci = i; // ok 15 ci = i; // Error:ci是一個const對象不能修改 16 } 17 void constTestClass::setMci(int i)const 18 { 19 var = i; // ok 20 mci = i; // ok mutable成員變量可以被const成員函數修改 21 ci = i; // Error 22 }
在成員函數調用的過程中,都有一個 this 指針被當做參數隱性地傳遞給成員函數(可能通過棧,也可能通過CPU寄存器)。這個this指針,指向調用這個函數的對象(這樣,成員函數才能找到成員變量的地址,從而對其進行操作)。這個this指針,是個 const指針,不能修改其指向(你不希望這個對象的函數,修改了那個對象的成員變量,對吧?)。
傳遞給const成員函數的this指針,指向一個const對象。也就是說,在const成員函數內部,這個this指針是一個指向const對象的const指針。
mutable 修飾符使得const函數的行爲有了一些靈活性。相當於提醒編譯器,這個成員變量比較特殊,就不要進行任何只讀檢查了。
爲什麼 const 對象只能夠調用const成員函數呢?,其實是這樣的。由於對象本身通過 const 修飾,那麼指向這個對象的指針也就是指向const對象的const指針了。換句話說,指向這個對象的this指針就是指向const對象的const指針。一般成員函數要求的this指針爲:指向對象的const指針。所以此處發生了參數不匹配,無法進行調用。而 const 成員函數要求的this指針,恰恰是 指向const對象的const指針。所以依然能夠調用。
八、const與函數
將函數的形參用const修飾是希望實參在函數內部不被修改,而一般函數接口可能會遇到以下三種情況:
1,const對象
2,指向const對象的指針
3,綁定const對象的引用
4,返回值是一個const對象
首先,我們看const對象的形參,這種接口用const修飾實際上沒有任何意義,因爲實參在傳遞給實參時是傳遞了一份副本,原實參是不會變化的。
1 int main(void) 2 { 3 int var = 42; 4 fun(var); 5 cout << var << endl; // print 42 6 return 0; 7 } 8 void fun( int i) 9 { 10 i = 10; 11 }
通過上面代碼可以看出,實參如果只能過值進行傳遞,函數接口不用const修改,也不會令實參的值改變。
而通過指針或引用傳遞給函數時,函數就可以通過形參來改變實參的值,這裏如果需要對實參進行保護,則需要在函數接口聲明形參爲指向const類型的指針或引用。
1 void fun( const int* p) 2 { 3 *p = 42; // error 4 int var = 10; 5 p = &var; // 可以改變p本身的值 6 } 7 void fun(const int& p) 8 { 9 p = 42; // error,p是一個指向const對象的引用 10 }
有的時候,我們需要函數的返回值是一個const對象,比如我們考慮一個有理數據類,我們給類定義了一個*的重載。
1 class Rational{ 2 // .... 3 }; 4 const Rational operator* (const Rational& lhs, const Rational& rhs); 5 Rational a, b, c; 6 a*b = c; // Error,因爲左端爲一個const對象
如果上面代碼中重載操作符返回對象不是const類型,則a*b=c這個式子就成立,實際上這與我們的內置類型的算術運算原則違背了,而我們希望我們設計的類的操作意義要像內置內類一樣。
參考博文:
[2]:C++const變量使用技巧總結