1. 程式人生 > >C/C++中const探討

C/C++中const探討

學習const之前先來回顧三個概念,常量,變數,常變數

常量:常量是程式執行過程中其值不能改變的量,我們任意說一個數字,一個單詞都是一個常量,例如3就是常量,apple也是常量,常量就相當於數學上常數的概念,只不過計算機資料型別並不是只有數字,所以稱之為常量。根據資料型別的不同又分為數值常量,字元常量,字串常量和符號常量。

變數:變數是在程式執行過程中其值可以改變的量,變數有變數型別,變數名和變數值三個屬性。變數型別就是該變數的資料型別,變數名代表該儲存空間存放的值。

常變數:常變數是在C語言中是用const修飾的變數,常變數值不能被改寫,只能使用不能改寫,除非用友元函式呼叫。

#define  PI   3.1415             //常量

int a = 10;                      //變數

const int b = 10;                //常變數

-------------------------------------------------------------------const----------------------------------------------------------------------------------------------

C語言中的const

       const 是 constant 的縮寫,是恆定不變的意思,也翻譯為常量和常數等 。正是因為這一點,很多人認為被const修飾的值是常量。這是不精確的,精確來說應該是隻讀的變數,其值在編譯時不能被使用,因為編譯器在編譯時不知道儲存的內容。const推出的初始目的,正是為了取代預編譯指令,消除了它的缺點,同時繼承了它的優點。

1 const修飾只讀變數

定義const只讀變數,具有不可變性。例如:

const int a = 10;       //a = 10
  //  a++;                    //error,表示式必須是可修改的左值
  //  int &a1 = a;            //error,將int&繫結在const int上,限定符丟失
const int &b = a;       //a = 10,b = 10
int *c = (int *)&a;     //c = 0x0123FFDE8,a = 10,b = 10,*c = 10
*c++;                   //c = 0x0123FFDEC,a = 10,b = 10,*c = -858944430
*c--;                   //c = 0x0123FFDE8,a = 10,b = 10,*c = 10
(*c)++;                 //c = 0x0123FFDE8,a = 10,b = 11,*c = 11

     const int Max =10;

     int arr[Max] = {0};

在vs2017編譯器裡分別建立.c和.cpp檔案並測試,在.c檔案中,編譯器報錯,而.cpp中順利執行。定義一個數組必須指定元素的個數,這也從側面證實了c中,const修飾的Max仍然是變數,只不過是只讀屬性罷了;而在C++中拓展了const的含義。

2 節省空間,避免不必要的記憶體分配,提高效率

編譯器通常不為普通 const 只讀變數分配儲存空間,而是將它們儲存在符號表中,這使得它成為編譯期間的一個值,沒有了儲存和讀記憶體的操作,使得效率提高。

#define PI 3              //巨集常量
const int p = 5;          //此時並未將p的值存放在記憶體中
...
int i = PI;               //此時為p分配記憶體,以後不再分配
int I = P;                //預編譯期間進行巨集替換,分配記憶體
int j = PI;               //沒有分配記憶體
int J = p;                //再一次進行巨集替換,分配記憶體

const 定義的只讀變數從彙編角度看,只是給出了相對應的記憶體地址,而不是像#define 一樣給出立即數,所以,const 定義的只讀變數在程式執行過程中只有一份備份(因為它是全域性的只讀變數,存放在靜態區), 而 #define 定義的巨集常量在記憶體中有若干備份。# define 巨集是在預編譯階段進行替換,而const 修飾的只讀變數是在編譯的時候確定其值。 #define沒有型別檢查,而const 修飾的具有特定型別。

3 修飾一般變數

一般變數是指簡單型別的只讀變數。這種只讀變數在定義時,修飾符 const 可以用在型別說明符之前,也可以在之後。

例如:

int const i = 2;
const int i = 2;

4 修飾陣列

定義一個只讀陣列可以如下:

int const arr[5] = {1,2,3,4,5};
const int arr[5] = {1,2,3,4,5};

5 修飾指標

const int *p;             //const修飾*p,p是指標p可變,p指向的物件*p不可變
int const *p;             //const修飾*p,p是指標p可變,p指向的物件*p不可變
int *const p;             //const修飾p,p是指標p不可變,p指向的物件*p可變
const int * const p;      //前一個const修飾*p,後一個const修飾p,指標p和p指向的物件都不可變

6 修飾函式的引數

const 修飾符可以修飾函式的引數,當不希望這個引數值在函式體內被意外改變時使用。

例如:

void fun(const int *p);

告訴編譯器 *p在函式體內不能改變,從而防止了使用者的一些無意的修改。

7 修飾函式的返回值

const 可以修飾函式的返回值,返回值不可被修改。

例如:

const int fun(void);

在另一檔案中引用const只讀變數:

extern const int i;              //正確的宣告
extern const int j = 10;         //錯誤,只讀變數的值不能被改變

