C++中的Const 作用

Const 作用

1.   const類型定義:指明變量或對象的值是不能被更新,引入目的是爲了取代預編譯指令

2.   可以保護被修飾的東西,防止意外的修改,增強程序的健壯性。

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

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

  例如:

     #define PI 3.14159         file://常量宏

     const doulbe  Pi=3.14159;  file://此時並未將Pi放入ROM中

        ......

     double i=Pi;             file://此時爲Pi分配內存,以後不再分配!

     double I=PI;               file://編譯期間進行宏替換,分配內存

     double j=Pi;               file://沒有內存分配

     double J=PI;               file://再進行宏替換,又一次分配內存!

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

 

對於基本聲明

1.  const int r=100; //標準const變量聲明加初始化,因爲默認內部連接所以必須被初始化,其作用域爲此文件,編譯器經過類型檢查後直接用100在編譯時替換

 

2.  extend const int r=100; //將const改爲外部連接,作用於擴大至全局,編譯時會分配內存,並且可以不進行初始化,僅僅作爲聲明,編譯器認爲在程序其他地方進行了定義

但是如果外部想鏈接r,不能這樣用

extern const int r=10;  //錯誤!常量不可以被再次賦值

3. const int r[ ]={1,2,3,4};

struct S {int a,b;};

const S s[ ]={(1,2),(3.4)}; //以上兩種都是常量集合,編譯器會爲其分配內存,所以不能在編譯期間使用其中的值,例如:int temp[r[2]];這樣的編譯器會報告不能找到常量表達式

 但是

 const int Max=100;

 int Array[Max]; 

正確。

 還有

 

 定義數組必須用常量,可以用const或者#define定義。 Static 雖然是編譯時確定,也不能用來聲明數組。

 

對於指針和引用

1.   const int *r=&x; //聲明r爲一個指向常量的x的指針,r指向的對象不能被修改,但他可以指向任何地址的常量

pointer const 可以指定普通變量,用改指針不能修改它指向的對象,並不表示指向的對象是const不能被改變,例如:

int i = 10;

const int * p =  &i;

*p = 11; //wrong

 

 i = 11 ; //correct

自己的一個經驗:一個具體的概念可以用範型的概念來賦值,但是一個範型的概念不能用具體的概念來賦值。

我們可以把const指針當成普通指針的父類,因爲普通指針改寫了const屬性,而具有比const指針更多的功能。 這樣的話只有父類指針可以指向子類,而子類指針不能指向父類。

2.   int const *r=&x; //與用法1完全等價,沒有任何區別

3.   int * const r=&x; //聲明r爲一個常量指針,他指向x,r這個指針的指向不能被修改,但他指向的地址的內容可以修改

4.  const int * const r=&x; //綜合1、3用法,r是一個指向常量的常量型指針

5.   const double & v;      該引用所引用的對象不能被更新

 引用必須定義是初始話,而且初始化後這個引用不能指向其他的對象。但是這裏加的const聲明不是這個意思,它是指不能改變v引用對象本身,也就是隻能調用該對象裏面的const成員函數。

 

對於類型檢查

可以把一個非const對象賦給一個指向const的指針,因爲有時候我們不想從這個指針來修改其對象的值;但是不可以把一個const對象賦值給一個非const指針,因爲這樣可能會通過這個指針改變指向對象的值,但也存在使這種操作通過的合法化寫法,使用類型強制轉換可以通過指針改變const對象:

const int r=100;

int * ptr = const_cast<int*>(&r);  //C++標準,C語言使用:int * ptr =(int*)&r;

 

對於字符數組

 

如char * name = “china”; 這樣的語句,在編譯時是能夠通過的,但是”china”是常量字符數組,任何想修改他的操作也能通過編譯但會引起運行時錯誤,如果我們想修改字符數組的話就要使用char name[ ] = “china”; 這種形式。

 

對於函數

1. void Fuction1 ( const int r ); //此處爲參數傳遞const值,意義是變量初值不能被函數改變

2. const int Fuction1 (int); //此處返回const值,意思指返回的原函數裏的變量的初值不能被修改,但是函數按值返回的這個變量被製成副本,能不能被修改就沒有了意義,它可以被賦給任何的const或非const類型變量,完全不需要加上這個const關鍵字。但這隻對於內部類型而言(因爲內部類型返回的肯定是一個值,而不會返回一個變量,不會作爲左值使用),對於用戶自定義類型,返回值是常量是非常重要的,見下麪條款3。

3.  Class CX; //內部有構造函數,聲明如CX(int r =0)

CX  Fuction1 () { return CX(); }

const CX Fuction2 () { return CX(); }

如有上面的自定義類CX,和函數Fuction1()和Fuction2(),我們進行如下操作時:

Fuction1() = CX(1); //沒有問題,可以作爲左值調用

