extern "C":實現C++和C的混合程式設計

2020-07-16 10:05:28
通過《C語言和C++到底有什麼關係?》一節的學習,讀者已經了解了 C++ 和 C 語言之間的關係。簡單的理解,C++ 就是在 C 語言的基礎上增加了一些新特性,從大的方面講,C++ 不僅支援程序導向程式設計,還支援物件導向程式設計和泛型程式設計;從小的方面講,C++ 還支援名稱空間、函數過載、行內函式等。

在此基礎上,很多讀者都存在一個疑問,即在一個專案中,能否既包含 C++ 程式又包含 C 程式呢?換句話說,C++ 和 C 可以進行混合程式設計嗎?

要知道,在 C++ 出現之前,很多實用的功能都是用 C 語言開發的,很多底層的庫也是用 C 語言編寫的。這意味著,如果能在 C++ 程式碼中相容 C 語言程式碼,無疑能極大地提高 C++ 程式設計師的開發效率。

而恰恰答案也正是我們想要的,C++ 和 C 可以進行混合程式設計。但需要注意的是,由於 C++ 和 C 在程式的編譯、連結等方面都存在一定的差異,而這些差異往往會導致程式執行失敗。

舉個例子,如下就是一個用 C++ 和 C 混合程式設計實現的範例專案:
//myfun.h
void display();

//myfun.c
#include <stdio.h>
#include "myfun.h"
void display(){
   printf("C++:http://c.biancheng/net/cplus/");
}

//main.cpp
#include <iostream>
#include "myfun.h"
using namespace std;
int main(){
   display();
   return 0;
}
在此專案中,主程式是用 C++ 編寫的,而 display() 函數的定義是用 C 語言編寫的。從表面上看,這個專案很完整,我們可以嘗試執行它:

In function `main': undefined reference to `display()'

如上是呼叫 GCC 編譯器執行此專案時給出的錯誤資訊,指的是編譯器無法找到 main.cpp 檔案中 display() 函數的實現程式碼。導致此錯誤的原因,就是因為 C++ 和 C 編譯程式的方式存在差異。

通過學習《C++函數過載》一節我們知道,之所以 C++ 支援函數的過載,是因為 C++ 會在程式的編譯階段對函數的函數名進行“再次重新命名”,例如:
  • void Swap(int a, int b) 會被重新命名為_Swap_int_int
  • void Swap(float x, float y) 會被重新命名為_Swap_float_float
顯然通過重新命名,可以有效避免編譯器在程式連結階段無法找到對應的函數。

但是,C 語言是不支援函數過載的,它不會在編譯階段對函數的名稱做較大的改動。仍以 void Swap(int a, int b) 和 void Swap(float x, float y) 為例,若以 C 語言的標準對它們進行編譯,兩個函數的函數名將都是_Swap

不同的編譯器有不同的重新命名方式,但根據 C++ 標準編譯後的函數名幾乎都由原有函數名和各個引數的資料型別構成,而根據 C 語言標準編譯後的函數名則僅有原函數名構成。這裡僅僅舉例說明,實際情況可能並非如此。

這也就意味著,使用 C 和 C++ 進行混合程式設計時,考慮到對函數名的處理方式不同,勢必會造成編譯器在程式連結階段無法找到函數具體的實現,導致連結失敗。

幸運的是,C++ 給出了相應的解決方案,即借助 extern "C",就可以輕鬆解決 C++ 和 C 在處理程式碼方式上的差異性。

extern "C"

extern 是 C 和 C++ 的一個關鍵字,但對於 extern "C",讀者大可以將其看做一個整體,和 extern 毫無關係。

extern "C" 既可以修飾一句 C++ 程式碼,也可以修飾一段 C++ 程式碼,它的功能是讓編譯器以處理 C 語言程式碼的方式來處理修飾的 C++ 程式碼。

仍以本節前面的範例專案來說,main.cpp 和 myfun.c 檔案中都包含 myfun.h 標頭檔案,當程式進行預處理操作時,myfun.h 標頭檔案中的內容會被分別複製到這 2 個原始檔中。對於 main.cpp 檔案中包含的 display() 函數來說,編譯器會以 C++ 程式碼的編譯方式來處理它;而對於 myfun.c 檔案中的 display() 函數來說,編譯器會以 C 語言程式碼的編譯方式來處理它。

為了避免 display() 函數以不同的編譯方式處理,我們應該使其在 main.cpp 檔案中仍以 C 語言程式碼的方式處理,這樣就可以解決函數名不一致的問題。因此,可以像如下這樣來修改 myfun.h:
#ifdef __cplusplus
extern "C" void display();
#else
void display();
#endif
可以看到,當 myfun.h 被引入到 C++ 程式中時,會選擇帶有 extern "C" 修飾的 display() 函數;反之如果 myfun.h 被引入到 C 語言程式中,則會選擇不帶 extern "C" 修飾的 display() 函數。由此,無論 display() 函數位於 C++ 程式還是 C 語言程式,都保證了 display() 函數可以按照 C 語言的標準來處理。

再次執行該專案,會發現之前的問題消失了,可以正常執行:

C++:http://c.biancheng/net/cplus/


在實際開發中,對於解決 C++ 和 C 混合程式設計的問題,通常在標頭檔案中使用如下格式:
#ifdef __cplusplus
extern "C" {
#endif

void display();

#ifdef __cplusplus
}
#endif
由此可以看出,extern "C" 大致有 2 種用法,當僅修飾一句 C++ 程式碼時,直接將其新增到該函數程式碼的開頭即可;如果用於修飾一段 C++ 程式碼,只需為 extern "C" 新增一對大括號{},並將要修飾的程式碼囊括到括號內即可。