C/C++中的const的用法

來說下c/c++中的const的用法。
在英語中常數的一種表達是“中的const的”,在編程中可能是借用了這個單詞(我猜的哈)。代表常量,不可改變的意思,在c/c++中,const表示的也是不可改變的意思(當然我們也有一些方法來對它進行改變)。

  • 什么是const?
    常類型是指使用類型修飾符const說明的類型,常類型的變量或對象的值是不能被更新的。(當然,我們可以偷梁換柱進行更新:)

  • 為什么引入const?
    const 推出的初始目的,正是為了取代預編譯指令,消除它的缺點,同時繼承它的優點。

  • cons有什么主要的作用?

    • 可以定義const常量,具有不可變性。
      例如:

    const int Max=100;
    int Array[Max];

    • 便于進行類型檢查,使編譯器對處理內容有更多了解,消除了一些隱患。

    void f(const int i)
    {
    ??.........
    }

    編譯器就會知道i是一個常量,不允許修改;

    • 可以避免意義模糊的數字出現,同樣可以很方便地進行參數的調整和修改。
      同宏定義一樣,可以做到不變則已,一變都變!如果想修改Max的內容,只需要

      const int Max=you want;

    • 可以保護被修飾的東西,防止意外的修改,增強程序的健壯性。
      如果在函數體內修改了i,編譯器就會報錯; 例如

      void f(const int i)
      {
      i=10;//error!
      }

    • 為函數重載提供了一個參考。 關于函數重載等下還要說明

      class A
      { ......
      ??void f(int i)
      ??{
      ??......
      ??} //一個函數
      ??void f(int i) const
      ??{
      ??......
      ??} //上一個函數的重載 ......
      };

    • 可以節省空間,避免不必要的內存分配。

      #define PI 3.14159 //常量宏
      const doulbe Pi=3.14159; //此時并未將Pi放入ROM中 ......
      double i=Pi; //此時為Pi分配內存,以后不再分配!
      double I=PI; //編譯期間進行宏替換,分配內存
      double j=Pi; //沒有內存分配
      double J=PI; //再進行宏替換,又一次分配內存!

      const定義常量從匯編的角度來看,只是給出了對應的內存地址,而不是象#define一樣給出的是立即數,所以,const定義的常量在程序運行過程中只有一份拷貝,而#define定義的常量在內存中有若干個拷貝。

    • 提高了效率。
      編譯器通常不為普通const常量分配存儲空間,而是將它們保存在符號表中,這使得它成為一個編譯期間的常量,沒有了存儲與讀內存的操作,使得它的效率也很高。

  • 如何使用const?

    • 修飾一般常量 一般常量是指簡單類型的常量。這種常量在定義時,修飾符const可以用在類型說明符前,也可以用在類型說明符后。

      int const x=2;
      const int x=2;

    • 修飾常數組 定義或說明一個常數組可采用如下格式:

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

    • 修飾常對象 常對象是指對象常量,定義格式如下:

      class A;
      const A a;

      A const a; 定義常對象時,同樣要進行初始化,并且該對象不能再被更新,修飾符const可以放在類名后面,也可以放在類名前面。

    • 修飾常指針

      const int *A; //const修飾指向的對象,A可變,A指向的對象不可變
      int const *A; //const修飾指向的對象,A可變,A指向的對象不可變
      int *const A; //const修飾指針A, A不可變,A指向的對象可變
      const int *const A;//指針A和A指向的對象都不可變
      - 修飾常引用 使用const修飾符也可以說明引用,被說明的引用為常引用,該引用所引用的對象不能被更新。其定義格式如下:
      > const double & v;
      - 修飾函數的常參數 const修飾符也可以修飾函數的傳遞參數,格式如下:
      > void Fun(const int Var);

      告訴編譯器Var在函數體中的無法改變,從而防止了使用者的一些無意的或錯誤的修改。

    • 修飾函數的返回值: const修飾符也可以修飾函數的返回值,是返回值不可被改變,格式如下:

      const int Fun1();
      const MyClass Fun2();
      - 修飾類的成員函數: const修飾符也可以修飾類的成員函數,格式如下:

      class ClassName
      {
      ?? public:
      ???? int Fun() const;
      ??.....
      };
      //這樣,在調用函數Fun時就不能修改類里面的數據

    • 在另一連接文件中引用const常量

      extern const int i;//正確的引用
      extern const int j=10;//錯誤!

      常量不可以被再次賦值 另外,還要注意,常量必須初始化! 例如:

      const int i=5;

  • 幾點值得討論的地方:

    • const究竟意味著什么?
      說了這么多,const意味著什么?一種修飾符?接口抽象?一種新類型? 也許都是,在Stroustup最初引入這個關鍵字時,只是為對象放入ROM做出了一種可能,對于const對象,C++既允許對其進行靜態初始化,也允許對他進行動態初始化。理想的const對象應該在其構造函數完成之前都是可寫的,在析夠函數執行開始后也都是可寫的,換句話說,const對象具有從構造函數完成到析夠函數執行之前的不變性,如果違反了這條規則,結果都是未定義的!雖然我們把const放入ROM中,但這并不能夠保證const的任何形式的墮落,我們后面會給出具體的辦法。無論const對象被放入ROM中,還是通過存儲保護機制加以保護,都只能保證,對于用戶而言這個對象沒有改變。換句話說,廢料收集器或數據庫系統對一個const的修改怎沒有任何問題。

    • 位元const V.S. 抽象const?
      對于關鍵字const的解釋有好幾種方式,最常見的就是位元const 和 抽象const。下面我們看一個例子:

      class A
      {
      ??public:
      ??......
      ??A f(const A& a);
      ??......
      };

      如果采用抽象const進行解釋,那就是f函數不會去改變所引用對象的抽象值,如果采用位元const進行解釋,那就成了f函數不會去改變所引用對象的任何位元。 我們可以看到位元解釋正是c++對const問題的定義,const成員函數不被允許修改它所在對象的任何一個數據成員。 為什么這樣呢?因為使用位元const有2個好處: 最大的好處是可以很容易地檢測到違反位元const規定的事件:編譯器只用去尋找有沒有對數據成員的賦值就可以了。另外,如果我們采用了位元const,那么,對于一些比較簡單的const對象,我們就可以把它安全的放入ROM中,對于一些程序而言,這無疑是一個很重要的優化方式。(關于優化處理,我們到時候專門進行討論) 當然,位元const也有缺點,要不然,抽象const也就沒有產生的必要了。 首先,位元const的抽象性比抽象const的級別更低!實際上,大家都知道,一個庫接口的抽象性級別越低,使用這個庫就越困難。 其次,使用位元const的庫接口會暴露庫的一些實現細節,而這往往會帶來一些負面效應。所以,在庫接口和程序實現細節上,我們都應該采用抽象const。 有時,我們可能希望對const做出一些其它的解釋,那么,就要注意了,目前,大多數對const的解釋都是類型不安全的,這里我們就不舉例子了,你可以自己考慮一下,總之,我們盡量避免對const的重新解釋。

    • 放在類內部的常量有什么限制?
      看看下面這個例子:

      class A {
      ??private:
      ????const int c3 = 7; // ???
      ????static int c4 = 7; // ???
      ????static const float c5 = 7; // ??? ......
      };

      上面的3句都不對!使用這種類內部的初始化語法的時候,常量必須是被一個常量表達式初始化的整型或枚舉類型,而且必須是static和const形式。這顯然是一個很嚴重的限制! 那么,我們的標準委員會為什么做這樣的規定呢?一般來說,類在一個頭文件中被聲明,而頭文件被包含到許多互相調用的單元去。但是,為了避免復雜的編譯器規則,C++要求每一個對象只有一個單獨的定義。如果C++允許在類內部定義一個和對象一樣占據內存的實體的話,這種規則就被破壞了。

    • 如何初始化類內部的常量?

      • 一種方法就是static 和 const 并用,在內部初始化,如上面的例子;
      • 另一個很常見的方法就是初始化列表:

        class A {
        ??public:
        ????A(int i=0):test(i)
        ????{
        ????}
        ??private:
        ????const int i;
        };
        - 還有一種方式就是在外部初始化,例如:

    > class A

