上接C/C++要點全掌握(四)。
13、mutable和volatile
很少遇到這兩個關鍵字,學嵌入式估計知道後者,深入研究C++的估計知道前者。
(1)mutable
在C++中,mutable是為了突破const的限制而設置的。被mutable修飾的變量,將永遠處於可變的狀態,即使在一個const函數中,甚至結構體變量或者類對象為const,其mutable成員也可以被修改。
struct ST
{
int a;
mutable int b;
};
const ST st={1,2};
st.a=11;//編譯錯誤
st.b=22;//允許
mutable在類中只能夠修飾非靜態數據成員。mutable 數據成員的使用看上去像是騙術,因為它能夠使const函數修改對象的數據成員。然而,明智地使用mutable 關鍵字可以提高代碼質量,因為它能夠讓你向用戶隱藏實現細節,而無須使用不確定的東西。我們知道,如果類的成員函數不會改變對象的狀態,那麼這個成員函數一般會聲明成const的。但是,有些時候,我們需要在const的函數裡面修改一些跟類狀態無關的數據成員,那麼這個數據成員就應該被mutalbe來修飾。
class ST
{
int a;
mutable int showCount;
void Show()const;
…
};
ST::Show()
{
…//顯示代碼
a=1;//錯誤,不能在const成員函數中修改普通變量
showCount++;//正確
}
const承諾的是一旦某個變量被其修飾,那麼只要不使用強制轉換(const_cast),在任何情況下該變量的值都不會被改變,無論有意還是無意,而被const修飾的函數也一樣,一旦某個函數被const修飾,那麼它便不能直接或間接改變任何函數體以外的變量的值,即使是調用一個可能造成這種改變的函數都不行。這種承諾在語法上也作出嚴格的保證,任何可能違反這種承諾的行為都會被編譯器檢查出來。
mutable的承諾是如果某個變量被其修飾,那麼這個變量將永遠處於可變的狀態,即使在一個const函數中。這與const形成了一個對稱的定義,一個永遠不變,而另外一個是永遠可變。
看一個變量或函數是否應該是const,只需看它是否應該是constant或invariant,而看一個變量是否應該是mutable,也只需看它是否是forever mutative。
這裡出現了令人糾結的3個問題:
1、為什麼要保護類的成員變量不被修改?
2、為什麼用const保護了成員變量,還要再定義一個mutable關鍵字來突破const的封鎖線?
3、到底有沒有必要使用const 和mutable這兩個關鍵字?
保護類的成員變量不在成員函數中被修改,是為了保證模型的邏輯正確,通過用const關鍵字來避免在函數中錯誤的修改了類對象的狀態。並且在所有使用該成員函數的地方都可以更准確的預測到使用該成員函數的帶來的影響。而mutable則是為了能突破const的封鎖線,讓類的一些次要的或者是輔助性的成員變量隨時可以被更改。沒有使用const和mutable關鍵字當然沒有錯,const和mutable關鍵字只是給了建模工具更多的設計約束和設計靈活性,而且程序員也可以把更多的邏輯檢查問題交給編譯器和建模工具去做,從而減輕程序員的負擔。
(2)volatile
象const一樣,volatile是一個類型修飾符。volatile修飾的數據,編譯器不可對其進行執行期寄存於寄存器的優化。這種特性,是為了滿足多線程同步、中斷、硬件編程等特殊需要。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的直接訪問。
volatile原意是“易變的”,但這種解釋簡直有點誤導人,應該解釋為“直接存取原始內存地址”比較合適。“易變”是相對與普通變量而言其值存在編譯器(優化功能)未知的改變情況(即不是通過執行代碼賦值改變其值的情況),而是因外在因素引起的,如多線程,中斷等。編譯器進行優化時,它有時會取一些值的時候,直接從寄存器裡進行存取,而不是從內存中獲取,這種優化在單線程的程序中沒有問題,但到了多線程程序中,由於多個線程是並發運行的,就有可能一個線程把某個公共的變量已經改變了,這時其余線程中寄存器的值已經過時,但這個線程本身還不知道,以為沒有改變,仍從寄存器裡獲取,就導致程序運行會出現未定義的行為。並不是因為用volatile修飾了的變量就是“易變”了,假如沒有外因,即使用volatile定義,它也不會變化。而加了volatile修飾的變量,編譯器將不對其相關代碼執行優化,而是生成對應代碼直接存取原始內存地址。
一般說來,volatile用在如下的幾個地方:
1、中斷服務程序中修改的供其它程序檢測的變量需要加volatile;
2、多任務環境下各任務間共享的標志應該加volatile;
3、存儲器映射的硬件寄存器通常也要加volatile說明,因為每次對它的讀寫都可能有不同意義;
使用該關鍵字的例子如下:
volatile int i=10;
int a = i;
...
//其他代碼,並未明確告訴編譯器,對i進行過操作
int b = i;
volatile 指出i是隨時可能發生變化的,每次使用它的時候必須從i的地址中讀取,因而編譯器生成的匯編代碼會重新從i的地址讀取數據放在b中。而優化做法是,由於編譯器發現兩次從i讀數據的代碼之間的代碼沒有對i進行過操作,它會自動把上次讀的數據(即10)放在b中,而不是重新從i裡面讀。這樣以來,如果i是一個寄存器變量或者表示一個端口數據就容易出錯,所以說volatile可以保證對特殊地址的直接訪問。
//addr為volatile變量
addr=0x57;
addr=0x58;
如果上述兩條語句是對外部硬件執行不同的操作,那麼編譯器就不能像對待普通的程序那樣對上述語句進行優化只認為“addr=0x58;”而忽略第一條語句(即只產生一條機器代碼),此時編譯器會逐一的進行編譯並產生相應的機器代碼(兩條)。
volatile總是與優化有關,編譯器有一種技術叫做數據流分析,分析程序中的變量在哪裡賦值、在哪裡使用、在哪裡失效,分析結果可以用於常量合並,常量傳播等優化,進一步可以死代碼消除。但有時這些優化不是程序所需要的,這時可以用volatile關鍵字禁止做這些優化,它有下面的作用:
1、不會在兩個操作之間把volatile變量緩存在寄存器中。在多任務、中斷等環境下,變量可能被其他的程序改變,編譯器自己無法知道,volatile就是告訴編譯器這種情況。
2、不做常量合並、常量傳播等優化,所以像下面的代碼,if的條件不會當作無條件真。
volatile int i = 1;
if (i > 0)
...
3、對volatile變量的讀寫不會被優化掉。如果你對一個變量賦值但後面沒用到,編譯器常常可以省略那個賦值操作,然而對Memory Mapped IO的處理是不能這樣優化的。
摘自 tht的專欄