1. 程式人生 > >轉:C++中const、volatile、mutable的用法

轉:C++中const、volatile、mutable的用法

const修飾普通變數和指標

const修飾變數,一般有兩種寫法:

const TYPE value;

TYPE const value;

這兩種寫法在本質上是一樣的。它的含義是:const修飾的型別為TYPE的變數value是不可變的。對於一個非指標的型別TYPE,無論怎麼寫,都是一個含義,即value值不可變。 例如:

const int nValue; //nValue是const

int const nValue; //nValue是const

但是對於指標型別的TYPE,不同的寫法會有不同情況:

(1) 指標本身是常量不可變

(char*) const pContent;

(2)指標所指向的內容是常量不可變

const (char) *pContent;

(char) const *pContent;

(3) 兩者都不可變

const char* const pContent;

識別const到底是修飾指標還是指標所指的物件,還有一個較為簡便的方法,也就是沿著*號劃一條線:

如果const位於*的左側,則const就是用來修飾指標所指向的變數,即指標指向為常量;

如果const位於*的右側,const就是修飾指標本身,即指標本身是常量。

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修飾。

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(); 正確 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];

… } 列舉常量不會佔用物件的儲存空間,他們在編譯時被全部求值。但是列舉常量的隱含資料型別是整數,其最大值有限,且不能表示浮點數。

const修飾成員函式

const修飾類的成員函式,用const修飾的成員函式不能改變物件的成員變數。一般把const寫在成員函式的最後:

class A

{

void function()const; //常成員函式, 它不改變物件的成員變數. 也不能呼叫類中任何非const成員函式。

}

對於const類物件/指標/引用,只能呼叫類的const成員函式。

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巨集定義的區別

(1) 編譯器處理方式不同

define巨集是在預處理階段展開。

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

(2)型別和安全檢查不同

define巨集沒有型別,不做任何型別檢查,僅僅是展開。

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

(3) 儲存方式不同

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來修飾。下面是一個小例子:

class ClxTest

{

public:

void Output() const;

};

void ClxTest::Output() const

{

cout << “Output for test!” << endl;

}

void OutputTest(const ClxTest& lx)

{

lx.Output();

}

類ClxTest的成員函式Output是用來輸出的,不會修改類的狀態,所以被宣告為const。

函式OutputTest也是用來輸出的,裡面呼叫了物件lx的Output輸出方法,為了防止在函式中呼叫成員函式修改任何成員變數,所以引數也被const修飾。

假如現在,我們要增添一個功能:計算每個物件的輸出次數。假如用來計數的變數是普通的變數的話,那麼在const成員函式Output裡面是不能修改該變數的值的;而該變數跟物件的狀態無關,所以應該為了修改該變數而去掉Output的const屬性。這個時候,就該我們的mutable出場了,只要用mutalbe來修飾這個變數,所有問題就迎刃而解了。下面是修改過的程式碼:

class ClxTest

{

public:

ClxTest();

~ClxTest();

void Output() const;

int GetOutputTimes() const;

private:

mutable int m_iTimes;

};

ClxTest::ClxTest()

{

m_iTimes = 0;

}

ClxTest::~ClxTest()

{}

void ClxTest::Output() const

{

cout << “Output for test!” << endl;

m_iTimes++;

}

int ClxTest::GetOutputTimes() const

{

return m_iTimes;

}

void OutputTest(const ClxTest& lx)

{

cout << lx.GetOutputTimes() << endl;

lx.Output();

cout << lx.GetOutputTimes() << endl;

}