前言
前幾天女票問了我一個阿里的面試題,是有關C++語言的const常量的,其實她一提出來我就知道考察的點了:肯定是const常量的內存不是分配在read-only的存儲區的,const常量的內存分配區是很普通的棧或者全局區域。也就是說const常量只是編譯器在編譯的時候做檢查,根本不存在什么read-only的區域。
所以說C++的const常量和常量字符串是不同的,常量字符串是存儲在read-only的區域的,他們的具體的存儲區域是不同的。
就好像楊立翔老師在上課舉得那個例子(講的東西忘得差不多了,但是例子都記得,什么撕戶口本,月餅模子之類的):
場景:老式的電影放映機,門口的檢票員,最后排那個座位上的放映師傅,好多的觀眾席。
你的電影票上有你的名字,檢票員的手里有個名字和座位號的對應表,檢票員不允許觀眾亂坐位置。
所以游戲就這樣開始了,電影院的每一個觀眾席都是const常量類型的,檢票員就是編譯器,在觀眾進門的時候,每個人都必須坐自己票上的位子,也就是說觀眾必須拿着自己的票進門。你拿着別人的票就不讓你進去,所以就不能更換座位。
類比到C++語言上,場景應該是這樣的:對於哪些const常量,在編譯的時候就已經決定了,他們在內存的存儲位置,你如果直接改這個常量的值是不合法的。就好像你不能拿着一張別人的電影票進電影院,因為檢票員手里有一張座位號到姓名的映射表,他知道哪個座位該坐誰,並且不讓觀眾隨意調換(這個例子尼瑪真的好形象啊,楊老師說的沒有這么好,我后期根據自己的理解稍微添油加醋)。其實檢票員手里的那張映射表就是編譯器的符號表,是不是真的很形象,關於這個符號表后面說。
那么新的問題來了,觀眾進到電影院以后,我跟旁邊的哥們說一聲,我給你100塊,你給我換一下位置吧,如果他同意了,我們就能換。檢票員管不着,進都進來了,還能怎樣。所以兩個人就調換了位子。
C++的const常量一樣是這個樣子,編譯器在編譯的時候要查看它手里的符號表,如果你拿的是一個const常量,並且你要修改這個常量的值,他就拒絕。但是這種檢查值會發生在編譯器,如果我能繞過編譯器,在運行的時候修改這個const的值,將會是很easy的一件事,就像前面說的,我給那哥們100塊就搞定了。所以說const常量的存儲空間同其他的變量的存儲空間沒有任何的區別,它的這種常量不允許就該的檢查只發生在編譯器的編譯階段。
但是常量字符串的存儲位置就不同了,它的存儲位置是read-only的區域,就像電影院最后的那個放映師的位置,那里是一個特殊的位置,是真的不允許隨便的調換,你給他1000也不給你換,因為師傅要在那個位置放映,不然沒法看電影。當然了,實現這種read-only的存儲區域也很簡單,把那個內存的頁(page)的屬性標記為只讀的就好了。
對了,當時我還想起了這個:
const int *p1; /* p1所指向的int變量值不可改變,為常量,但可以改變p1指針的值 */ int * const p2; /* p2指針為常量,即p2的值不可改變,但可以改變p2指向對象的值 */ const int * const p3; /* p3指針是常量,同時p3所指向int對象的值也是常量 */
啰啰嗦嗦說這么長一個例子,其實很簡單的道理,就是為了好玩,好記。先宏觀的說一下const常量的實現機制,下面說一些具體的實現。
其實女票提出的問題不算難,是C++語言的一個知識點,總而言之就是一個常量折疊。
先說一個錯誤的理解(為什么要說一個錯誤的理解,因為它有助於正確的理解,哈哈):可折疊的常量就像宏一樣,在預編譯階段對const常量的引用一律被替換為常量所對應的值。就和普通的宏替換沒有什么區別,並且編譯器不會為該常量分配存儲空間。
看清楚了,上面說的是一個錯誤的理解,常量折疊確實會像宏替換一樣把對常量的引用替換為常量對應的值,但是該常量是分配空間的,並且靠編譯器來檢查常量屬性。
#define PI 3.14
int main()
{
const int r = 10;
int p = PI;//這里在預編譯階段產生宏替換,PI直接替換為3.14,其實就是int p = 3.14
int len = 2 * r;//這里會發生常量折疊,也就是說常量r的引用會替換成它對應的值,相當於int len = 2 * 10;
return 0;
}
如上述代碼中所述,常量折疊表面上的效果和宏替換是一樣的,只是,“效果上是一樣的”,而兩者真正的區別在於,宏是字符常量,在預編譯階段的宏替換完成后,該宏名字會消失,所有對宏如PI的引用已經全部被替換為它所對應的值,編譯器當然沒有必要再維護這個符號。而常量折疊發生的情況是,對常量的引用全部替換為該常量如r的值,但是,常量名r並不會消失,編譯器會把他放入到符號表中,同時,會為該變量分配空間,棧空間或者全局空間。既然放到了符號表中,就意味着可以找到這個變量的地址(埋一個伏筆先)。
符號表不是一張表,是一系列表的統稱,這里的const常量,會把這個常量的名字、類型、內存地址、值都放到常量表中。符號表還有一個變量表,這個表放變量的名字、類型、內存地址,但是沒有放變量的值。
為了更能體現出常量折疊,看下面的對比實驗:
int main() { int i0 = 11; const int i = 0; //定義常量i int *j = (int *) &i; //看到這里能對i進行取值,判斷i必然后自己的內存空間 *j = 1; //對j指向的內存進行修改 printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //觀看實驗效果 const int ck = 9; //這個對照實驗是為了觀察,對常量ck的引用時,會產生的效果 int ik = ck; int i1 = 5; //這個對照實驗是為了區別,對常量和變量的引用有什么區別 int i2 = i1; return 0; }
下面看一下不同編譯器的輸出結果
vc6.0:
vs2010:
g++的輸出結果:
注意:對於Linux的GUN中的gcc是用來編譯.c文件的C語言編譯器,g++是用來編譯.cpp的C++語言編譯器。
我們這里講的是C++的商量折疊,所以源文件要是.cpp的才可以。(C語言的const常量最后再說)
上面的程序的運行結果至少說明兩點:
(1)i和j地址相同,指向同一塊空間,i雖然是可折疊常量,但是,i確實有自己的空間
(2)i和j指向同一塊內存,但是*j = 1對內存進行修改后,按道理來說,*j==1,i也應該等於1,而實驗結果確實i實實在在的等於0。
這是為什么呢,就是本文所說的內容,i是可折疊常量,在編譯階段對i的引用已經別替換為i的值了,同時不同於宏替換的是,這個i還被存到了常量表中。
也就是說:
printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j);
中的i在預處理階段已經被替換,其實已經被改為:
printf("0x%p\n0x%p\n%d\n%d\n",&i,j,0,*j);
同時在常量表中也有i這個變量,不然的話對i取地址是不合法的,這是和宏替換的不同點,宏替換是不會把宏名稱放到常量表中的,預編譯完就用不到了。
為了更加直觀,下面直接上這個程序的反編譯的匯編語言:
(1)方法:打個斷點調試->窗口反匯編(還有好多功能,比如內存多線程等)
(2)反匯編代碼:

