C++中const、volatile、mutable用法小結

首先:總結一下有關const 的常見用法

1. const修飾普通變量和指針

  const修飾變量,一般有兩種寫法:
  const TYPE value;
  TYPE const value;

這兩種寫法在本質上是一樣的。它的含義是:const修飾的類型爲TYPE的變量value是不可變的。對於一個非指針的類型TYPE,無論怎麼寫,都是一個含義,即value值不可變。 例如:
    const int nValue;    //nValue是const
    int const nValue;    //nValue是const

但是對於指針類型的TYPE,不同的寫法會有不同情況:
l.  指針本身是常量不可變,指針不能再指向其他變量,但是可以改變指針指向的變量的內容
   (char*) const pContent;
lI. 指針所指向的內容是常量不可變,但是指針本身是變量,可以指向其他變量
    const (char) *pContent;
    (char)const *pContent;
lII.  兩者都不可變,即指針本身是常量,指針所指向的變量的內容也是常量
    const char* const pContent;

識別const到底是修飾指針還是指針所指的對象,還有一個較爲簡便的方法,也就是沿着*號劃一條線:
如果const位於*的左側,則const就是用來修飾指針所指向的變量,即指針指向爲常量;
如果const位於*的右側,const就是修飾指針本身,即指針本身是常量。

2. const修飾引用,稱爲const引用,表明不能修改引用所引用的變量的值

    const int &ref = x;

    int &ref2 = x; //Error, ref2不是常量,可以修改引用所指向的變量x的值,而x實際上是常量,不允許修改,故不合邏輯,這種用法是錯誤的

3.const修飾函數參數

const修飾函數參數是它最廣泛的一種用途,它表示在函數體中不能修改參數的值(包括參數本身的值或者參數其中包含的值)
void function(const int Var);     //傳遞過來的參數在函數內不可以改變(無意義,該函數以傳值的方式調用)
void function(const char* Var);   //參數指針所指內容爲常量不可變
void function(char* const Var);   //參數指針本身爲常量不可變(也無意義,var本身也是通過傳值的形式賦值的)
void function(const Class& Var); //引用參數在函數內不可以改變

參數const通常用於參數爲指針或引用的情況,若輸入參數採用“值傳遞”方式,由於函數將自動產生臨時變量用於複製該參數,該參數本就不需要保護,所以不用const修飾。

4.const修飾類對象/對象指針/對象引用

const修飾類對象表示該對象爲常量對象,其中的任何成員都不能被修改。對於對象指針和對象引用也是一樣。

const修飾的對象,該對象的任何非const成員函數都不能被調用,因爲任何非const成員函數會有修改成員變量的企圖。

例如:
class AAA
{
   void func1();
   void func2() const;
}

const AAA aObj;
aObj.func1(); 錯誤
aObj.func2(); 正確

const AAA* aObj = new AAA();

aObj->func1(); 錯誤

aObj->func2(); 正確

5.const修飾數據成員

const數據成員只在某個對象生存期內是常量,而對於整個類而言卻是可變的。因爲類可以創建多個對象,不同的對象其const數據成員的值可以不同。所以不能在類聲明中初始化const數據成員,因爲類的對象未被創建時,編譯器不知道const 數據成員的值是什麼,例如:
class A
{
    const int size = 100; //錯誤
    int array[size];       //錯誤,未知的size
}
const數據成員的初始化只能在類的構造函數的初始化列表中進行。要想建立在整個類中都恆定的常量,可以用類中的枚舉常量來實現,例如:
class A
{

  enum {size1=100, size2 = 200 };
  int array1[size1];
  int array2[size2];


}
枚舉常量不會佔用對象的存儲空間,他們在編譯時被全部求值。但是枚舉常量的隱含數據類型是整數,其最大值有限,且不能表示浮點數。

6.const修飾成員函數

const修飾類的成員函數,用const修飾的成員函數不能改變對象的成員變量。一般把const寫在成員函數的最後
class A
{
  …
void function()const; //常成員函數, 它不改變對象的成員變量. 也不能調用類中任何非const成員函數。
}

對於const類對象/指針/引用,只能調用類的const成員函數。

7.const修飾成員函數的返回值

(1)、一般情況下,函數的返回值爲某個對象時,如果將其聲明爲const時,多用於操作符的重載。通常,不建議用const修飾函數的返回值類型爲某個對象或對某個對象引用的情況。原因如下:如果返回const對象,或返回const對象的引用,則返回值具有const屬性,返回實例只能訪問類A中的公有(保護)數據成員和const成員函數,並且不允許對其進行賦值操作,這在一般情況下很少用到。