Fuction2() = CX(1); //編譯錯誤,const返回值禁止作爲左值調用。因爲左值把返回值作爲變量會修改其返回值,const聲明禁止這種修改。

4.  函數中指針的const傳遞和返回:

int F1 (const char * pstr); //作爲傳遞的時候使用const修飾可以保證不會通過這個指針來修改傳遞參數的初值,這裏在函數內部任何修改*pstr的企圖都會引起編譯錯誤。

const char * F2 (); //意義是函數返回的指針指向的對象是一個const對象,它必須賦給一個同樣是指向const對象的指針。

const char * const F3(); //比上面多了一個const,這個const的意義只是在他被用作左值時有效,它表明了這個指針除了指向const對象外,它本身也不能被修改,所以就不能當作左值來處理。

5.   函數中引用的const傳遞:

void F1 ( const X& px); //這樣的一個const引用傳遞和最普通的函數按值傳遞的效果是一模一樣的,他禁止對引用的對象的一切修改,唯一不同的是按值傳遞會先建立一個類對象的副本,然後傳遞過去,而它直接傳遞地址,所以這種傳遞比按值傳遞更有效。

**另外只有引用的const傳遞可以傳遞一個臨時對象,因爲臨時對象都是const屬性,且是不可見的,他短時間存在一個局部域中,所以不能使用指針,只有引用的const傳遞能夠捕捉到這個傢伙。

6.  有一點可以注意一下

    const爲函數重載提供了一個參考。

         class A

         {

           ......

           void f(int i)       {......} file://一個函數

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

            ......

          };

 

        關於函數overloading, 不能根據返回值類型來確定

 

        double max( int a, int b);

        int        max( int a, int b);

        也不能根據參數的默認值來判斷

        int max( int a, int b);

        int max( int a, int b, int c=12);

        一句話不能讓編譯器有多個選擇就ok了

 

對於類

1.  首先,對於const的成員變量,只能在構造函數裏使用初始化成員列表來初始化,試圖在構造函數體內進行初始化const成員變量會引起編譯錯誤。初始化成員列表形如:

X:: X ( int ir ): r(ir) {} //假設r是類X的const成員變量

2.  const成員函數。提到這個概念首先要談到const對象,正象內置類型能夠定義const對象一樣(const int r=10;),用戶自定義類型也可以定義const對象(const X px(10);),編譯器要保證這個對象在其生命週期內不能夠被改變。如果你定義了這樣的一個const對象,那麼對於這個對象的一切非const成員函數的調用,編譯器爲了保證對象的const特性,都會禁止並在編譯期間報錯。所以如果你想讓你的成員函數能夠在const對象上進行操作的話,就要把這個函數聲明爲const成員函數。假如f( )是類中的成員函數的話,它的聲明形如:

int f( ) const; //const放在函數的最後,編譯器會對這個函數進行檢查,在這個函數中的任何試圖改變成員變量和調用非const成員函數的操作都被視爲非法

**類的構造和析構函數都不能是const函數。

3.  建立了一個const成員函數,但仍然想用這個函數改變對象內部的數據。這樣的一個要求也會經常遇到,尤其是在一個苛刻的面試考官那裏。首先我們要弄清楚考官的要求,因爲有兩種方法可以實現,如果這位考官要求不改變原來類的任何東西,只讓你從當前這個const成員函數入手,那麼你只有使用前面提到的類型強制轉換方法。實例如下:

//假如有一個叫做X的類,它有一個int成員變量r,我們需要通過一個const成員函數f( )來對這個r進行++r操作,代碼如下

void X::f( ) const

{  (const_cast<X*>(this)) -> ++r;  } //通過this指針進行類型強制轉換實現

另外一種方法就是使用關鍵字:mutable。如果你的成員變量在定義時是這個樣子的:

mutable int r ;

那麼它就告訴編譯器這個成員變量可以通過const成員函數改變。編譯器就不會再理會對他的檢查了。

 

關於const一些問題

 [思考1]: 以下的這種賦值方法正確嗎?

 const A_class* c=new A_class();

 A_class* e = c;

這種方法不正確,因爲聲明指針的目的是爲了對其指向的內容進行改變,而聲明的指針e指向的是一個常量,所以不正確;

[思考2]: 以下的這種賦值方法正確嗎?

 A_class* const c = new A_class();

 A_class* b = c;

這種方法正確,因爲聲明指針所指向的內容可變;

[思考3]: 這樣定義賦值操作符重載函數可以嗎?

const A_class& operator=(const A_class& a);

不正確;在const A_class::operator=(const A_class& a)中,參數列表中的const的用法正確,而當這樣連續賦值的時侯,問題就出現了:A_class a,b,c:(a=b)=c;因爲a.operator=(b)的返回值是對a的const引用,不能再將c賦值給const常量。

 

 

