C 語言中的 extern "C"

    在你的 C 語言代碼中,不知能否看到類似下面的代碼:

    這好像沒有什麼問題,你應該還會想:“嗯⋯是啊,我們的代碼都是這樣寫的,從來沒有因此碰到過什麼麻煩啊~”。

    你說的沒錯,如果你的頭文件從來沒有被任何 C++ 程序引用過的話。

    這與 C++ 有什麼關係呢? 看看__cplusplus(注意前面是兩個下劃線) 的名字你就應該知道它與 C++ 有很大關係。__cplusplus 是一個 C++ 規範規定的預定義宏。你可以信任的是:所有的現代 C++ 編譯器都預先定義了它;而所有 C 語言編譯器則不會。另外,按照規範__cplusplus 的值應該等於 1 9 9 7 1 1 L ,然而不是所有的編譯器都照此實現,比如 g++ 編譯器就將它的值定義爲 1。

    所以,如果上述代碼被 C 語言程序引用的話,它的內容就等價於下列代碼。

    在這種情況下,既然 extern "C" { } 經過預處理之後根本就不存在,那麼它和 #include 指令之間的關係問題自然也就是無中生有。相關推薦: 整理的比較全面的 C 語言入門筆記!

extern "C" 的前世今生

    在 C++ 編譯器裏,有一位暗黑破壞神,專門從事一份稱作 “名字粉碎”(name mangling) 的工作。當把一個 C++ 的源文件投入編譯的時候,它就開始工作,把每一個它在源文件裏看到的外部可見的名字粉碎的面目全非,然後存儲到二進制目標文件的符號表裏。

    之所以在 C++ 的世界裏存在這樣一個怪物,是因爲 C++ 允許對一個名字給予不同的定義,只要在語義上沒有二義性就好。比如,你可以讓兩個函數是同名的,只要它們的參數列表不同即可,這就是函數重載 (function overloading);甚至,你可以讓兩個函數的原型聲明是完全相同的,只要它們所處的名字空間(namespace) 不一樣即可。事實上,當處於不同的名字空間時,所有的名字都是可以重複的,無論是函數名,變量名,還是類型名。

    另外,C++ 程序的構造方式仍然繼承了 C 語言的傳統:編譯器把每一個通過命令行指定的源代碼文件看做一個獨立的編譯單元,生成目標文件;然後,鏈接器通過查找這些目標文件的符號表將它們鏈接在一起生成可執行程序。相關文章:C 語言編譯過程。編譯和鏈接是兩個階段的事情。

    事實上,編譯器和鏈接器是兩個完全獨立的工具。編譯器可以通過語義分析知道那些同名的符號之間的差別;而鏈接器卻只能通過目標文件符號表中保存的名字來識別對象。

    所以,編譯器進行名字粉碎的目的是爲了讓鏈接器在工作的時候不陷入困惑,將所有名字重新編碼,生成全局唯一,不重複的新名字,讓鏈接器能夠準確識別每個名字所對應的對象。

    但 C 語言卻是一門單一名字空間的語言,也不允許函數重載,也就是說,在一個編譯和鏈接的範圍之內,C 語言不允許存在同名對象。比如,在一個編譯單元內部,不允許存在同名的函數,無論這個函數是否用 static 修飾;在一個可執行程序對應的所有目標文件裏,不允許存在同名對象,無論它代表一個全局變量,還是一個函數。所以,C 語言編譯器不需要對任何名字進行復雜的處理(或者僅僅對名字進行簡單一致的修飾(decoration),比如在名字前面統一的加上單下劃線_)。

    C++ 的締造者 Bjarne Stroustrup 在最初就把——能夠兼容 C,能夠複用大量已經存在的 C 庫——列爲 C++ 語言的重要目標。但兩種語言的編譯器對待名字的處理方式是不一致的,這就給鏈接過程帶來了麻煩。

    例如,現有一個名爲 my_handle.h 的頭文件,內容如下:

    然後使用 C 語言編譯器編譯 my_handle.c,生成目標文件 my_handle.o。由於 C 語言編譯器不對名字進行粉碎,所以在 my_handle.o 的符號表裏,這三個函數的名字和源代碼文件中的聲明是一致的。

    隨後,我們想讓一個 C++ 程序調用這些函數,所以,它也包含了頭文件 my_handle.h。假設這個 C++ 源代碼文件的名字叫 my_handle_client.cpp,其內容如下:

    其中,粗體的部分就是那三個函數的名字被粉碎後的樣子。

    然後,爲了讓程序可以工作,你必須將 my_handle.o 和 my_handle_client.o 放在一起鏈接。由於在兩個目標文件對於同一對象的命名不一樣,鏈接器將報告相關的 “符號未定義” 錯誤。

    爲了解決這一問題,C++ 引入了鏈接規範 (linkage specification) 的概念,表示法爲 extern"language string",C++ 編譯器普遍支持的 "language string" 有 "C" 和 "C++",分別對應 C 語言和 C++ 語言。

    鏈接規範的作用是告訴 C++ 編譯:對於所有使用了鏈接規範進行修飾的聲明或定義,應該按照指定語言的方式來處理,比如名字,調用習慣(calling convention)等等。