(2)、如果給採用“指針傳遞”方式的函數返回值加const修飾,那麼函數返回值(即指針所指的內容)不能被修改,該返回值只能被賦給加const 修飾的同類型指針:
const char * GetString(void);
如下語句將出現編譯錯誤:
char *str=GetString();
正確的用法是:
const char *str=GetString();

(3)、函數返回值採用“引用傳遞”的場合不多,這種方式一般只出現在類的賻值函數中,目的是爲了實現鏈式表達。如:
class A
{

    A &operate= (const A &other); //賦值函數
}
A a,b,c; //a,b,c爲A的對象

a=b=c;   //正常
(a=B)=c; //不正常,但是合法
若賦值函數的返回值加const修飾,那麼該返回值的內容不允許修改,上例中a=b=c依然正確。(a=b)=c就不正確了。

const常量與define宏定義的區別

I.  編譯器處理方式不同

define宏是在預處理階段展開。

const常量是編譯運行階段使用。

II.  類型和安全檢查不同

define宏沒有類型,不做任何類型檢查,僅僅是展開。

const常量有具體的類型,在編譯階段會執行類型檢查。

III.  存儲方式不同

define宏僅僅是展開,有多少地方使用,就展開多少次,不會分配內存

const常量會在內存中分配(可以是堆中也可以是棧中)。

————————————————————————————————————————————————————————————————————————————

其次:介紹有關 volatile關鍵字的用法

volatile的本意是“易變的”,volatile關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如操作系統、硬件或者其它線程等。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。

當要求使用volatile 聲明的變量的值的時候,系統總是重新從它所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。而且讀取的數據立刻被寄存。例如:

volatile int i=10;

int a = i;

..........//其他代碼,並未明確告訴編譯器,對i進行過操作

int b = i;


volatile 指出i是隨時可能發生變化的,每次使用它的時候必須從i的地址中讀取,因而編譯器生成的彙編代碼會重新從i的地址讀取數據放在b中。而優化做法是,由於編譯器發現兩次從i讀數據的代碼之間的代碼沒有對i進行過操作,它會自動把上次讀的數據放在b中。而不是重新從i裏面讀。這樣以來,如果i是一個寄存器變量或者表示一個端口數據就容易出錯,所以說volatile可以保證對特殊地址的穩定訪問。

注意,在vc6中,一般調試模式沒有進行代碼優化,所以這個關鍵字的作用看不出來。下面通過插入彙編代碼,測試有無volatile關鍵字,對程序最終代碼的影響。首先用classwizard建一個win32 console工程,插入一個voltest.cpp文件,輸入下面的代碼:

#include <stdio.h>

void main()

{

int i=10;
int a = i;
printf("i= %d/n",a);

//下面彙編語句的作用就是改變內存中i的值,但是又不讓編譯器知道

__asm {
  mov dword ptr [ebp-4], 20h
}

int b = i;
printf("i= %d/n",b);
}

然後,在調試版本模式運行程序,輸出結果如下:
i = 10
i = 32

然後,在release版本模式運行程序,輸出結果如下:
i = 10
i = 10

輸出的結果明顯表明,release模式下,編譯器對代碼進行了優化,第二次沒有輸出正確的i值。下面,我們把 i的聲明加上volatile關鍵字,看看有什麼變化:

#include <stdio.h>

void main()

{
volatile int i=10; //表示讓編譯器不對其進行優化
int a = i;
printf("i= %d/n",a);

__asm {

  mov dword ptr [ebp-4], 20h

}
 
int b = i;
printf("i= %d/n",b);
}

分別在調試版本和release版本運行程序,輸出都是:
i = 10
i = 32

這說明這個關鍵字發揮了它的作用!

關於volatile的補充信息:


一個定義爲volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。精確地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器裏的備份。下面是volatile變量的幾個例子:

    1). 並行設備的硬件寄存器(如:狀態寄存器)

    2). 一箇中斷服務子程序中會訪問到的非自動變量(Non-automatic variables)

    3). 多線程應用中被幾個任務共享的變量

    我認爲這是區分C程序員和嵌入式系統程序員的最基本的問題。嵌入式系統程序員經常同硬件、中斷、RTOS等等打交道,所用這些都要求volatile變量。不懂得volatile內容將會帶來災難。假設被面試者正確地回答了這是問題(嗯,懷疑這否會是這樣),我將稍微深究一下,看一下這傢伙是不是直正懂得volatile的重要性:

    1). 一個參數既可以是const還可以是volatile嗎?解釋爲什麼。

    2). 一個指針可以是volatile 嗎?解釋爲什麼。

    3). 下面的函數有什麼錯誤:

         int square(volatile int *ptr)
         {
              return *ptr * *ptr;
         }

    下面是答案:

    1). 是的。一個例子是隻讀的狀態寄存器。它是volatile因爲它可能被意想不到地改變。它是const因爲程序不應該試圖去修改它。

    2). 是的。儘管這並不很常見。一個例子是當一箇中服務子程序修該一個指向一個buffer的指針時。

    3). 這段代碼的有個惡作劇。這段代碼的目的是用來返指針*ptr指向值的平方,但是,由於*ptr指向一個volatile型參數,編譯器將產生類似下面的代碼:

    int square(volatile int *ptr)
    {
         int a,b;
         a = *ptr;
         b = *ptr;
         return a * b;
     }
    由於*ptr的值可能被意想不到地該變,因此a和b可能是不同的。結果,這段代碼可能返不是你所期望的平方值!正確的代碼如下:
     long square(volatile int *ptr)
    {
            int a;
            a = *ptr;
            return a * a;
     }

————————————————————————————————————————————————————————————————————————————

最後:介紹有關 mutable關鍵字的用法

mutalbe的中文意思是“可變的,易變的”,跟constant(既C++中的const)是反義詞。在C++中,mutable也是爲了突破const的限制而設置的。被mutable修飾的變量(mutable只能用於修飾類的非靜態數據成員),將永遠處於可變的狀態,即使在一個const函數中。

我們知道,假如類的成員函數不會改變對象的狀態,那麼這個成員函數一般會聲明爲const。但是,有些時候,我們需要在const的函數裏面修改一些跟類狀態無關的數據成員,那麼這個數據成員就應該被mutalbe來修飾。下面是一個小例子:

#include <iostream>
using namespace std;
class Test
{
public:
    Test(int x):x_(x), outputTimes_(0)
    {
    }
    //因爲Getx()這個成員函數不會去更改x數據成員,所以用const修飾,稱之爲const成員函數,它不能去修改數據成員的值,即不會修改類的狀態
    int Getx() const  
    {
        //x_ = 100 ;// Error, const成員函數不能修改數據成員的值
        cout << "const Getx..."<< endl;
        return x_;
    }
    //非const成員函數與上面const成員函數構成重載
    int Getx()  //非const與const成員函數是可以構成重載的
    {
        cout << "Getx..."<<endl;
        return x_;
    }
    int Setx()
    {
        return x_;
    }
    void Output() const 
    {
        //x_ = 102; //Error,不能修改非mutable數據成員
        cout << "x = "<< x_ << endl;
        outputTimes_++; //可以修改mutable類型的數據成員
    }
    
    int GetOutputTimes() const
    {
        return outputTimes_;
    }
private:
    int x_;
    mutable int outputTimes_; //mutable數據成員,可以突破const限制,在const成員函數中也被修改
    
};
int main()
{
    //const對象,對象是常量,對象的狀態不能更改,聲明時要初始化
    const Test t(10);
    t.Getx(); //前提:若int Getx() const註釋掉,則const對象是不能調用非const的Getx()成員函數的
   
    //t.Setx(); //Error,理由同上,即const對象只能調用const成員函數,理由是:const對象是常量,對象的狀態是不允許更改的,而非const即普通成員函數可以更改對象的狀態,這所以爲了實現不允許修改對象的狀態這一目的,const對象禁止調用非const成員函數,而只能調用const成員函數,因爲const成員函數也是禁止修改數據成員的,符合這一目的。
    Test t2(20);
    t2.Getx(); //調用的是非const的Getx,因爲t2是普通對象,不是const對象
    t2.Output(); //普通對象可以調用const成員函數,如果有重載的非const成員函數,則調用的是非const的成員函數
    t2.Output(); //每輸出一次,outputTimes_加1,因爲它是mutable類型的數據成員,允許在const成員函數中被修改
    cout << "outputTimes= "<< t2.GetOutputTimes() << endl; 
    return 0;
}

運行結果:

const Getx...

Getx...

x=20

x=20

outputTimes=2

這裏需要注意的是const對象由於不能修改對象的狀態,const對象只能調用const成員函數,而普通對象可以調用任何的成員函數。此外const對象在聲明時要進行初始化。這與const修改的變量是一致的,如const int x = 100; 當類中定義了2個同名稱的函數,其中一個用const修飾,另一個沒有const修飾,他們之間是構成重載的,這時const對象調用的是const成員函數,而普通對象調用的是非const成員函數。

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