幾點值得討論的地方:
(1)const究竟意味着什麼?
 說了這麼多,你認爲const意味着什麼?一種修飾符?接口抽象?一種新類型?
 也許都是,在Stroustup最初引入這個關鍵字時,只是爲對象放入ROM做出了一種可能,對於const對象,C++既允許對其進行靜態初始化,也允許對他進行動態初始化。理想的const對象應該在其構造函數完成之前都是可寫的,在析夠函數執行開始後也都是可寫的,換句話說,const對象具有從構造函數完成到析夠函數執行之前的不變性,如果違反了這條規則,結果都是未定義的!雖然我們把const放入ROM中,但這並不能夠保證const的任何形式的墮落,我們後面會給出具體的辦法。無論const對象被放入ROM中,還是通過存儲保護機制加以保護,都只能保證,對於用戶而言這個對象沒有改變。換句話說,廢料收集器(我們以後會詳細討論,這就一筆帶過)或數據庫系統對一個const的修改怎沒有任何問題。
(2)位元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的重新解釋。
(3)放在類內部的常量有什麼限制?
 看看下面這個例子:
    class A
    {
      private:
           const int c3 = 7;           // ???
          static int c4 = 7;          // ???
            static const float c5 = 7;  // ???
          ......
     };
你認爲上面的3句對嗎?呵呵,都不對!使用這種類內部的初始化語法的時候,常量必須是被一個常量表達式初始化的整型或枚舉類型,而且必須是static和const形式。這顯然是一個很嚴重的限制!
 那麼,我們的標準委員會爲什麼做這樣的規定呢?一般來說,類在一個頭文件中被聲明,而頭文件被包含到許多互相調用的單元去。但是,爲了避免複雜的編譯器規則,C++要求每一個對象只有一個單獨的定義。如果C++允許在類內部定義一個和對象一樣佔據內存的實體的話,這種規則就被破壞了。
(4)如何初始化類內部的常量?
 一種方法就是static 和 const 並用,在內部初始化,如上面的例子;
另一個很常見的方法就是初始化列表:
      class A
      {
          public:
                A(int i=0):test(i) {}
          private:
                const int i;
       };
       還有一種方式就是在外部初始化,例如:
      class A
      {
          public:
                A() {}
          private:
                static const int i;  file://注意必須是靜態的!
       };
          const int A::i=3;
(5)常量與數組的組合有什麼特殊嗎?
  我們給出下面的代碼:
           const int size[3]={10,20,50};
           int array[size[2]];
有什麼問題嗎?對了,編譯通不過!爲什麼呢?
 const可以用於集合,但編譯器不能把一個集合存放在它的符號表裏,所以必須分配內存。在這種情況下,const意味着“不能改變的一塊存儲”。然而,其值在編譯時不能被使用,因爲編譯器在編譯時不需要知道存儲的內容。自然,作爲數組的大小就不行了:)
 你再看看下面的例子:
       class A
       {
          public:
                A(int i=0):test[2]({1,2}) {} file://你認爲行嗎?
          private:
                const int test[2];
        };
vc6下編譯通不過,爲什麼呢?
關於這個問題,前些時間,njboy問我是怎麼回事?我反問他:“你認爲呢?”他想了想,給出了一下解釋,大家可以看看:我們知道編譯器堆初始化列表的操作是在構造函數之內,顯式調用可用代碼之前,初始化的次序依據數據聲明的次序。初始化時機應該沒有什麼問題,那麼就只有是編譯器對數組做了什麼手腳!其實做什麼手腳,我也不知道,我只好對他進行猜測:編譯器搜索到test發現是一個非靜態的數組,於是,爲他分配內存空間,這裏需要注意了,它應該是一下分配完,並非先分配test[0],然後利用初始化列表初始化,再分配test[1],這就導致數組的初始化實際上是賦值!然而,常量不允許賦值,所以無法通過。
呵呵,看了這一段冠冕堂皇的話,真讓我笑死了!njboy別怪我揭你短呀:)我對此的解釋是這樣的:C++標準有一個規定,不允許無序對象在類內部初始化,數組顯然是一個無序的,所以這樣的初始化是錯誤的!對於他,只能在類的外部進行初始化,如果想讓它通過,只需要聲明爲靜態的,然後初始化。
 這裏我們看到,常量與數組的組合沒有什麼特殊!一切都是數組惹的禍!
(6)this指針是不是const類型的?
 this指針是一個很重要的概念,那該如何理解她呢?也許這個話題太大了,那我們縮小一些:this指針是個什麼類型的?這要看具體情況:如果在非const成員函數中,this指針只是一個類類型的;如果在const成員函數中,this指針是一個const類類型的;如果在volatile成員函數中,this指針就是一個volatile類類型的。
