https://m.toutiao.com/is/UftrH7N/?= “常量為什么不能被改寫?有魔法嗎?是的,有魔法” 提出問題 什么是常量?常量就是:數(shù)值不能變化的變量。如果如此簡單、易懂的定義,你都能挑出毛病,那可能就真的是在:嘩眾取寵了。提出問題的人,可能需要在自己身上找找原因了。 但你是否考慮過一個問題:是什么機(jī)制,在保證常量,不能改變這個初衷呢?要知道,市面上所有的內(nèi)存條,都是可讀、可寫、可改的,想試圖阻止 CPU 的寫操作,談何容易呀! 是程序員的自律?還是編譯器的銅墻鐵壁?就讓我們的用 CPU 的視角,解讀一個常量背后的故事。 代碼分析 打開 Compiler Explorer,定義一個常量a,再定義一個變量b;為了防止編譯器優(yōu)化常量 a 的讀寫操作,我們特意在定義常量 a 的時候加上了關(guān)鍵字:volatile;至于 volatile 的工作原理,請查看“CPU眼里的volatile”。 接著,寫一個函數(shù)func1,用來讀取并返回常量a的值;然后,再寫一個函數(shù)func2,用來讀取并返回變量b的值: 老規(guī)矩,不要關(guān)心匯編指令的具體含義,我們只比較二者的差異,很顯然除了a、b的內(nèi)存地址不同外,兩個函數(shù)的匯編指令完全相同!編譯器并沒有對變量和常量作任何的區(qū)分和特殊處理。 難道,我們又要得到一個聳人聽聞的結(jié)論:變量與常量,本質(zhì)上沒有任何區(qū)別?且慢,我們再看看寫操作,先給變量b賦值,編譯通過,沒有問題。 再給常量a,賦相同的值:1 inf func1(){ a = 1; return 0;} 雖然我們并沒有試圖改變常量a的值,但我們的代碼,還是會被編譯器無情的拒絕! 看來,常量的含義不僅僅是:它的值不可改變,原來它是徹底的拒絕寫操作呀。即便是你并不打算改變它的值??磥?const 關(guān)鍵字還真能保護(hù)常量的值,不會被重新寫入。 為了解除編譯器層面的禁止,我們需要為常量a稍微換個馬甲,幫助它繞過編譯器的檢查: 如你所見,對常量a作一個向普通 int 類型的轉(zhuǎn)換,這樣就可以通過編譯了。 再比較一下函數(shù)func1和函數(shù)func2的匯編指令,如你所見:除了a、b的內(nèi)存地址不同外,它們的匯編指令是完全一致的! 好了,如此看來:常量a和變量b,在讀、寫操作上面,都是完全一致的。排除編譯器在語法層面,對常量的保護(hù),我們能否認(rèn)為:常量與變量的本質(zhì)是完全相同的呢? 到底是否相同,我們實際運(yùn)行一下就知道了。寫一個函數(shù)main,先調(diào)用一下函數(shù)func2,一切正常;然后我們再調(diào)用一下函數(shù)func1: 如你所見,在調(diào)用函數(shù)func1的時候程序出錯,返回值:139意味著:段錯誤(segmentation fault)這是為什么呢?讓我們分別打印:常量a和變量b的內(nèi)存地址: 如你所見,雖然a、b是依次定義的,但是內(nèi)存地址的距離,卻超過了:8K 字節(jié)。它們顯然不在同一個內(nèi)存頁里面,如“CPU眼里的程序運(yùn)行”所說,程序運(yùn)行前,代碼中的全局變量、常量會被拷貝到數(shù)據(jù)段。只是這個數(shù)據(jù)段還會被細(xì)分成:只讀數(shù)據(jù)段和可讀、寫數(shù)據(jù)段,因此,它們所在的內(nèi)存頁的讀、寫屬性可能是不同的。 我們猜:變量b所在的內(nèi)存頁,在MMU映射表中的屬性是:可讀、可寫的;常量a所在的內(nèi)存頁,在MMU映射表中屬性是:只讀的。因此,當(dāng)我們強(qiáng)行對a進(jìn)行:寫操作時,就會觸發(fā)CPU異常,導(dǎo)致程序崩潰! 因為,內(nèi)存頁的讀寫屬性,不僅對常量a所在的內(nèi)存有效,甚至對整個4KB的內(nèi)存頁,都是有效的。所以,即使試圖對a周圍的內(nèi)存,進(jìn)行寫操作也是不被允許的: 夸張的說:現(xiàn)代操作系統(tǒng)和編程語言的實現(xiàn),都離不開MMU這個好幫手。更多的MMU知識,還可以查看“CPU眼里的虛擬內(nèi)存”。 總結(jié)
但如果常量所在的內(nèi)存頁是可讀、寫的,例如:函數(shù)內(nèi)部定義的臨時的“?!背A浚捎凇岸褩!北旧硎强勺x、可寫的,所以在逃過編譯器檢查后,“?!背A恳彩强梢皂樌麑懭氲摹?/p> 熱點問題 Q1:如果我通過cast強(qiáng)行轉(zhuǎn)換成非const變量呢? A1:效果是一樣的,你或許可以繞過編譯器的檢查,但在真正作寫操作的時候,會被MMU察覺到。 Q2:單片機(jī),例如:STM32,沒有MMU,它能阻止對常量的寫操作嗎? A2:雖然單片機(jī)可能沒有MMU,但它仍然有可能阻止程序?qū)ΤA康膶懖僮?。因為,單片機(jī)在編譯完程序后,往往會通過專門的設(shè)備把程序中的:常量、函數(shù),燒寫在:ROM上面。 由于ROM的寫入過程比較特殊,需要配合特定的設(shè)備和總線操作;CPU不能通過常規(guī)的內(nèi)存讀、寫指令(例如:MOV指令)來改寫ROM上的信息,所以,代碼對常量的寫操作,即使可以順利運(yùn)行,但也很難真正改寫ROM上的常量。 |
|