C++中const

  常變數:  const 型別說明符 變數名

  常引用:  const 型別說明符 &引用名

  常物件:  類名 const 物件名

  常成員函式:  類名::fun(形參) const

  常陣列:  型別說明符 const 陣列名[大小]    

  常指標:  const 型別說明符* 指標名 ,型別說明符* const 指標名

首先提示的是:在常變數(const 型別說明符 變數名)、常引用(const 型別說明符 &引用名)、常物件(類名 const 物件名)、 常陣列(型別說明符 const 陣列名[大小]), const” 與 “型別說明符”或“類名”(其實類名是一種自定義的型別說明符) 的位置可以互換。如:

     const int a=5; 與 int const a=5; 等同

     類名 const 物件名 與 const 類名 物件名 等同

1 常量

取代了C中的巨集定義,宣告時必須進行初始化(!c++類中則不然)。const限制了常量的使用方式,並沒有描述常量應該如何分配。如果編譯器知道了某const的所有使用,它甚至可以不為該const分配空間。最簡單的常見情況就是常量的值在編譯時已知,而且不需要分配儲存。―《C++ Program Language》
    用const宣告的變數雖然增加了分配空間,但是可以保證型別安全。
    C標準中,const定義的常量是全域性的,C++中視宣告位置而定。

2 指標和常量

使用指標時涉及到兩個物件:該指標本身和被它所指的物件。將一個指標的宣告用const“預先固定”將使那個物件而不是使這個指標成為常量。要將指標本身而不是被指物件宣告為常量,必須使用宣告運算子*const。
所以出現在 * 之前的const是作為基礎型別的一部分:

char *const cp;        //到char的const指標
char const *pc1;       //到const char的指標
const char *pc2;       //到const char的指標(後兩個宣告是等同的)


    從右向左讀的記憶方式:
cp is a const pointer to char.   故pc不能指向別的字串,但可以修改其指向的字串的內容
pc2 is a pointer to const char. 故*pc2的內容不可以改變,但pc2可以指向別的字串

且注意:允許把非 const 物件的地址賦給指向 const 物件的指標,不允許把一個 const 物件的地址賦給一個普通的、非 const 物件的指標。

3 const修飾函式傳入引數

   將函式傳入引數宣告為const,以指明使用這種引數僅僅是為了效率的原因,而不是想讓呼叫函式能夠修改物件的值。同理,將指標引數宣告為const,函式將不修改由這個引數所指的物件。
    通常修飾指標引數和引用引數:

void Fun(const A *in);   //修飾指標型傳入引數
void Fun(const A &in);   //修飾引用型傳入引數

4 修飾函式返回值

可以阻止使用者修改返回值。返回值也要相應的付給一個常量或常指標。

5 const修飾成員函式(c++特性)

const物件只能訪問const成員函式,而非const物件可以訪問任意的成員函式,包括const成員函式;
const物件的成員是不能修改的,而通過指標維護的物件確實可以修改的;
const成員函式不可以修改物件的資料,不管物件是否具有const性質。編譯時以是否修改成員資料為依據進行檢查。


說明

    常量函式是C++對常量的一個擴充套件,它很好的確保了C++中類的封裝性。在C++中,為了防止類的資料成員被非法訪問,將類的成員函式分成了兩類,一類是常量成員函式(也被稱為觀察者);另一類是非常量成員函式(也被成為變異者)。在一個函式的簽名後面加上關鍵字const後該函式就成了常量函式。對於常量函式,最關鍵的不同是編譯器不允許其修改類的資料成員。例如:  

class Test
{

public:

    void func() const;

private:

    int intValue;
};
void Test::func() const
{

    intValue = 100;
}

    上面的程式碼中,常量函式func函式內試圖去改變資料成員intValue的值,因此將在編譯的時候引發異常。

    當然,對於非常量的成員函式,我們可以根據需要讀取或修改資料成員的值。但是,這要依賴呼叫函式的物件是否是常量。通常,如果我們把一個類定義為常量,我們的本意是希望他的狀態(資料成員)不會被改變。那麼,如果一個常量的物件呼叫它的非常量函式會產生什麼後果呢?看下面的程式碼:

class Fred
{

public:

    void inspect() const;

    void mutate();
};
void UserCode(Fred& changeable, const Fred& unChangeable)
{

    changeable.inspect(); // 正確,非常量物件可以呼叫常量函式。

    changeable.mutate(); // 正確,非常量物件也允許修改呼叫非常量成員函式修改資料成員。

    unChangeable.inspect(); // 正確,常量物件只能呼叫常量函式。因為不希望修改物件狀態。

    unChangeable.mutate(); // 錯誤!常量物件的狀態不能被修改,而非常量函式存在修改物件狀態的可能

}

    從上面的程式碼可以看出,由於常量物件的狀態不允許被修改,因此,通過常量物件呼叫非常量函式時將會產生語法錯誤。實際上,我們知道每個成員函式都有一個隱含的指向物件本身的this指標。而常量函式則包含一個this的常量指標。如下: 