{
??public:
????A()
????{
????}
??private:
????static const int i;//注意必須是靜態的!
};
const int A::i=3;
- 常量與數組的組合有什么特殊嗎? 我們給出下面的代碼:
>const int size[3]={10,20,50};
int array[size[2]];

  有什么問題嗎?對了,編譯通不過!為什么呢? Const可以用于集合,但編譯器不能把一個集合存放在它的符號表里,所以必須分配內存。在這種情況下,const意味著“不能改變的一塊存儲”。然而,其值在編譯時不能被使用,因為編譯器在編譯時不需要知道存儲的內容。自然,作為數組的大小就不行了:)


  > class A

{
??public:
????A(int i=0):test[2]({1,2}) {}//你認為行嗎?
??private:
????const int test[2];
};

  vc6下編譯通不過,為什么呢? 關于這個問題,前些時間,自己猜想了下,給出了一下解釋,大家可以看看:我們知道編譯器堆初始化列表的操作是在構造函數之內,顯式調用可用代碼之前,初始化的次序依據數據聲明的次序。初始化時機應該沒有什么問題,那么就只有是編譯器對數組做了什么手腳!其實做什么手腳,我也不知道,我只好對他進行猜測:編譯器搜索到test發現是一個非靜態的數組,于是,為他分配內存空間,這里需要注意了,它應該是一下分配完,并非先分配test[0],然后利用初始化列表初始化,再分配test[1],這就導致數組的初始化實際上是賦值!然而,常量不允許賦值,所以無法通過。 其實是不對的,正確的是:C++標準有一個規定,不允許無序對象在類內部初始化,數組顯然是一個無序的,所以這樣的初始化是錯誤的!對于他,只能在類的外部進行初始化,如果想讓它通過,只需要聲明為靜態的,然后初始化。 這里我們看到,常量與數組的組合沒有什么特殊!一切都是數組惹的禍!