鏈接規範的用法有兩種

    1. 單個聲明的鏈接規範,比如:

    2. 一組聲明的鏈接規範,比如:

    對我們之前的例子而言,如果我們把頭文件 my_handle.h 的內容改成:

    然後使用 C++ 編譯器重新編譯 my_handle_client.cpp,所生成目標文件 my_handle_client.o 中的符號表就變爲:

    從中我們可以看出,此時,用 extern "C" 修飾了的聲明,其生成的符號和 C 語言編譯器生成的符號保持了一致。這樣,當你再次把 my_handle.o 和 my_handle_client.o 放在一起鏈接的時候,就不會再有之前的 “符號未定義” 錯誤了。

    但此時,如果你重新編譯 my_handle.c,C 語言編譯器將會報告 “語法錯誤”,因爲 extern"C" 是 C++ 的語法,C 語言編譯器不認識它。此時,可以按照我們之前已經討論的,使用宏__cplusplus 來識別 C 和 C++ 編譯器。修改後的 my_handle.h 的代碼如下:

小心門後的未知世界

    在我們清楚了 extern "C" 的來歷和用途之後,回到我們本來的話題上,爲什麼不能把 #include 指令放置在 extern "C" {...} 裏面?

    我們先來看一個例子,現有 a.h,b.h,c.h 以及 foo.cpp,其中 foo.cpp 包含 c.h,c.h 包含 b.h,b.h 包含 a.h,如下:

    現使用 C++ 編譯器的預處理選項來編譯 foo.cpp,得到下面的結果:

    正如你看到的,當你把 #include 指令放置在 extern "C" {} 裏的時候,則會造成 extern "C" {} 的嵌套。這種嵌套是被 C++ 規範允許的。當嵌套發生時,以最內層的嵌套爲準。比如在下面代碼中,函數 foo 會使用 C++ 的鏈接規範,而函數 bar 則會使用 C 的鏈接規範。

    如果能夠保證一個 C 語言頭文件直接或間接依賴的所有頭文件也都是 C 語言的,那麼按照 C++ 語言規範,這種嵌套應該不會有什麼問題。但具體到某些編譯器的實現,比如 MSVC2005,卻可能由於 extern "C" {} 的嵌套過深而報告錯誤。不要因此而責備微軟,因爲就這個問題而言,這種嵌套是毫無意義的。你完全可以通過把 #include 指令放置在 extern "C" {} 的外面來避免嵌套。拿之前的例子來說,如果我們把各個頭文件的 #include 指令都移到 extern "C" { } 之外,然後使用 C++ 編譯器的預處理選項來編譯 foo.cpp,就會得到下面的結果:

    這樣的結果肯定不會引起編譯問題的結果——即便是使用 MSVC。

    把 #include 指令放置在 extern "C" { } 裏面的另外一個重大風險是,你可能會無意中改變一個函數聲明的鏈接規範。比如:有兩個頭文件 a.h,b.h,其中 b.h 包含 a.h,如下:

    按照 a.h 作者的本意,函數 foo 是一個 C++ 自由函數,其鏈接規範爲 "C++"。但在 b.h 中,由於 #include "a.h" 被放到了 extern "C" { } 的內部,函數 foo 的鏈接規範被不正確地更改了。

    由於每一條 #include 指令後面都隱藏這一個未知的世界,除非你刻意去探索,否則你永遠都不知道,當你把一條條 #include 指令放置於 extern "C" {} 裏面的時候,到底會產生怎樣的結果,會帶來何種的風險。或許你會說,“我可以去查看這些被包含的頭文件,我可以保證它們不會帶來麻煩”。但,何必呢?畢竟,我們完全可以不必爲不必要的事情買單,不是嗎?

