標籤:

C++筆記 · C++關鍵字 - const

一. 基本描述

定義變數時的限定符,表示變數值不能改變。

const int bufSize = 512;bufSize = 512; // 錯誤:試圖向const對象寫值

由於const一旦創建就不可更改,所以const對象必須初始化(否則定義一個默認值且不可修改的變數沒有任何意義)。

const int i = get_size(); // 正確const int j = 42; // 正確const int k; // 錯誤:未初始化

使用值傳遞初始化時,被初始化的對象是否為const與初始化對象是否為const無關

int i = 0;int j = i; // 正確const int k = i; // 正確const int x = 0;int y = x; // 正確const int z = x; // 正確

二. const初始化引用時的例外

C++規定引用類型必須與被引用對象一致:

int i = 2;double &j = i; // 錯誤:引用類型與對象類型不一致

還規定引用必須綁定到左值:

int &i = 2; // 錯誤:不允許用右值初始化int &j = a * 2 // 錯誤:不允許用表達式初始化

但是用const初始化引用時會有例外:

const引用類型與對象類型不一致(但可以轉化):

int i = 2;const double &j = i; // 正確:j是常量引用

const引用綁定到一個非左值上(類型一致或可以轉化)

const int &i = 2; // 正確:i是常量引用const int &j = a * 2 // 正確:j是常量引用

原因在於,const引用將會額外創建一個臨時變數,並綁定上去。

C++支持這種做法的目的在於,既然不能通過const引用修改對象值,那麼額外創建一個常量和直接綁定對象並沒有什麼區別,所以乾脆讓const引用支持這種非常規做法。

三. 頂層const和底層const

通常在指針/引用與const符同時使用時會用到這個概念。

1. const與指針

指針本身是一個獨立的對象,它又可以指向另一個對象

所以指針和const同時使用時,有兩種情況:

int i = 0;int *const j = &i; // 指針j指向i,const修飾指針j本身,所以j的地址值不允許修改,但是可以通過j修改i的值const int *k = &i; // 指針k指向i,const修飾k指向的i,所以k的地址值可以修改,但是不可以通過k修改i的值

以上兩種情況,修飾指針j本身的const稱為頂層const,修飾k所指向變數i的const成為底層const。底層const與頂層const是兩個互相獨立的修飾符,互不影響。

2. const與引用

引用一旦初始化,就不能再修改(綁定),所以引用本身就具有"const"的性質。

與指針相比,引用相當於內置了頂層const

所以使用引用時,就只需考慮是否為底層const:

int i = 0;const int &j = i; // j為綁定到i的const引用,不允許使用j來修改i

3. 其他

(1). 可以將底層const的指針(或引用)指向(或綁定)到非const對象,但不允許非底層const的指針(或引用)指向(或綁定)到const對象。 (即:const對象不允許通過任何方式(指針/引用)被修改。)

(2). 修飾值本身的const均為頂層const:

const int i = 0; // 頂層const;

四. const與函數

1. 值傳遞的const形參

void fcn(const int i) { /* ... */ }

這個函數中,變數i為值傳遞形參,根據值傳遞的初始化規則,形參i是否為const與傳入的實參是否為const是完全無關的。這裡的const僅表示i在函數體中不允許修改。

如下的調用均為合法調用:

int x = 0;fcn(x);const int y = 0;fcn(y);

因為值傳遞的const形參在調用上與非const形參沒有區別,所以僅僅使用const無法區分參數類別,所以無法實現函數重載,如下的重載是錯誤的:

void fcn1(const int i) { /* ... */ }void fcn1(int i) { /* ... */ } // 錯誤:重複定義函數,不能實現重載

2. const指針/引用的形參

對於頂層const的指針,與上一小節一樣,其const性質與實參無關,頂層const僅表示指針/引用本身在函數體中不允許修改。

所以我們只需要討論底層const的指針/引用。

void fcn2(const int &x) { /* ... */ } // 接受const或非const的int引用,但是不允許通過x修改傳入的對象void fcn2(const int *y) { /* ... */ } // 接受const或非const的int指針,但是不允許通過y修改傳入的對象

如上兩個函數都定義了底層const的形式參數,它們可以接受const或非const對象,但是不能在函數體內修改這些對象。

所以如下的調用都是合法的:

int i = 0;fcn2(i); // 正確:調用第一個函數fcn2(&i); // 正確:調用第二個函數const int j = 0;fcn2(j); // 正確:調用第一個函數fcn2(&j); // 正確:調用第二個函數

由於底層const描述實參性質,可以在調用時區分const,所以使用底層const的指針/引用可以實現函數重載

void fcn3(int &x) { /* ... */ } void fcn3(const int &x) { /* ... */ } // 新函數,作用於const的引用