1 --- d:\cv_projects\commontest\commontest\commontest.cpp ------------------------ 2 #include <stdio.h> 3 int main() 4 { 5 01311380 push ebp 6 01311381 mov ebp,esp 7 01311383 sub esp,114h 8 01311389 push ebx 9 0131138A push esi 10 0131138B push edi 11 0131138C lea edi,[ebp-114h] 12 01311392 mov ecx,45h 13 01311397 mov eax,0CCCCCCCCh 14 0131139C rep stos dword ptr es:[edi] 15 int i0 = 11; 16 0131139E mov dword ptr [i0],0Bh 17 18 const int i = 0; //定義常量i 19 013113A5 mov dword ptr [i],0 //編譯器確實為常量i分配了棧空間,並賦值為0 20 int *j = (int *) &i; //看到這里能對i進行取值,判斷i必然后自己的內存空間 21 013113AC lea eax,[i] 22 013113AF mov dword ptr [j],eax 23 *j = 1; //對j指向的內存進行修改 24 013113B2 mov eax,dword ptr [j] 25 013113B5 mov dword ptr [eax],1 26 printf("0x%p\n0x%p\n%d\n%d\n",&i,j,i,*j); //觀看實驗效果 27 013113BB mov esi,esp 28 013113BD mov eax,dword ptr [j] 29 013113C0 mov ecx,dword ptr [eax] 30 013113C2 push ecx 31 013113C3 push 0 32 013113C5 mov edx,dword ptr [j] 33 013113C8 push edx 34 013113C9 lea eax,[i] 35 013113CC push eax 36 013113CD push offset string "0x%p\n0x%p\n%d\n%d\n" (131573Ch) 37 013113D2 call dword ptr [__imp__printf (13182B0h)] 38 013113D8 add esp,14h 39 013113DB cmp esi,esp 40 013113DD call @ILT+295(__RTC_CheckEsp) (131112Ch) 41 42 const int ck = 9; //這個對照實驗是為了觀察,對常量ck的引用時,會產生的效果 43 013113E2 mov dword ptr [ck],9 //為常量分配棧空間,這是符號表中已經有了這個變量 44 int ik = ck; 45 013113E9 mov dword ptr [ik],9 //看到否,對常量ck的引用,會直接替換為常量的值9,這種替換很類似於宏替換,再看下面的實驗 46 47 int i1 = 5; //這個對照實驗是為了區別,對常量和變量的引用有什么區別 48 013113F0 mov dword ptr [i1],5 49 int i2 = i1; //這里引用變量i1,對i2進行賦值,然后看到否,對常量i1引用沒有替換成i1的值,而是去棧中先取出i1的值,到edx寄存器中,然后再把值mov到i2所在的內存中 50 013113F7 mov eax,dword ptr [i1] 51 013113FA mov dword ptr [i2],eax 52 53 return 0; 54 013113FD xor eax,eax 55 }
通過上述實驗的分析可以容易看出,對可折疊的常量的引用會被替換為該常量的值,而對變量的引用就需要訪問變量的內存。
總結:常量折疊說的是,在編譯階段,對該變量進行值替換,同時,該常量擁有自己的內存空間,並非像宏定義一樣不分配空間,需澄清這點
前面說了個不許用gcc編譯.c的C語言的程序,不然就沒有了常量折疊的問題,先看一下執行的結果:
最后說一下gcc編譯的C語言的const常量,這里並沒有做常量折疊的這種優化,類似於const常量前面加上volatile這個關鍵字。具體是怎么回事?下一篇博客再說。