1. 程式人生 > >面試題9: const、static、volatile關鍵字使用說明

面試題9: const、static、volatile關鍵字使用說明

關於const關鍵字的用法,潛意識下就會想到 修飾變數,一直沒有深入挖掘。最近在面試的時候常常會被問到const、static、votaile等關鍵字的使用與區別。藉此機會,重新複習總結關於此關鍵字的使用。

1、首先看一下,const與巨集定義之間的比較:

巨集作用: 在開發中會把一些常用的變數的值定義成巨集;

const作用:
      1.用於修飾右邊變數(基本變數,指標變數)
      2.被const修飾變數只讀(普通的變數是可讀可寫的)


const與巨集區別:
    1.編譯時刻: 巨集:預編譯 const:編譯
    2.編譯檢查:  巨集不會做編譯檢查 const會
    3.巨集好處: 巨集可以定義函式和方法 const不行
    4.巨集壞處: 大量使用巨集,會導致預編譯時間過長

// 典型面試題
    int * const p1; // p1:只讀  *p1:變數
    const int *p2; // p2:變數 *p2:只讀
    int const *p3; // p3:變數 *p3:只讀
    int const * const p4; // p4:只讀 *p4:只讀
    const int * const p5; // p5:只讀 *p5:只讀

2、關於const的使用

const 可以修飾函式的引數、返回值,甚至函式的定義體。被const 修飾的東西都受到強制保護,可以預防意外的變動,能提高程式的健壯性。所以很多C++程式設計書籍建議:“Use const whenever you need”。

(1)const 修飾函式的引數

如果引數作輸出用,不論它是什麼資料型別,也不論它採用“指標傳遞”還是“引用傳遞”,都不能加const 修飾,否則該引數將失去輸出功能。

const 只能修飾輸入引數:如果輸入引數採用“指標傳遞”,那麼加const 修飾可以防止意外地改動該指標,起到保護作用。

void StringCopy(char *strDestination, const char *strSource);

“const &”修飾輸入引數總結如下:

對於非內部資料型別的輸入引數,應該將“值傳遞”的方式改為“const 引用傳遞”,目的是提高效率。例如將void Func(A a) 改為void Func(const A &a)。

 對於內部資料型別的輸入引數,不要將“值傳遞”的方式改為“const 引用傳遞”。否則既達不到提高效率的目的,又降低了函式的可理解性。例如void Func(int x) 不應該改為void Func(const int &x)。

(2)const 修飾函式的返回值

如果給以“指標傳遞”方式的函式返回值加const 修飾,那麼函式返回值(即指標)的內容不能被修改,該返回值只能被賦給加const 修飾的同類型指標。

函式返回值採用“引用傳遞”的場合並不多,這種方式一般只出現在類的賦值函式中,目的是為了實現鏈式表達。

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 則是非法的。

(3) const 成員函式(const的作用:說明其不會修改資料成員)

任何不會修改資料成員的函式都應該宣告為const 型別。如果在編寫const 成員函式時,不慎修改了資料成員,或者呼叫了其它非const 成員函式,編譯器將指出錯誤,這無疑會提高程式的健壯性。

關於Const函式的幾點規則:

a. const物件只能訪問const成員函式,而非const物件可以訪問任意的成員函式,包括const成員函式.

b. const物件的成員是不可修改的,然而const物件通過指標維護的物件卻是可以修改的.

c. const成員函式不可以修改物件的資料,不管物件是否具有const性質.它在編譯時,以是否修改成員資料為依據,進行檢查.

e. 然而加上mutable修飾符的資料成員,對於任何情況下通過任何手段都可修改,自然此時的const成員函式是可以修改它的。

const 函式只能呼叫 const 函式,即使某個函式本質上沒有修改任何資料,但沒有宣告為const,也是不能被const函式呼叫的。

每個非static非const成員函式都有一個隱含的this指標,非const型的不能接受const型實參;

如果用const來修飾函式,那麼函式一定是類的成員函式。const 型別的成員函式不能返回非const型別的引用。這句話的意思是如果你的成員函式是const型別的,並且要求返回值是類的非cosnt或者非mutable成員變數,返回型別是引用,那麼這是錯誤的。

class Test  
{  
public :  
int & GetValue()const;  
private:  
int value;  
};  
int &Test::GetValue() const  
{  
return value; //value此時具有const屬性,與返回值型別int &的非const屬性不匹配  
}  

這樣的程式碼在vs2003中提示的錯誤:error C2440: “return” : 無法從“const int”轉換為“int &”。
在const函式中傳遞this的時候把this變成了const T* const this(個人理解),所以一個非const的引用指向一個const型別的變數,就會error。
可以這樣改:

1.把int value 改成mutable int value. mutable修飾的變數使之在const函式中可以被改變的。
2.return value 改成。 return const_cast(value)。const_cast去掉了const性質。
3.把函式寫成const int &Test::GetValue() const ,.這樣做的目的是使引用的變數也是const型別的,就相當於const int & b 。
4.把引用去掉,寫成返回值型別的。
5.把函式後面的const去掉。
6.返回值不是類的成員變數。

3、static使用方法     

static的作用主要有以下3個:

1、擴充套件生存期;

這一點主要是針對普通區域性變數和static區域性變數來說的。宣告為static的區域性變數的生存期不再是當前作用域,而是整個程式的生存期。

2、限制作用域;

對於全域性變數而言,不論是普通全域性 變數還是static全域性變數,其儲存區都是靜態儲存區,因此在記憶體分配上沒有什麼區別。

1) 普通的全域性變數和函式,其作用域為整個程式或專案,外部檔案(其它cpp檔案)可以通過extern關鍵字訪問該變數和函式。一般不提倡這種用法,如果要在多個cpp檔案間共享資料,應該將資料宣告為extern型別。

 在標頭檔案裡宣告為extern:

extern int g_value;     // 注意,不要初始化值!

然後在其中任何一個包含該標頭檔案的cpp中初始化(一次)就好:

int g_value = 0;     // 初始化一樣不要extern修飾,因為extern也是宣告性關鍵字;

然後所有包含該標頭檔案的cpp檔案都可以用g_value這個名字訪問相同的一個變數;


2) static全域性變數和函式,其作用域為當前cpp檔案,其它的cpp檔案不能訪問該變數和函式。如果有兩個cpp檔案聲明瞭同名的全域性靜態變數,那麼他們實際上是獨立的兩個變數。
static函式的好處是不同的人編寫不同的函式時,不用擔心自己定義的函式,是否會與其它檔案中的函式同名。

3、唯一性;

針對靜態資料成員而言, 成員函式不管是否是static, 在記憶體中只有一個副本, 普通成員函式呼叫時, 需要傳入this指標, static成員函式呼叫時, 沒有this指標. )

在C/C++中, 區域性變數按照儲存形式可分為三種auto, static, register:

區域性變數的預設型別都是auto,從棧中分配記憶體。auto的含義是由程式自動控制變數的生存週期,通常指的就是變數在進入其作用域的時候被分配,離開其作用域的時候被釋放。

而static變數,不管是區域性還是全域性,都存放在靜態儲存區。表面意思就是不auto,變數在程式初始化時被分配,直到程式退出前才被釋放;也就是static是按照程式的生命週期來分配釋放變數的,而不是變數自己的生命週期. 

關於堆、棧儲存區域的區別:

1)   堆是由低地址向高地址擴充套件,棧是由高地址向低地址擴充套件。

2)   堆是不連續的空間,棧是連續的空間。

3)   在申請空間後,棧的分配要比堆的快。對於堆,先遍歷存放空閒儲存地址的連結串列、修改連結串列、再進行分配;對於棧,只要剩下的可用空間足夠,就可分配到,如果不夠,那麼就會報告棧溢位。

4)   棧的生命期最短,到函式呼叫結束時;靜態儲存區的生命期最長,到程式結束時;堆中的生命期是到被我們手動釋放時(如果整個過程中都不手動釋放,那就到程式結束時)。

static資料成員的初始化:

(1) 初始化在類體外進行,而前面不加static,以免與一般靜態變數或物件相混淆。

(2) 初始化時不加該成員的訪問許可權控制符private,public等。

(3) 初始化時使用作用域運算子來標明它所屬類,因此,靜態資料成員是類的成員,而不是物件的成員。

(4) 靜態資料成員是靜態儲存的,它是靜態生存期,必須對它進行初始化。


static成員函式

靜態成員函式和靜態資料成員一樣,它們都屬於類的靜態成員,它們都不是物件成員。因此,對靜態成員的引用不需要用物件名。

靜態成員函式僅能訪問靜態的資料成員,不能訪問非靜態的資料成員,也不能訪問非靜態的成員函式,這是由於靜態的成員函式沒有this指標。

4、volatile的使用

volatile 關鍵字是一種型別修飾符,用它宣告的型別變量表示可以被某些編譯器未知的因素更改,比如:作業系統、硬體或者其它執行緒等。遇到這個關鍵字宣告的變數,編譯器對訪問該變數的程式碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。

宣告時語法:int volatile vInt; 

當要求使用 volatile 宣告的變數的值的時候,系統總是重新從它所在的記憶體讀取資料,即使它前面的指令剛剛從該處讀取過資料。而且讀取的資料立刻被儲存。

1	volatile int i=10;
2	int a = i;
3	...
4	// 其他程式碼,並未明確告訴編譯器,對 i 進行過操作
5	int b = i;

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

一般說來,volatile用在如下的幾個地方: 
1) 中斷服務程式中修改的供其它程式檢測的變數需要加volatile; 
2) 多工環境下各任務間共享的標誌應該加volatile; 
3) 儲存器對映的硬體暫存器通常也要加volatile說明,因為每次對它的讀寫都可能由不同意義;

volatile 指標注意:

(1) 可以把一個非volatile int賦給volatile int,但是不能把非volatile物件賦給一個volatile物件。

(2) 除了基本型別外,對使用者定義型別也可以用volatile型別進行修飾。

(3) C++中一個有volatile識別符號的類只能訪問它介面的子集,一個由類的實現者控制的子集。使用者只能用const_cast來獲得對型別介面的完全訪問。此外,volatile向const一樣會從類傳遞到它的成員。