再探C 中的常量

2021-09-17 21:19:49 字數 3434 閱讀 9232

什麼是常量?相信絕大多數寫過程式的人見到這兩個字的第一反應就是——不能更改的量。沒問題,書本上這樣告訴我們,編譯器也這樣告訴我們,好像常量就真的是那麼簡單,純粹,讓人心安理得的去使用它,不需要任何顧慮。但事實並沒有想象中的那麼單純。

1.常量並不一定是常量

int main()

這是乙個「假常量」,當我們在下邊敲出 x=4; 這樣的**時,編譯器根本不會讓你通過,至於為什麼說它是假的,因為編譯器可以被輕而易舉的繞過去。

首先我們來看看表面現象,對p的一頓操作讓p成功的指向了x,我們成功的繞過了編譯器,獲得了指向常量x的非常量指標,並且沒有動用const_cast,當我們竊喜可以修改這個「常量」x的值的時候,執行結果給我們當頭一棒,x的值並沒有改。難道是編譯器明察秋毫發現我們的小伎倆了?不,編譯器真的那麼聰明是不會把結果顯示出來的,甚至都不會讓你通過編譯。那為什麼x的值沒有改變呢?讓我們來看看彙編**。我先吧整體的**貼出來再節選部分進行分析。

const int x=3;

00176e2e mov dword ptr [x],3

int y=4;

00176e35 mov dword ptr [y],4

int*p=&y;

00176e3c lea eax,[y]

00176e3f mov dword ptr [p],eax

p=p-(&y-&x);

00176e42 lea eax,[y]

00176e45 lea ecx,[x]

00176e48 sub eax,ecx

00176e4a sar eax,2

00176e4d shl eax,2

00176e50 mov edx,dword ptr [p]

00176e53 sub edx,eax

00176e55 mov dword ptr [p],edx

*p=5;

00176e58 mov eax,dword ptr [p]

00176e5b mov dword ptr [eax],5

cout不要被這一大堆亂七八糟的字元嚇到,其實大多數跟我們的主題沒有關係,這裡全貼出來是為了讓讀者相信的確是所有的**都在這了,並沒有漏掉什麼關鍵的語句,先來看下面這兩行。

const int x=3;

00176e2e  mov         dword ptr [x],3  

int y=4;

00176e35  mov         dword ptr [y],4  

所以說為什麼把這個x叫做假常量,因為有沒有這個const並不影響翻譯出來的彙編**,也就是說除了我們和編譯器,誰也不知道有這個const的存在,我們之所以不能無視它,是因為「嚴厲」的編譯器決定了我們的**能不能變成可執行的程式。這裡有一點非常重要,x是在棧上有空間的,和y一樣,都是棧上的變數。一定記住這句話,這很重要。在下文中,形如x這種常量我稱作「棧上常量」。

*p=5;

00176e58  mov         eax,dword ptr [p]  

00176e5b  mov         dword ptr [eax],5  

我跳過了前兩行語句,這裡只需要知道p現在確實指向x的位址就夠了,下邊的輸出結果也印證了這一點。可見,我們確實改變了x的值。那麼為什麼列印出來的x還是3呢?

所有的原因都在這裡,當我們輸出y的時候,編譯器忠實的把y的值放到了暫存器裡,把暫存器裡的值當成引數傳給了函式,但是對於x,編譯器根本沒有從x的記憶體裡取x的值,它直接就push了乙個3給函式,這就是為什麼列印出的是3而不是5。那麼這個3是從**來的呢?3是乙個常量,它存在於程序位址空間的常量區,當我們敲出const int x=3; 的時候,我們不但把x所屬的空間賦值為3,我們還在常量區建立了乙個數字3的常量。

那麼為什麼我們使用x的時候編譯器卻回過頭來操作這個3呢?這個現象叫做「常量摺疊」,是一種編譯器對於棧上常量的優化,當使用棧上常量的時候會直接從常量區取對應的值出來,這樣少了乙個從變數中取值到暫存器的過程,可以和對y的操作對比來看。

如果你還是不理解常量摺疊,可以試試看一下下邊的**

換成字串可能會比整數要方便理解一些,不過本質上來看,作為常量的它們都是記憶體位址空間常量段中的一塊記憶體罷了。

想要禁用常量摺疊也很簡單,使用volitile讓x在每次使用的時候都從記憶體中取值就ok了

這裡出現了乙個很大的問題,對x取位址居然輸出的是1,我測試了一下只要變數前邊加了volatile,對它取位址就是1,彙編**我暫時也看不懂發生了什麼,這個坑以後再填。

2.常量真的就是常量

const int x=3;

int main ()

把定義的位置放到全域性空間裡,一切都不一樣了,這下它真的是常量了。

const int y=4;

int main()

const int x=3;

003d4bfe  mov         dword ptr [x],3  

int*p=const_cast(&y);

003d4c05  mov         dword ptr [p],3dcbb0h  

int*q=const_cast(&x);

003d4c0c  lea         eax,[x]  

003d4c0f  mov         dword ptr [q],eax  

對y取位址獲得的是乙個實打實的常量區位址,我們可以用 *q=12345; 改變x的值,但是就別想 *p=56789了,編譯器雖然發現不了,但是作業系統是不會允許你這樣做的,執行的時候程式會報錯,因為你企圖修改程序虛擬位址空間中的唯讀段。

在全域性空間用const宣告常量的時候,這個常量處於位址空間的常量段,是乙個真正的常量

在棧內空間用const宣告常量的時候,這個常量本質上和非常量沒有區別,它處於堆疊中,存的值和常量相同,因為在賦值的時候執行了拷貝,但是在宣告的同時,程序虛擬位址空間的常量段也會同時多出乙個對應的常量,當我們使用這個棧上常量的時候,編譯器會自動幫我們把它替換成常量段的常量。

C 語句函式再探

1.表示式只計算,拋棄計算結果 2.空語句什麼也不做 3.switch case語句漏寫break,將會從匹配到的情況開始執行,直到語句結束 4.形參 實參 區域性變數 靜態區域性變數 所謂形參就是佔位之用,在函式開始時申請空間,並由傳入實參進行例項化 也可稱拷貝構造 一般是區域性的,即只能在函式體...

C 筆記 IO庫再探

標準庫定義了一組操縱符,用來修改流的格式狀態。乙個操縱符是乙個函式或者物件,會影響流的狀態,並能用作輸入或輸出運算子的運算物件。例如 endl 在使用endl時,就像是將它寫到流中去。但是endl實際上是乙個操作 它輸出乙個換行,並重新整理緩衝區。當操縱符改變流的狀態時,會影響該流的後續操作 例如 ...

C 筆記 15 8 文字查詢再探

將層次關係隱藏於介面類中 這是乙個抽象基類,具體的查詢型別從中派生,所有成員都是private的 class query base 這是乙個管理query base繼承體系的介面類 class query string rep const private query shared ptr query...