const、volatile、mutable的用法總結
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,不同的寫法會有不同情況:
l 指標本身是常量不可變
(char*) const pContent;
l 指標所指向的內容是常量不可變
const (char) *pContent;
(char) const *pContent;
l 兩者都不可變
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巨集定義的區別
l 編譯器處理方式不同
define巨集是在預處理階段展開。
const常量是編譯執行階段使用。
l 型別和安全檢查不同
define巨集沒有型別,不做任何型別檢查,僅僅是展開。
const常量有具體的型別,在編譯階段會執行型別檢查。
l 儲存方式不同
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;
}
計數器m_iTimes被mutable修飾,那麼它就可以突破const的限制,在被const修飾的函式裡面也能被修改。