(7)const到底是不是一個重載的參考對象?
        先看一下下面的例子:
        class A
         {
                  ......
                void f(int i)       {......} file://一個函數
                void f(int i) const {......} file://上一個函數的重載
                ......
          };
        上面是重載是沒有問題的了,那麼下面的呢?
         class A
         {
                ......
                void f(int i)       {......} file://一個函數
                void f(const int i) {......} file://?????
                  ......
         };這個是錯誤的,編譯通不過。那麼是不是說明內部參數的const不予重載呢?再看下面的例子:
        class A
         {
                ......
                void f(int& )       {......} file://一個函數
                void f(const int& ) {......} file://?????
                ......
         };
 這個程序是正確的,看來上面的結論是錯誤的。爲什麼會這樣呢?這要涉及到接口的透明度問題。按值傳遞時,對用戶而言,這是透明的,用戶不知道函數對形參做了什麼手腳,在這種情況下進行重載是沒有意義的,所以規定不能重載!當指針或引用被引入時,用戶就會對函數的操作有了一定的瞭解,不再是透明的了,這時重載是有意義的,所以規定可以重載。
(8)什麼情況下爲const分配內存?
       以下是我想到的可能情況,當然,有的編譯器進行了優化,可能不分配內存。
        A、作爲非靜態的類成員時;
        B、用於集合時;
        C、被取地址時;
        D、在main函數體內部通過函數來獲得值時;
        E、const的 class或struct有用戶定義的構造函數、析構函數或基類時;。
        F、當const的長度比計算機字長還長時;
        G、參數中的const;
        H、使用了extern時。
        不知道還有沒有其他情況,歡迎高手指點:)       
(9)臨時變量到底是不是常量?
很多情況下,編譯器必須建立臨時對象。像其他任何對象一樣,它們需要存儲空間而且必須被構造和刪除。區別是我們從來看不到編譯器負責決定它們的去留以及它們存在的細節。對於C++標準草案而言:臨時對象自動地成爲常量。因爲我們通常接觸不到臨時對象,不能使用與之相關的信息,所以告訴臨時對象做一些改變有可能會出錯。當然,這與編譯器有關,例如:vc6、vc7都對此作了擴展,所以,用臨時對象做左值,編譯器並沒有報錯。
(10)與static搭配會不會有問題?
        假設有一個類:
        class A
        {
         public:
                ......
                static void f() const { ......}
                ......
         };
  我們發現編譯器會報錯,因爲在這種情況下static不能夠與const共存!
  爲什麼呢?因爲static沒有this指針,但是const修飾this指針,所以...
 (11)如何修改常量?
  有時候我們卻不得不對類內的數據進行修改,但是我們的接口卻被聲明瞭const,那該怎麼處理呢?我對這個問題的看法如下:
  1)標準用法:mutable
              class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const { test=i; }
               private:
                      mutable int test;   file://這裏處理!
               };
2)強制轉換:const_cast
               class A
               {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { const_cast <int>(test)=i; }//這裏處理!
               private:
                      int test;  
               }
3)靈活的指針:int*
               class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { *test=i; }
               private:
                      int* test;   file://這裏處理!
               };
4)未定義的處理
              class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { int *p=(int*)&test; *p=i; }//這裏處理!
               private:
                      int test;  
               };
                注意,這裏雖然說可以這樣修改,但結果是未定義的,避免使用!
5)內部處理:this指針
              class A
              {
               public:
                      A(int i=0):test(i)        { }
                      void SetValue(int i)const
                      { ((A*)this)->test=i; }//這裏處理!
               private:
                      int test;  
               };
             6)最另類的處理:空間佈局
               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;                 file://此處改變了test的值!
                    return 0;
                 }
雖然我給出了6中方法,但是我只是想說明如何更改,但出了第一種用法之外,另外5種用法,我們並不提倡,不要因爲我這麼寫了,你就這麼用,否則,我真是要誤人子弟了:)  
(12)最後我們來討論一下常量對象的動態創建。
  既然編譯器可以動態初始化常量,就自然可以動態創建,例如:
  const int* pi=new const int(10);
這裏要注意2點:
 1)const對象必須被初始化!所以(10)是不能夠少的。
 2)new返回的指針必須是const類型的。
 那麼我們可不可以動態創建一個數組呢?答案是否定的,因爲new內置類型的數組,不能被初始化。
 這裏我們忽視了數組是類類型的,同樣對於類內部數組初始化我們也做出了這樣的忽視,因爲這涉及到數組的問題,我們以後再討論。
 


          

Reference:

http://blog.csdn.net/boox/archive/2005/05/30/384509.aspx

http://www.bloghome.cn/posts/61287.html

http://blog.csdn.net/hwalk/archive/2006/05/20/746471.aspx

http://blog.csdn.net/hustli/archive/2003/06/30/19342.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章