文章目錄
- 1、Terms34:如何在同一個程序中結合C++和C
- 1.1 名稱重整
- 1.2 statics的初始化
- 1.3 動態內存的分配
- 1.4 數據結構的兼容性
- 2、總結
- 3、參考
1、Terms34:如何在同一個程序中結合C++和C
在大型項目中一般都用C++進行開發,但是不可避免會用一些C語言進行底層的調用。在確定C++和C的編譯器都能產生兼容的目標文件之后你要重點考慮四件事情:名稱重整,statics的初始化,動態內存的分配,數據結構的兼容性。
1.1 名稱重整
一、問題引出
在 C++ 出現之前,很多實用的功能都是用 C 語言開發的,很多底層的庫也是用 C 語言編寫的,如果在 C++ 代碼中可以兼容 C 語言代碼,無疑能極大地提高 C++ 程序員的開發效率。但是在一個項目中,能否既包含 C++ 程序又包含 C 程序呢?
答案是可以的,但是要小心處理,因為C++ 和 C 在程序的編譯、鏈接等方面都存在一定的差異,這些差異往往會導致程序運行失敗。
C++編譯器會為程序中的每個函數編出獨一無二的名稱;但是在C語言中沒有必要,因為C語言不支持函數重載。但是發展到現在,C++項目中幾乎都會有一些函數擁有相同的名稱,但是重載并不兼容大部分鏈接器,因此名稱重整(修飾),是對鏈接器的一個讓步。
例如你有一個函數funca(),被編譯器重整為xyzzy,你可以使用funca()的名稱,沒人在乎編譯器重整的名稱。但是如果funca處于C函數庫中,你的C++原始代碼會有一個頭文件,有如下聲明。
void funca(int x1,int x2,int x3,int x4);
//調用未經重整的函數名稱
當你使用funca(a,b,c,d)時,你的目標文件內含的是這樣的代碼:
xyzzy(a,b,c,d)
//調用重整后的函數名稱
但是如果funca()是一個C函數,那么funca()代碼在目標文件會有一個名為funca的函數,名稱并未重整。當你試圖鏈接那個目標文件,就會獲得一個錯誤信息,因為鏈接器尋找xyzzy的函數,并不存在。
解決方法就是告訴C++編譯器,不要重整某些函數名稱。如果調用一個名為funca的C函數,它的真正名稱就叫做funca,你的目標代碼內含有一份reference,指向那個名稱,而非一個重整后的名稱。
舉個例子:
比如下面是一個用 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;
}
可見主程序mian.cpp文件是用 C++ 編寫的,而 display() 函數的定義myfun.c文件是用 C 編寫的。
表面上看這個項目很完整,但調用 GCC 編譯器運行此項目(見利用GCC編譯器編譯C/C++程序),提示錯誤信息如下:
In function `main': undefined reference to `display()'
它表示編譯器無法找到 main.cpp 文件中 display() 函數的實現代碼。
導致此錯誤的原因,是 C++ 和 C 編譯程序時,對函數名的處理方式不同。
(1)通過函數重載詳解可知,C++ 之所以支持函數的重載,是因為在程序的編譯階段,C++會對函數的函數名進行“重命名”,比如:
void Swap(int a, int b) 會被重命名為_Swap_int_int;
void Swap(float x, float y) 會被重命名為_Swap_float_float。
(2)但是C 語言不支持函數重載,它不會在編譯階段對函數的名稱做較大的改動,比如:
void Swap(int a, int b) 會被重命名為_Swap;
void Swap(float x, float y)也會被重命名為_Swap。
不同的編譯器有不同的重命名方式,但根據 C++ 標準編譯后的函數名幾乎都由原有函數名和各個參數的數據類型構成,而根據 C 語言標準編譯后的函數名則僅由原函數名構成。
(3)這就意味著,使用 C 和 C++ 進行混合編程時,兩者對函數名的處理方式不同,勢必會造成編譯器在程序鏈接階段無法找到函數具體的實現,導致鏈接失敗。
(4)幸運的是,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 __cplusplusextern "C" void display();
#elsevoid 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 __cplusplusextern "C" {
#endifvoid display();#ifdef __cplusplus}
#endif
由此可以看出,extern “C” 大致有 2 種用法,當僅修飾一句 C++ 代碼時,直接將其添加到該函數代碼的開頭即可;如果用于修飾一段 C++ 代碼,只需為 extern “C” 添加一對大括號{},并將要修飾的代碼囊括到括號內即可。
1.2 statics的初始化
許多代碼會在main之前和之后執行代碼。更明確說,static class對象、全局對象、namespace內對象以及文件范圍(file scope)內的對象,其constructors總是在main之前執行,這個過程稱為static initialization。通過static initialization產生出來的對象,其destructors必須在所謂的static destruction過程中被調用。那是發生在main結束之后。
經過編譯的main,看起來像這樣:
int main()
{performStaticInitialization(); //此行由編譯器加入the statements you put in main go here;performStaticDestruction(); //此行由編譯器加入
}
重點是:如果一個C++編譯器采用這種方法來構造和析構對象,那么除非程序中有main,否則這種對象既不會被構造也不會被析構。
有時候,在C成分中撰寫main似乎比較合理——如果程序主要以C完成而C++只是個支持庫的話。盡管如此,C++程序庫中內含static對象仍是極有可能的,所以如果能夠,還是盡量在C++中撰寫main的好。然而這并非意味你需要重寫你的C代碼。只要將你的C main重新命名的realMain,然后讓C++ main調用realMain:
extern "C"
int realMain(int argc,char* argv[]); //以C語言完成此函數int main(int argc,char* argv[])
{realMain(argc,argv);
}
1.3 動態內存的分配
動態分配規則很簡單:程序的C++部分使用new和delete,程序的C部分則使用malloc和free。
其次,嚴密地將new/delete與malloc/free分隔開來。
有時候說比做容易很多,考慮粗糙(但好用)的strdup函數,它雖然并非C或C++標準的一份子,卻被廣泛使用:
char* strdup(const char* ps); //返回一個ps所指字符串的副本
strdup分配的內存必須由strdup的調用者負責釋放。如果它自C函數庫,使用free;如果它來自一個C++程序庫,那么應該用delete。因此調用strdup后,你應該做的事情不只隨系統的不同而不同,也隨編譯器的不用而不同。為了降低這種頭痛的移植問題,請避免調用標準程序庫以外的函數或是大部分計算平臺上尚未穩定的函數。
1.4 數據結構的兼容性
如果你的C++和C編譯器有著兼容的輸出,兩個語言的函數便可以安全的交換對象指針、non-member函數指針或者static函數指針。很自然的,structs以及內建類型的變量也可以安全跨越C++/C邊界。
對于struct來說沒如果只是加上一些非虛函數,其內存布局應該不會改變,如果加上虛函數,或者繼承也會改變struct的布局,所以一個struct如果帶有base structs(或classes),無法和C函數交換。
2、總結
- 確定你的C++和C編譯器產出兼容的目標文件(object files) 。
- '將雙方都使用的函數聲明為extern “C”。
- 如果可能,盡量在C++中撰寫main。
- 總是以delete刪除new返回的內存;總是以free釋放malloc返回的內存。
- 將兩個語言間的“數據結構傳遞”限制于C所能了解的形式;
- C++ structs如果內含非虛函數,倒是不受此限制。
3、參考
3.1 《More Effective C++》
3.2 如何在同一個程序中結合C++和C