C++中的 const 關鍵字

baixiaofei567發表於2020-12-15

轉載自知乎:https://zhuanlan.zhihu.com/p/37514756

1. 基本描述

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

const int bufSize = 512;
bufSize = 512; // 錯誤:試圖向const物件寫值

由於const一旦建立就不可更改,所以const物件必須初始化(否則定義一個預設值且不可修改的變數沒有任何意義)。

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

使用值傳遞初始化時,被初始化的物件是否為const與初始化物件是否為const無關。也即,const物件與非const物件可以互為初始化。

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

C++規定引用型別必須與被引用物件一致:

int i = 2;
double &j = i; // 錯誤:引用型別與物件型別不一致

C++還規定引用必須繫結到左值:

注:左值和右值的辨別方法是,能取地址的是左值。

int &i = 2;    // 錯誤:不允許用右值初始化
int &j = a * 2 // 錯誤:不允許用表示式初始化,實際上表示式(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引用支援這種非常規做法。

3. 頂層const和底層const

通常在指標/引用與const符同時使用時會用到這個概念。修飾指標本身的const稱為頂層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不可改,i可改
第二行k可改,i不可改

2. const與引用

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

與指標相比,引用相當於內建了頂層const(也就是後置的const)。

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

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

j預設不能改,此時i也不能改

3. 其他

(1). 可以將底層const的指標(或引用)指向(或繫結)到非const物件,但不允許非底層const的指標(或引用)指向(或繫結)到const物件。 (即:const物件不允許通過任何方式(指標/引用)被修改。)
//非底層const的指標和引用可以改變指向物件的值,所以不能繫結到const物件上
(2). 修飾值本身的const均為頂層const:

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

4. 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,實參都不會被修改。因為是值傳遞本身就不能修改實參的值),所以僅僅使用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); // 正確:呼叫第二個函式

注意,當傳遞非常量物件時,編譯器會優先呼叫非常量版本的函式。
總結

  • 頂層const的形式引數不能實現函式過載,但底層const形參可以
  • 當函式不修改引數值時,儘可能將形式引數定義為(底層)const引數。一方面,(底層)const引數可以保護引數物件;另一方面,因為(底層)const引數可以接受常量與非常量物件,但非(底層)const引數只能接受非常量物件。

5. const與類

1. const與類的成員變數

一個類通常包含成員函式和成員變數。

  1. 類的物件的const修飾表示該物件的成員變數不允許被修改。
  2. 無論類的成員變數本身是否為const,只要物件宣告為const,成員變數就不允許被修改。
class Number
{
public:
    int number = 0;
};

int main()
{
    const Number n;
    n.number = 1; // 錯誤,n為const物件,不允許被修改
    return 0;
}

2. const與類的成員函式
當物件被宣告為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;
}
  1. 將成員函式宣告為const函式,則可以被const物件呼叫,宣告const函式的方法為在其引數列表後新增const關鍵字。
  2. const成員函式中不允許修改成員變數。也即,並非所有成員函式都可以被宣告為const函式,C++會在編譯時檢查被宣告為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形參一樣,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. 總結

  1. 當函式不修改成員變數時,儘可能將函式宣告為const函式,因為const函式可以被非const物件和const物件呼叫,而非const函式只能被非const物件呼叫。
  2. 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機制逐條語句檢查來實現的。

相關文章