- this指針是不是const類型的?
this指針是一個很重要的概念,那該如何理解她呢?也許這個話題太大了,那我們縮小一些:this指針是個什么類型的?這要看具體情況:如果在非const成員函數中,this指針只是一個類類型的;如果在const成員函數中,this指針是一個const類類型的;如果在volatile成員函數中,this指針就是一個volatile類類型的。
- const到底是不是一個重載的參考對象? (這個就是剛剛說的要再講下的重載)
先看一下下面的例子:
  > class A
  {

??......
??void f(int i)
??{
??......
??}//一個函數
??void f(int i) const
??{
??......
??}//上一個函數的重載
......
};

  上面是重載是沒有問題的了,那么下面的呢?
  >class A

{
??......
??void f(int i)
????{
????......
????}//一個函數
??void f(const int i)
??{
????......
??}//?????
??......
};

  這個是錯誤的,編譯通不過。那么是不是說明內部參數的const不予重載呢?再看下面的例子:
  > class A
{

??......
??void f(int& )
??{
????......
??}//一個函數
??void f(const int& )
??{
????......
??}//?????
??......
};

  這個程序是正確的,看來上面的結論是錯誤的。為什么會這樣呢?這要涉及到接口的透明度問題。按值傳遞時,對用戶而言,這是透明的,用戶不知道函數對形參做了什么手腳,在這種情況下進行重載是沒有意義的,所以規定不能重載!當指針或引用被引入時,用戶就會對函數的操作有了一定的了解,不再是透明的了,這時重載是有意義的,所以規定可以重載。
  • 什么情況下為const分配內存?
    以下是我想到的可能情況,當然,有的編譯器進行了優化,可能不分配內存。
    A、作為非靜態的類成員時;
    B、用于集合時;
    C、被取地址時;
    D、在main函數體內部通過函數來獲得值時;
    E、const的 class或struct有用戶定義的構造函數、析構函數或基類時;
    F、當const的長度比計算機字長還長時;
    G、參數中的const;
    H、使用了extern時。 不知道還有沒有其他情況,歡迎高手指點:)

  • 臨時變量到底是不是常量?
    很多情況下,編譯器必須建立臨時對象。像其他任何對象一樣,它們需要存儲空間而且必須被構造和刪除。區別是我們從來看不到編譯器負責決定它們的去留以及它們存在的細節。對于C++標準草案而言:臨時對象自動地成為常量。因為我們通常接觸不到臨時對象,不能使用與之相關的信息,所以告訴臨時對象做一些改變有可能會出錯。當然,這與編譯器有關,例如:vc6、vc7都對此作了擴展,所以,用臨時對象做左值,編譯器并沒有報錯。

  • 與static搭配會不會有問題?
    假設有一個類:

    class A
    {
    ??public:
    ????......
    ????static void f() const
    ??????{
    ????????......
    ??????}
    ????......
    };

    我們發現編譯器會報錯,因為在這種情況下static不能夠與const共存! 為什么呢?因為static沒有this指針,但是const修飾this指針,所以...

  • 如何修改常量?
    有時候我們卻不得不對類內的數據進行修改,但是我們的接口卻被聲明了const,那該怎么處理呢?

    • 標準用法:

      mutable class A {
      ??public:
      ????A(int i=0):test(i)
      ????{
      ????}
      ????void SetValue(int i)const
      ??????{
      ????????test=i;
      ??????}
      ????private: mutable int test;//這里處理!
      };

    • 強制轉換:

      const_cast class A {
      ??public:
      ????A(int i=0):test(i)
      ????{
      ????}
      ????void SetValue(int i)const
      ????{
      ??????const_cast (test)=i;
      ????}//這里處理!
      ??private:
      ??????int test;
      };

    • 靈活的指針:

      int* class A
      {
      ??public:
      ????A(int i=0):test(i)
      ????{
      ????}
      ????void SetValue(int i)const
      ????{
      ???? ??test=i;
      ????}
      ??private:
      ????int
      test; //這里處理!
      };

    • 未定義的處理

      class A
      {
      ??public:
      ????A(int i=0):test(i)
      ????{
      ????}
      ????void SetValue(int i)const
      ????{
      ??????int p=(int)&test;
      ??????*p=i;
      ????}//這里處理!
      ??private:
      ????int test;
      };

      注意,這里雖然說可以這樣修改,但結果是未定義的,避免使用!

    • 內部處理:this指針

      class A
      {
      ??public:
      ????A(int i=0):test(i)
      ????{
      ????}
      ????void SetValue(int i)const
      ????{
      ??????((A*)this)->test=i;
      ????}//這里處理!
      ??private:
      ????int test;
      };

    • 最另類的處理:空間布局

      class A
      {
      ??public:
      ????A(int i=0):test(i),c('a')
      ????{
      ???? }
      ??private:
      ????char c;
      ????const int test;
      };
      int main()
      {
      ??A a(3);
      ??A* pa=&a;
      ??char* p=(char)pa;
      ??int
      pi=(int)(p+4);//利用邊緣調整
      ??
      pi=5;//此處改變了test的值!
      ??return 0;
      }

      雖然我給出了6中方法,但是我只是想說明如何更改,但出了第一種用法之外,另外5種用法,我們并不提倡,不要因為我這么寫了,你就這么用,否則,我真是要誤人子弟了:)

  • 最后我們來討論一下常量對象的動態創建。 既然編譯器可以動態初始化常量,就自然可以動態創建,例如:

    const int* pi=new const int(10);

    這里要注意2點:
    1)const對象必須被初始化!所以(10)是不能夠少的。
    2)new返回的指針必須是const類型的。 那么我們可不可以動態創建一個數組呢? 答案是否定的,因為new內置類型的數組,不能被初始化。 這里我們忽視了數組是類類型的,同樣對于類內部數組初始化我們也做出了這樣的忽視,因為這涉及到數組的問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • (1)可以定義 const 常量 (2)const 可以修飾函數的參數、返回值. 詳細內容: 1、什么是const...
    幽鬼09閱讀 714評論 0 4
  • C++中的const關鍵字的用法非常靈活,而使用const將大大改善程序的健壯性,本人根據各方面查到的資料進行總結...
    Charliehhh閱讀 955評論 0 1
  • 題目類型 a.C++與C差異(1-18) 1.C和C++中struct有什么區別? C沒有Protection行為...
    阿面a閱讀 7,717評論 0 10
  • Lua 5.1 參考手冊 by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,918評論 0 38
  • C中常用:“ #define 變量名 變量值”定義一個值替代,然而卻有個致命缺點:缺乏類型檢測機制,這樣預處理在C...
    ater_man閱讀 1,927評論 0 2