所以可以分別調用兩個函數:

int i = 0;fcn3(i); // 正確:調用第一個函數const int j = 0;fcn3(j); // 正確:調用第二個函數

當傳遞非常量對象時,編譯器會優先調用非常量版本的函數。

3. 總結

  • 頂層const的形式參數不能實現函數重載,但底層const形參可以
  • 當函數不修改參數值時,儘可能將形式參數定義為(底層)const參數,因為(底層)const參數可以接受常量與非常量對象,但非(底層)const參數只能接受非常量對象。

五. const與類

1. const與類的成員變數

一個類通常包含成員函數和成員變數,對象的const修飾表示該對象的成員變數不允許被修改:

class Number{public: int number = 0;};int main(){ const Number n; n.number = 1; // 錯誤,n為const對象,不允許被修改 return 0;}

無論類的成員變數本身是否為const,只要對象聲明為const,均不允許被修改。

2. const與類的成員函數

當對象被聲明為const時,該對象不能調用非const函數:

class Number{public: void set(int num) { number = num; } int get() { return number; } int number = 0;};int main(){ const Number n; n.set(1); // 錯誤,n為const對象,不能調用非const函數 cout << n.get() << endl; // 錯誤,原因同上 return 0;}

const對象不能調用非const函數的原因顯而易見:非const函數可能修改成員變數。

將成員函數聲明為const函數,則可以被const函數調用,聲明const函數的方法為在其參數列表後添加const關鍵字:

class Number{public: void set(int num) const { number = num; } // 錯誤:const函數不允許修改成員變數 int get() const { return number; } // 正確:函數沒有修改成員變數,被聲明為const函數 int number = 0;};int main(){ const Number n; n.set(1); // 錯誤,const函數不允許修改成員變數 cout << n.get() << endl; // 正確,const對象可以調用const函數 return 0;}

並非所有成員函數都可以被聲明為const函數,C++會在編譯時檢查被聲明為const的函數是否修改了成員變數,若是,則報錯,編譯不通過。

與底層const形參一樣,const成員函數也可以實現重載

class T{public: int fcn() { return 1; } int fcn() const { return 2; } // 正確:定義了可以重載的新函數};int main(){ T t1; cout << t1.fcn() << endl; // 調用第一個函數,輸出"1" const T t2; cout << t2.fcn() << endl; // 調用第二個函數,輸出"2" return 0;}

同樣,當非常量對象調用函數時,編譯器會優先調用非常量版本的函數。

3. 總結

  • 當函數不修改成員變數時,儘可能的將函數聲明為const函數,因為const函數可以被非const對象和const對象調用,而非const函數只能被非const對象調用。
  • const函數並不意味著數據安全,雖然不能通過const函數修改成員變數,但是這樣的const僅為頂層const,若成員變數包含非底層const的指針/引用,則依然可以通過這些指針/引用修改其指向/綁定的對象。

4. const成員函數實現機制

一個類包含成員變數和成員函數,更簡單一點,一個類包含數據和代碼。對象是類的實例,一個類可以構造許多對象,對象們的數據(成員變數)各自獨立,而代碼(成員函數)共用一份。

通常,我們調用成員函數和調用成員變數的方式類似:

Number n;n.number; // 調用成員變數n.set(2); // 調用成員函數

實際上,由於成員函數共享,所以調用成員函數的機制與調用成員變數的機制略有區別,簡而言之,編譯器先找到類,然後調用類的函數,再隱式地在參數列表中傳入一個對象指針(this指針),表示需要操作該對象。

所以,成員函數set()的聲明和定義可以理解為:

void Number::set(Number *const this, int num) { number = num; } // 僅作為參考,實際上,C++規定顯式定義this指針為非法操作

即,任何一個成員函數都隱式地接受了一個指向對象的this指針。

而在成員函數中對成員變數的默認調用實際上都是使用this指針的隱式調用,比如 number = num 等價於 this->number = num。

那麼,C++編譯器檢查const函數是否修改了成員變數的機制就很好理解了。

只需要將this指針定義為底層const,以表示不能通過改指針修改成員變數:

void Number::set(const Number *const this, int num) { number = num; } // 僅作為參考,實際上,C++規定顯式定義this指針為非法操作

第一個const聲明了this指針為底層const,而函數中的 number = num 實際為 this->number = num,由於this為底層const,所不能通過this修改number,該操作非法,所以該函數不能聲明為const。

本質上,const函數還是通過傳統的const機制逐條語句檢查來實現的。


推薦閱讀:

有沒有男生干過寫程序追女生的事情?都寫了什麼?
知乎的搜索演算法是什麼樣的?為什麼搜索的結果不如人意?

TAG:C | 编程 | CC |