Q&A

Q: 難道任何 #include 指令都不能放在 extern "C" 裏面嗎?

A: 正像這個世界的大多數規則一樣,總會存在特殊情況。

    有時候,你可能利用頭文件機制 “巧妙” 的解決一些問題。比如,#pragma pack 的問題。這些頭文件和常規的頭文件作用是不一樣的,它們裏面不會放置 C 的函數聲明或者變量定義,鏈接規範不會對它們的內容產生影響。這種情況下,你可以不必遵守這些規則。

    更加一般的原則是,在你明白了這所有的原理之後,只要你明白自己在幹什麼,那就去做吧。

Q: 你只說了不應該放入 extern "C" 的,但什麼可以放入呢?    

A: 鏈接規範僅僅用於修飾函數和變量,以及函數類型。所以,嚴格的講,你只應該把這三種對象放置於 extern "C" 的內部。

    但,你把 C 語言的其它元素,比如非函數類型定義(結構體,枚舉等)放入 extern "C" 內部,也不會帶來任何影響。更不用說宏定義預處理指令了。

    所以,如果你更加看重良好組織和管理的習慣,你應該只在必須使用 extern "C" 聲明的地方使用它。即使你比較懶惰,絕大多數情況下,把一個頭件自身的所有定義和聲明都放置在 extern"C" 裏面也不會有太大的問題。

Q: 如果一個帶有函數 / 變量聲明的 C 頭文件裏沒有 extern "C" 聲明怎麼辦?

A: 如果你可以判斷,這個頭文件永遠不可能讓 C++ 代碼來使用,那麼就不要管它。

    但現實是,大多數情況下,你無法準確的推測未來。你在現在就加上這個 extern "C",這花不了你多少成本,但如果你現在沒有加,等到將來這個頭文件無意中被別人的 C++ 程序包含的時候,別人很可能需要更高的成本來定位錯誤和修復問題。

Q: 如果我的 C+ + 程序想包含一個 C 頭文件 a . h,它的內容包含了 C 的函數 / 變量聲明,但它們卻沒有使用 extern "C" 鏈接規範,該怎麼辦?

A: 在 a.h 裏面加上它。

    某些人可能會建議你,如果 a.h 沒有 extern "C",而 b.cpp 包含了 a.h,可以在 b.cpp 里加上 :

    這是一個邪惡的方案,原因在之前我們已經闡述。但值得探討的是,這種方案這背後卻可能隱含着一個假設,即我們不能修改 a.h。不能修改的原因可能來自兩個方面:

    對於第一種情況,不要試圖自己進行 workaround,因爲這會給你帶來不必要的麻煩。正確的解決方案是,把它當作一個 bug,發送缺陷報告給相應的團隊 或第三方公司。如果是自己公司的團隊或你已經付費的第三方公司,他們有義務爲你進行這樣的修改。如果他們不明白這件事情的重要性,告訴他們。如果這些頭文 件屬於一個免費開源軟件,自己進行正確的修改,併發布 patch 給其開發團隊。

    在第二種情況下,你需要拋棄掉這種不必要的安全意識。因爲,首先,對於大多數頭文件而言,這種修改都不是一種複雜的,高風險的修改,一切都在可控的範圍之 內;其次,如果某個頭文件混亂而複雜,雖然對於遺留系統的哲學應該是:“在它還沒有帶來麻煩之前不要動它”,但現在麻煩已經來了,逃避不如正視,所以上策 是,將其視作一個可以整理到乾淨合理狀態的良好機會。

Q: 我們代碼中關於 extern "C" 的寫法如下,這正確嗎?