void inspect(const Fred* this) const;
void mutate(Fred* this);

     也就是說對於常量函式,我們不能通過this指標去修改物件對應的記憶體塊。但是,在上面我們已經知道,這僅僅是編譯器的限制,我們仍然可以繞過編譯器的限制,去改變物件的狀態。看下面的程式碼:  

class Fred
{
public:

    void inspect() const;
private:

    int intValue;
};

void Fred::inspect() const
{

    cout << "At the beginning. intValue = "<< intValue << endl;

    // 這裡,我們根據this指標重新定義了一個指向同一塊記憶體地址的指標。

    // 通過這個新定義的指標,我們仍然可以修改物件的狀態。

    Fred* pFred = (Fred*)this;

    pFred->intValue = 50;

    cout << "Fred::inspect() called. intValue = "<< intValue << endl;

}
int main()
{

    Fred fred;

    fred.inspect();

    return 0;

}

    上面的程式碼說明,只要我們願意,我們還是可以通過常量函式修改物件的狀態。同理,對於常量物件,我們也可以構造另外一個指向同一塊記憶體的指標去修改它的狀態。

    關於常量函式,還有一個問題是過載。  

 

#include <iostream>
#include <string>
using namespace std;
class Fred
{
public:

    void func() const;

    void func();

};

void Fred::func() const
{

    cout << "const function is called."<< endl;

}
void Fred::func()
{

    cout << "non-const function is called."<< endl;
}
void UserCode(Fred& fred, const Fred& cFred)
{

    cout << "fred is non-const object, and the result of fred.func() is:" << endl;

    fred.func();

    cout << "cFred is const object, and the result of cFred.func() is:" << endl;

    cFred.func();
}
int main()
{

    Fred fred;

    UserCode(fred, fred);

    return 0;
}

    輸出結果為:

    fred is non-const object, and the result of fred.func() is:

    non-const function is called.

    cFred is const object, and the result of cFred.func() is:

    const function is called.

    從上面的輸出結果,我們可以看出。當存在同名同參數和返回值的常量函式和非常量函式時,具體呼叫哪個函式是根據呼叫物件是常量對像還是非常量物件來決定的。常量物件呼叫常量成員;非常量物件呼叫非常量的成員。

    總之,我們需要明白常量函式是為了最大程度的保證物件的安全。通過使用常量函式,我們可以只允許必要的操作去改變物件的狀態,從而防止誤操作對物件狀態的破壞。但是,就像上面看見的一樣,這樣的保護其實是有限的。關鍵還是在於我們開發人員要嚴格的遵守使用規則。另外需要注意的是常量物件不允許呼叫非常量的函式。這樣的規定雖然很武斷,但如果我們都根據原則去編寫或使用類的話這樣的規定也就完全可以理解了。

C和C++中const的區別

1. C++中的const正常情況下是看成編譯期的常量,編譯器並不為const分配空間,只是在編譯的時候將期值儲存在符號表中,並在適當的時候摺合在程式碼中.所以,以下程式碼:

int main()
{
    const int a = 1;
    const int b = 2;
    int array[ a + b ] = {0};
    for (int i = 0; i < sizeof array / sizeof *array; i++)
    {
         printf("%d",array[i]);
    }
    return 0;
}


在.cpp可以通過編譯,並且正常執行.但稍加修改後,放在.c中,便會出現錯誤:
錯誤訊息:
c:\test1\te.c(8): error C2057: 應輸入常數表示式
c:\test1\te.c(8): error C2466: 不能分配常數大小為 0 的陣列
出現這種情況的原因是:在C中,const是一個不能被改變的普通變數,既然是變數,就要佔用儲存空間,所以編譯器不知道編譯時的值.而且,陣列定義時的下標必須為常量.

2. 在C語言中: const int size; 這個語句是正確的,因為它被C編譯器看作一個宣告,指明在別的地方分配儲存空間.但在C++中這樣寫是不正確的.C++中const預設是內部連線,如果想在C++中達到以上的效果,必須要用extern關鍵字.即C++中,const預設使用內部連線.而C中使用外部連線.
(1) 內連線:編譯器只對正被編譯的檔案建立儲存空間,別的檔案可以使用相同的表示符或全域性變數.C/C++中內連線使用static關鍵字指定.
(2) 外連線:所有被編譯過的檔案建立一片單獨儲存空間.一旦空間被建立,聯結器必須解決對這片儲存空間的引用.全域性變數和函式使用外部連線.通過extern關鍵字宣告,可以從其他檔案訪問相應的變數和函式.

3. C++中,是否為const分配空間要看具體情況.如果加上關鍵字extern或者取const變數地址,則編譯器就要為const分配儲存空間.
4. C++中定義常量的時候不再採用define,因為define只做簡單的巨集替換,並不提供型別檢查.