簡單來說,extern 「C」是C++宣告或定義C語言符號的方法,是為了與C相容。說來容易,要理解起來還是得費些周折,首先我們要從C++和C的差別說起。
符號
大家都知道,從程式碼到執行程式需要經過編譯和連結兩個過程,其中編譯階段會做語法檢測,程式碼展開,另外它還會做一件事,就是將變數轉成符號,連結的時候其實是透過符號來定位的。編譯器在編譯C和C++程式碼時,將變數轉成符號的過程是不同的。本文所使用的編譯器為gcc4.4.7
我們先來看一段簡單的程式碼
/* hello.c */ #include <stdio.h> const char* g_prefix = "hello "; void hello(const char* name) { printf("%s%s", g_prefix, name); }
注意,這裡的檔名hello.c,我們執行編譯gcc -c hello.c得到目標檔hello.o,在Linux下用nm查看目標檔的符號表得到以下結果($符號代表shell命令提示字元)
$ nm hello.o 0000000000000000 D g_prefix 0000000000000000 T hello U printf
這是C程式碼編譯後的符號列表,其中第三列為編譯後的符號名,我們主要看自己定義的全域變數g_prefix和函數hello,它們的編譯後的符號名稱和程式碼裡的名字是一樣的。我們將hello.c重新命名為hello.cpp,重新編譯gcc -c hello.cpp得到hello.o,在用nm查看,結果如下
0000000000000000 T _Z5helloPKc U __gxx_personality_v0 0000000000000000 D g_prefix U printf
這是C++程式碼編譯後的符號列表,gcc會自動根據文件後綴名來識別C和C++程式碼,這時我們發現g_prefix的符號沒變,但函數hello的符號變成了_Z5helloPKc,這就說明gcc在編譯C和C++程式碼時處理方式是不一樣的,對於C程式碼,變數的符號名稱就是變數本身(在早期編譯器會為C程式碼變數前加底線_,現在預設都不會了,在編譯時可以透過編譯選項-fno-leading-underscore和-fleading-underscore來明確設定),而對於C++程式碼,如果是資料變數且沒有嵌套,符號名也是本身,如果變數名稱有嵌套(在名稱空間或類別中)或是函數名,符號名就會依下列規則來處理
1、 符號以_Z開始
2、 如果有嵌套,後面緊跟N,然後是名稱空間、類別、函數的名字,名字前的數字是長度,以E結尾
3 、 如果沒嵌套,則直接是名字長度後面跟著名字
4、 最後是參數列表,類型和符號對應關係如下
int -> i float -> f double -> d char -> c void -> v const -> K * -> P
這樣就很好理解為什麼C++程式碼裡的void hello(const char*)編譯之後符號為_Z5helloPKc(PKc翻譯成型別要從右到左翻譯為char const *,這是編譯器內部的表示方式,我們習慣的表示方式是const char*,兩者是一樣的),c++filt工具可以從符號反推名字,使用方法為c++filt _Z5helloPKc
這樣也很容易理解為什麼C++支援函數重載而C不支援了,因為C++將函數修飾為符號時把函數的參數型別加進去了,而C卻沒有,所以在C++下,即便函數名稱相同,只要參數不同,它們的符號名稱是不會衝突的。我們可以透過下面一個例子來驗證變數名和符號的這種關係。
/ * filename : test.cpp */ #include <stdio.h> namespace myname { int var = 42; } extern int _ZN6myname3varE; int main() { printf("%d\n", _ZN6myname3varE); return 0; }
這裡我們在名稱空間namespace定義了全域變數var,根據前面的內容,它會被修飾為符號_ZN6myname3varE,然後我們手動聲明了外部變數_ZN6myname3varE並將其列印出來。編譯並運行,它的值正好就是var的值
$ gcc test.cpp -o test -lstdc++ $ ./test 42
extern "C"
有了符號的概念我們再來看extern “C”的用法就很容易了
extern "C" { int func(int); int var; }
它的意思就是告訴編譯器將extern “C”後面的括號裡的程式碼當做C程式碼來處理,當然我們也可以用單條語句來聲明
extern "C" int func(int); extern "C" int var;
這樣就聲明了C類型的func和var。很多時候我們寫一個頭檔聲明了一些C語言的函數,而這些函數可能被C和C++程式碼調用,當我們提供給C++程式碼調用時,需要在頭檔裡加extern “C”,否則C++編譯的時候會找不到符號,而給C程式碼呼叫時又不能加extern “C”,因為C是不支援這樣的語法的,常見的處理方式是這樣的,我們以C的函式庫函數memset為例
#ifdef __cplusplus extern "C" { #endif void *memset(void*, int, size_t); #ifdef __cplusplus } #endif
其中__cplusplus是C++編譯器定義的一個宏,如果這份程式碼和C++一起編譯,那麼memset會在extern "C"裡被聲明,如果是和C程式碼一起編譯則直接聲明,由於__cplusplus沒有被定義,所以也不會有文法錯誤。這樣的技巧在系統頭檔裡常被用到。
更多C++中的extern 「C」用法詳解相關文章請關注PHP中文網!