A: 不確定。

    按照 C++ 的規範定義,__cplusplus 的值應該被定義爲 199711L,這是一個非零的值;儘管某些編譯器並沒有按照規範來實現,但仍然能夠保證__cplusplus 的值爲非零——至少我到目前爲止還沒有看到哪款編譯器將其實現爲 0。這種情況下,#if __cplusplus ... #endif 完全是冗餘的。

    但,C++ 編譯器的廠商是如此之多,沒有人可以保證某款編譯器,或某款編譯器的早期版本沒有將__cplusplus 的值定義爲 0。但即便如此,只要能夠保證宏__cplusplus 只在 C++ 編譯器中被預先定義 ,那麼,僅僅使用 #ifdef __cplusplus ⋯ #endif 就足以確保意圖的正確性;額外的使用 #if __cplusplus ... #endif 反而是錯誤的。

    只有在這種情況下:即某個廠商的 C 語言和 C++ 語言編譯器都預先定義了__cplusplus ,但通過其值爲 0 和非零來進行區分,使用 #if __cplusplus ... #endif 纔是正確且必要的。

    既然現實世界是如此複雜,你就需要明確自己的目標,然後根據目標定義相應的策略。比如:如果你的目標是讓你的代碼能夠使用幾款主流的、正確遵守了規範的編譯器進行編譯,那麼你只需要簡單的使用 #ifdef __cplusplus ... #endif 就足夠了。

    但如果你的產品是一個雄心勃勃的,試圖兼容各種編譯器的(包括未知的)跨平臺產品, 我們可能不得不使用下述方法來應對各種情況 ,其中__ALIEN_C_LINKAGE__是爲了標識那些在 C 和 C++ 編譯中都定義了__cplusplus 宏的編譯器。

    這應該可以工作,但在每個頭文件中都寫這麼一大串,不僅有礙觀瞻,還會造成一旦策略進行修改,就會到處修改的狀況。違反了 DRY(Don't Repeat Yourself) 原則,你總要爲之付出額外的代價。解決它的一個簡單方案是,定義一個特定的頭文件——比如 clinkage.h,在其中增加這樣的定義:

    以下舉例中 c 的函數聲明和定義分別在 cfun.h 和 cfun.c 中,函數打印字符串 “this is c fun call”,c++ 函數聲明和定義分別在 cppfun.h 和 cppfun.cpp 中,函數打印字符串 "this is cpp fun call", 編譯環境 vc2010.

C++ 調用 C 的方法

    c++ 調用 c 的方法,關鍵是要讓 c 的函數按照 c 的方式編譯,而不是 c++ 的方式。

(1) cfun.h 如下:

 cppfun.cpp 如下:

(2)cfun.h 同上

  cppfun.cpp 如下:

extern "C"

{

#include "cfun.h"// 注意 include 語句一定要單獨佔一行;

}

#include "cppfun.h"

#include 

using namespace std;

void cppfun()

{

cout<<"this is cpp fun call"<<endl;

}

int main()

{

    cfun();

return 0;

}

(3)cfun.h 如下:

#ifndef C_FUN_H

#define C_FUN_H

#ifdef __cplusplus

extern "C"

{

#endif

void cfun();

#ifdef __cplusplus

}

#endif

#endif

cppfun.cpp 如下:

#include "cfun.h"

#include "cppfun.h"

#include 

using namespace std;

void cppfun()

{

cout<<"this is cpp fun call"<<endl;

}

int main()

{

    cfun();

return 0;

}

C 調用 C++ 的方法

    c 調用 c++,關鍵是 C++ 提供一個符合 C 調用慣例的函數。

    在 vs2010 上測試時,沒有聲明什麼 extern 等,只在在 cfun.c 中包含 cppfun.h,然後調用 cppfun() 也可以編譯運行,在 gcc 下就編譯出錯,按照 c++/c 的標準這種做法應該是錯誤的。以下方法兩種編譯器都可以運行。

cppfun.h 如下:

#ifndef CPP_FUN_H

#define CPP_FUN_H

extern "C" void cppfun();

#endif

cfun.c 如下:

//#include "cppfun.h" // 不要包含頭文件,否則編譯出錯

#include "cfun.h"

#include <stdio.h>

void cfun()

{

printf("this is c fun call\n");

}

extern void cppfun();

int main()

{

#ifdef __cplusplus

    cfun();

#endif

    cppfun();

return 0;

}

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Qlc1Nu1N1o449T70UC1FGA