C++ 面試之 static 關鍵字
static是 c++ 的關鍵字,顧名思義是表示靜態的含義。它在 c++ 中既可以修飾變數也可以修飾函式。那當我們使用 static 時,編譯器究竟做了哪些事情呢?
早先面試中被問到 static 關鍵字,感覺既熟悉又陌生。熟悉是都知道如何去使用它,陌生又來自不知道它究竟對我們程式做了什麼。今天就來好好複習下這個關鍵字,本文的重點也在第三部分。
目錄
-
- 1.1extern 用於修飾變數
- 1.2extern 用於修飾函式
- 1.3extern 用於指定編譯型別
-
- 2.1static 用於修飾變數
- 2.2static 用於修飾函式
- 3 關於text 、bss 與data 段
先看一下示例程式碼:
test1.cpp
#include <iostream> extern int a_int; extern void func2(); static char c_array[10000]; void func1() { static int a_tmp = 0; std::cout << a_tmp++ << std::endl; return; } int main(int argc, char **argv) { a_int = 1; //靜態區域性變數示例 for (auto i = 0; i < 5; i++) { func1(); } //比較靜態全域性變數的地址示例 std::cout << static_cast<const void *>(c_array) << std::endl; func2(); return 0; }
test2.cpp
#include <iostream> int a_int; static char c_array[1000]; void func2() { std::cout << static_cast<const void *>(c_array) << std::endl; return 0; }
extern 關鍵字用於告訴編譯器,在其他的模組中尋找相應的定義
為什麼 static 前要先說extern 呢?因為他們就像相互對立的一對關鍵字,所以 extern 與 static 一起用時編譯器會報錯~
1.1extern 用於修飾變數
以示例程式碼中的 a_int 變數為例,假設其他的變數和函式不存在
我們先將 extern 關鍵字去掉(test1.cpp:2) ,然後執行步驟:
g++ -c -o test1.o test1.cpp nm test1.o
00000000000000d0 S _a_int
可以看到 a_int 為一個未初始化的符號。說明符號在 test1.o 中已經被定義了。此時直接編譯(g++ -o test1 test1.cpp
)是不會報錯的。
然後我們再將 extern 關鍵字加上(test1.cpp:2) ,並重覆上面步驟觀察符號
nm test1.o
會發現 test1.o 中沒有該符號的定義。並且再編譯會報錯:
Undefined symbols for architecture x86_64: "_a_int", referenced from: _main in test1-ed3c01.o ld: symbol(s) not found for architecture x86_64
很明顯,連結器沒有找到 a_int 的定義。此時只需要將 test2.o 加入再編譯(g++ -o test test1.cpp test2.cpp)就可以啦
注:此時如果去掉 main 函式中對 a_int 變數的引用,也可以編譯通過,畢竟 a_int 在程式中實際沒有用到
1.2extern 用於修飾函式
以示例程式碼中的 func2() 函式為例
因為在 main 函式中呼叫了 func2(),所以需要在 main 之前進行函式宣告。但此時的函式宣告無論加不加 extern 其實並無多少區別
1.3extern 用於指定編譯型別
因為 C++ 編譯時會進行name mangling[wiki] ,導致所看到的函式與實際編譯後的符號差距很大。在某些情況下會導致連結時找不到符號的問題
此時可以使用
extern "C" { ... }
這樣在範圍內的程式碼都將按照 C 的格式進行編譯
static 關鍵字在我看來的作用是
- 能夠改變變數的儲存方式
- 能夠改變變數與函式的訪問範圍
2.1static 用於修飾變數
我們都知道當程式經過編譯後:
- 函式體內的區域性變數會儲存在棧中,區域性變數隨著函式的呼叫和返回進行構造與析構,並且在函式返回後無法使用。
- 全域性變數儲存在靜態資料區直到程式退出時才會被析構掉。所以在整個程式內全域性變數都可以使用(當然要考慮到作用域)。
對於區域性變數 ,當我們在變數前加上static 時,就是告訴了編譯器將該變數放入靜態資料區。既函式退出時不會將該變數析構掉,當我們下次再呼叫改函式依然可以取得記憶體中的這個變數。
例如 test1.cpp 中,每次呼叫 func1() 時 a_tmp 變數都不會被銷燬,最後輸出
對於全域性變數 ,加上 static 關鍵字後該變數只能用於當前的檔案。
例如 test1.cpp 中的 c_array,加上 static 後只能在當前原始檔使用。
此時如果我們再在 test2.cpp 中定義一個同名的全域性靜態陣列進行編譯(g++ -o test test1.cpp test2.cpp
)並且輸出他們的地址
test1.cpp[c_array]0x10bb5e100 test2.cpp[c_array]0x10bb60810
可以看到兩個地址是不同的,所以雖說是同名的兩個全域性變數。但都經過 static 修飾後,他們實際還是兩個地址不同相互獨立的變數。
那麼再試一下,將 test1.cpp 中的
static char c_array[10000];
修改為
extern char c_array[10000];
然後再編譯(g++ -o test test1.cpp test2.cpp
)可以看到
Undefined symbols for architecture x86_64: "_c_array", referenced from: _main in test1-5d6201.o ld: symbol(s) not found for architecture x86_64
這當然是因為 test2.cpp 中的 c_array 還是有 static 進行修飾的,導致我們無法在 test1.cpp 檔案中訪問到。那就將 static 去掉,看到結果
test1.cpp[c_array]0x10b1e2100 test2.cpp[c_array]0x10b1e2100
它們的地址相同對應的同一塊記憶體,是同一個變數!
2.2static 用於修飾函式
static 對於函式於變數其實比較類似,它限定了函式只能在當前的模組中使用。
假如我們將 test2.cpp 中的 func2() 函式加上static
關鍵字,那麼編譯(g++ -o test test1.cpp test2.cpp
)也會報錯找不到符號
Undefined symbols for architecture x86_64: "func2()", referenced from: _main in test1-80a5c0.o ld: symbol(s) not found for architecture x86_64
3 關於text 、bss 與data 段
關於資料段、編譯、連結方面的知識非常推薦看看
<<程式設計師的自我修養:連結、裝載與庫>>3.1 區域性變數的編譯
是否曾經好奇函式內的臨時變數經過編譯會變成什麼樣子?
假設我們寫了如下程式碼,並編譯成名為 test 的可執行檔案
int main() { char s1[11] = "helloworld"; char s2[11] = "helloworld"; return 0; }
那麼可以通過objdump -DS test
觀察到 main 函式中有如下片段(有省略)
Disassembly of section .text: ..... 00000000004005b0 <main>: 4005b4:48 b8 68 65 6c 6c 6f movabs $0x726f776f6c6c6568,%rax 4005bb:77 6f 72 4005be:48 89 45 f0mov%rax,-0x10(%rbp) 4005c2:66 c7 45 f8 6c 64movw$0x646c,-0x8(%rbp) 4005c8:c6 45 fa 00movb$0x0,-0x6(%rbp) 4005cc:48 b8 68 65 6c 6c 6f movabs $0x726f776f6c6c6568,%rax 4005d3:77 6f 72 4005d6:48 89 45 e0mov%rax,-0x20(%rbp) 4005da:66 c7 45 e8 6c 64movw$0x646c,-0x18(%rbp) 4005e0:c6 45 ea 00movb$0x0,-0x16(%rbp) ......
觀察下 0x646c 和 0x726f776f6c6c6568,轉化成 ascii 就是
100 108 114 111 119 111 108 108 101 104
對應的字元
‘d’ ‘l’ ‘r’ ‘o’ ‘w’ ‘o’ ‘l’ ‘l’ ‘e’ ‘h’,看出來了吧,編譯器將 “helloworld” 以立即數的方式寫到了 text 段內。
然後通過readelf -a test
會發現並沒有 s1 與 s2 的符號。
現在將程式碼改為這樣又會如何?
static char s1[11] = "helloworld"; static char s2[11] = "helloworld";
繼續通過objdump -DS test
觀察發現 main 中發生了改變
Disassembly of section .text: 00000000004005b0 <main>: 4005b0:55push%rbp 4005b1:48 89 e5mov%rsp,%rbp 4005b4:b8 00 00 00 00mov$0x0,%eax 4005b9:5dpop%rbp 4005ba:c3retq 4005bb:0f 1f 44 00 00nopl0x0(%rax,%rax,1)
通過readelf -a test
可以看到新增了兩個地址不同的符號,由此可見 static 確實改變了變數的儲存方式
Symbol table '.symtab' contains 66 entries: Num:ValueSize TypeBindVisNdx Name 37: 000000000060102c11 OBJECTLOCALDEFAULT24 _ZZ4mainE2s2 38: 000000000060103711 OBJECTLOCALDEFAULT24 _ZZ4mainE2s1
那麼如果指向常量呢?稍微改下
const char *s1 = "helloworld"; const char *s2 = "helloworld";
繼續通過 objdump 觀察到 main 中有這兩程式碼,很明顯了 0x400660儲存著我們的 “helloworld”的字串常量
00000000004005b0 <main>: 4005b4:48 c7 45 f8 60 06 40 movq$0x400660,-0x8(%rbp) 4005bb:00 4005bc:48 c7 45 f0 60 06 40 movq$0x400660,-0x10(%rbp)
找到這個地址,發現這個地址屬於.rodata 段。這就是我們常說用來儲存字面值常量的資料段。
Disassembly of section .rodata: 0000000000400658 <__dso_handle>: ... 400660:68 65 6c 6c 6fpushq$0x6f6c6c65 400665:77 6fja4006d6 <__dso_handle+0x7e> 400667:72 6cjb4006d5 <__dso_handle+0x7d> 400669:64fs
觀察下十六進位制的值,就是我們的 “helloworld” 沒錯啦。
3.2 全域性變數的編譯
那麼對於全域性變數又應該是如何儲存的呢?
首先我們知道無論靜態還是非靜態的變數都應該儲存在靜態資料區。我們熟悉的靜態資料區就有.bss 和.data 。
.bss在編譯時實際上不佔據空間,只有在執行時才會由被分配空間。那麼還是來驗證下
char a_array[10000]; static char b_array[10000]; int main() { return 0; }
編譯一下(g++ -o test test.cpp
),然後通過 size 命令觀察(size test
)
textdatabssdechexfilename 1320588200482195655c4test
可以看出 a_array 和 b_array 都實際記錄在.bss
段,並且.data
段的大小顯然不符合我們定義的陣列大小。通過ll test
會發現檔案大小不足10000 位元組,所以可以肯定的是申請的這兩個陣列在編譯時併為被分配記憶體。
那麼繼續改一下看看
char a_array[10000] = "helloworld"; static char b_array[10000];
繼續使用sizetest
看下
textdatabssdechexfilename 132010616100322196855d0test
data 段和檔案都多出了 10000 多位元組!!!
這就是因為 a_array 進行了初始化,所以編譯器為其分配了記憶體。同理如果 b_array 也進行了初始化,那麼大小還會增加。
tips:
如果進行了初始化,但是記憶體中還是 0 值的話,編譯器依舊不會為其分配記憶體的,例如
int a_array[10000] = {0};
如果有人會看的話非常感謝能看到這裡~~
對於能指出文中的錯誤不勝感激! :D