C++和C的混合編譯的項目實踐
簡介
C++
語言的創(chuàng)建初衷是 “a better C”,但是這并不意味著 C++
中類似 C
語言的全局變量和函數所采用的編譯和連接方式與 C
語言完全相同。作為一種欲與 C
兼容的語言, C++
保留了一部分過程式語言的特點(被世人稱為"不徹底地面向對象"),因而它可以定義不屬于任何類的全局變量和函數。但是, C++
畢竟是一種面向對象的程序設計語言,為了支持函數的重載, C++
對全局函數的處理方式與 C
有明顯的不同。
本文將介紹如何通過 extern “C” 關鍵字在 C++
中支持 C
語言 和 在C
語言中如何支持 C++
。
某企業(yè)曾經給出如下的一道面試題
為什么標準頭文件都有類似以下的結構?
//head.h #ifndef HEAD_H #define HEAD_H #ifdef __cplusplus extern "C" { #endif /*...*/ #ifdef __cplusplus } #endif #endif /* HEAd_H */
問題分析
- 這個頭文件head.h可能在項目中被多個源文件包含(#include “head.h”),而對于一個大型項目來說,這些冗余可能導致錯誤,因為一個頭文件包含類定義或inline函數,在一個源文件中head.h可能會被#include兩次(如,a.h頭文件包含了head.h,而在b.c文件中#include a.h和head.h)——這就會出錯(在同一個源文件中一個結構體、類等被定義了兩次)。
- 從邏輯觀點和減少編譯時間上,都要求去除這些冗余。然而讓程序員去分析和去掉這些冗余,不僅枯燥且不太實際,最重要的是有時候又需要這種冗余來保證各個模塊的獨立。
為了解決這個問題,上面代碼中的
#ifndef HEAD_H #define HEAD_H /*……………………………*/ #endif /* HEAD_H */
就起作用了。如果定義了HEAD_H,#ifndef/#endif之間的內容就被忽略掉。因此,編譯時第一次看到head.h頭文件,它的內容會被讀取且給定HEAD_H一個值。之后再次看到head.h頭文件時,HEAD_H就已經定義了,head.h的內容就不會再次被讀取了。
那么下面這段代碼的作用又是什么呢?
#ifdef __cplusplus extern "C" { #endif /*.......*/ #ifdef __cplusplus } #endif
我們將在后面對此進行詳細說明。
關于 extern “C”
前面的題目中的 __cplusplus
宏,這是C++中已經定義的宏,是用來識別編譯器的,也就是說,將當前代碼編譯的時候,是否將代碼作為 C++
進行編譯。
首先從字面上分析extern “C”,它由兩部分組成:extern關鍵字、“C”。下面我就從這兩個方面來解讀extern "C"的含義。
首先,被它修飾的目標是 extern
的;其次,被它修飾的目標是 C
的。
extern關鍵字
被 extern “C” 限定的函數或變量是 extern
類型的。
extern是C/C++語言中表明函數和全局變量作用范圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變量和函數時只需包含模塊A的頭文件即可。這樣,模塊B中調用模塊A中的函數時,在編譯階段,模塊B雖然找不到該函數,但是并不會報錯;它會在連接階段中從模塊A編譯生成的目標代碼中找到此函數。
被extern修飾的函數,需要在編譯階段去鏈接該目標文件,并且與extern對應的關鍵字是 static,被static修飾的全局變量和函數只能在本模塊中使用。因此,一個函數或變量只可能被本模塊使用時,其一般是不可能被extern “C”修飾的。
**注意:**例如語句 extern int a;
僅僅是對變量的聲明,其并不是在定義變量 a
,聲明變量并未為 a
分配內存空間。定義語句形式為 int a;
變量 a
在所有模塊中作為一種全局變量只能被定義一次,否則會出現連接錯誤。
被 extern “C” 修飾的變量和函數是按照 C
語言方式編譯和連接的。
由于C++和C兩種語言的親密性,并且早期大量的庫都是由C語言實現的,所以不可避免的會出現在C++程序中調用C的代碼、C的程序中調用C++的代碼,但是它們各自的編譯和鏈接的規(guī)則是不同的。
函數名修飾
- 由于Windows下vs的修飾規(guī)則過于復雜,而Linux下gcc的修飾規(guī)則簡單易懂,下面我們使用了gcc演示了這個修飾后的名字。
- 通過下面我們可以看出gcc的函數修飾后名字不變。而g++的函數修飾后變成【_Z+函數長度+函數名+類型首字母】。
分別使用C的編譯器和C++的編譯器去編譯并獲得一個可執(zhí)行文件
使用C語言(gcc)編譯器編譯后結果
使用objdump -S 命令查看gcc生成的可執(zhí)行文件:
使用C++編譯器(g++)編譯后結果
使用objdump -S 命令查看g++生成的可執(zhí)行文件:
**linux:**修飾后的函數名= _Z + 函數名長度 + 形參類型首字母,Windows下也是相似的,細節(jié)上會有所不同,本質上都是通過函數參數信息去修飾函數名。
C++的編譯和鏈接方式
采用g++編譯完成后,函數的名字將會被修飾,編譯器將函數的參數類型信息添加到修改后的名字中,因此當相同函數名的函數擁有不用類型的參數時,在g++編譯器看來是不同的函數,而我們另一個模塊中想要調用這些函數也就必須使用C++的規(guī)則去鏈接函數(找修飾后的函數名)才能找到函數的地址。
C的編譯和鏈接方式
對于C程序,由于不支持重載,編譯時函數是未加任何修飾的,而且鏈接時也是去尋找未經修飾的函數名。
C和C++直接混合編譯時的鏈接錯誤
在C++程序,函數名是會被參數類型信息修飾的,這就造成了它們之間無法直接相互調用。
例如:
print(int)函數,使用g++編譯時函數名會被修飾為 _Z5printi,而使用gcc編譯時函數名則仍然是print,如果直接在C++中調用使用C編譯規(guī)則的函數,會鏈接錯誤,因為它會去尋找 _Z5printi而不是 print。
【C和C++的編譯和鏈接方式的不同】參考:
extern“C”的使用
extern "C"指令非常有用,因為C和C++的近親關系。注意:extern "C"指令中的C,表示的一種編譯和連接規(guī)約,而不是一種語言。
并且extern "C"指令僅指定編譯和連接規(guī)約,并不影響語義,編譯時仍是一個C++的程序,遵循C++的類型檢查等規(guī)則。
對于下面的代碼它們之間是有區(qū)別的
extern "C" void Add(int a, int b); //指定Add函數應該根據C的編譯和連接規(guī)約來鏈接 extern void Add(int a, int b); //聲明在Add是外部函數,鏈接的時候去調用Add函數
如果有很多內容要被加上extern “C”,你可以將它們放入extern “C”{ }中。
通過上面的分析,我們知道extern "C"的真實目的是實現類C和C++的混合編程,在C++源文件中的語句前面加上extern “C”,表明它按照類C的編譯和連接規(guī)約來編譯和連接,而不是C++的編譯的連接規(guī)約。這樣在類C的代碼中就可以調用C++的函數or變量等。
那么混合編譯首先要處理的問題就是要讓我們所寫的C++程序和C程序函數的編譯時的修飾規(guī)則和鏈接時的修飾規(guī)則保持一致。
總共就有下面四種情況,也就是說一個C的庫,應該能同時被C和C++調用,而一個C++的庫也應能夠同時兼容C和C++。
為了展示如上四種情況,我們分別建立一個C靜態(tài)庫和C++靜態(tài)庫。
C程序調用C的庫,C++程序調用C++的庫,這是理所應當的,因此我們關注的問題是如何交叉調用。
用法舉例
靜態(tài)庫是什么
庫是寫好的現有的,成熟的,可以復用的代碼。現實中每個程序都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,因此庫的存在意義非同尋常
之所以稱為【靜態(tài)庫】,是因為在鏈接階段,會將匯編生成的目標文件.o與引用到的庫一起鏈接打包到可執(zhí)行文件中。因此對應的鏈接方式稱為靜態(tài)鏈接。
試想一下,靜態(tài)庫與匯編生成的目標文件一起鏈接為可執(zhí)行文件,那么靜態(tài)庫必定跟.o文件格式相似。其實一個靜態(tài)庫可以簡單看成是一組目標文件(.o/.obj文件)的集合,即很多目標文件經過壓縮打包后形成的一個文件。靜態(tài)庫特點總結:
- 靜態(tài)庫對函數庫的鏈接是放在編譯時期完成的。
- 程序在運行時與函數庫再無瓜葛,移植方便。
- 浪費空間和資源,因為所有相關的目標文件與牽涉到的函數庫被鏈接合成一個可執(zhí)行文件。
靜態(tài)庫在程序編譯時會被連接到目標代碼中,程序運行時將不再需要該靜態(tài)庫,因此體積較大
創(chuàng)建C靜態(tài)庫
我們以一個棧的靜態(tài)庫為例:
首先新建項目Stack_C
新建源文件和頭文件
寫好棧的代碼
注意一定是C程序,即源文件后綴為c
更改輸出文件類型
右鍵項目名稱—>屬性
更改為配置類型為靜態(tài)庫
生成靜態(tài)庫
查看是否生成成功
VS一般在項目路徑下的x64\Debug路徑下:
至此,靜態(tài)庫已經可以成功建立了。
再新建一個項目,寫一個去調用該靜態(tài)庫實現的棧的程序(以括號匹配問題為例)
不過對于VS我們的靜態(tài)庫是默認不去使用的,因此我們需要將靜態(tài)庫的路徑和庫的名稱分別添加到庫目錄和依賴項,才能讓程序能去調用該靜態(tài)庫。
更改鏈接器配置
右鍵項目名—>點擊屬性
“屬性面板“—>”配置屬性”—> “鏈接器”—>”常規(guī)”,附加依賴庫目錄中輸入,靜態(tài)庫所在目錄;
增加庫目錄(路徑為我們剛剛生成的靜態(tài)庫所在的Debug文件夾)
增加附加依賴項
名稱為Stack_C項目生成的靜態(tài)庫名,一般是項目名 + .lib
“屬性面板”—>”配置屬性”—> “鏈接器”—>”輸入”,附加依賴庫中輸入靜態(tài)庫名StaticLibrary.lib。
我們先嘗試使用C程序來調用該靜態(tài)庫
新建項目
將源文件后綴改為c;包含上Stack_C項目(靜態(tài)庫項目)的頭文件;點擊生成解決方案;
成功生成,說明成功調用。
嘗試使用C++程序調用C靜態(tài)庫
- 將源文件后綴改為cpp;
- 頭文件保持不變;
- 點擊生成解決方法
結果報錯了:
這說明在鏈接的過程中出現了問題,也就是在我們的程序找不到靜態(tài)庫中函數的地址,原因是我們的靜態(tài)庫是C語言的,沒有對函數進行修飾,但在我們的調用方是C++程序,在鏈接過程中找的是修飾過的函數名,因此無法找到函數的地址。
既然C語言的靜態(tài)庫只能按照C的規(guī)則去編譯這些函數(即不修飾函數名),那么我們只要讓C++程序按照C語言的鏈接規(guī)則(即找未經修飾的函數名)去找到函數名不就解決了?
兩種思路:
- 改變C庫的編譯和鏈接方式為C++規(guī)則;
- 改變C++程序調用庫函數的編譯和鏈接方式為C的規(guī)則;
方法1是不行的,因為C語言中可沒有extern “C++”這種東西,那么考慮方法2;
這時我們可以借助extern“C”改變C++程序的鏈接規(guī)則,讓C++去按照C的規(guī)則去找函數名,即未經過任何修飾的函數名,那就一定能找到函數的地址,來去正確調用靜態(tài)庫。
在源文件test.cpp
使用extern “C”,去改變包含的頭文件中的函數的鏈接規(guī)則
//調用庫的的模塊的頭文件包含 extern "C" { #include"..\..\Stack_C\Stack_C\stack.h" } //程序的代碼 //...
那么在test.cpp去鏈接函數時,就會直接去找原函數名。
這樣就解決了。
還有一個一步到位的解決方法,利用條件編譯,根據當前程序的類型,選擇是否去執(zhí)行extern “C”指令。
- 調用方是C程序,不做處理;
- 調用方是C++程序,需要使用extern“C”將程序改為C的鏈接規(guī)則;
//調用庫的的模塊的頭文件包含 #ifdef __cplusplus//如果是c++程序,就執(zhí)行extern “C”,使用C的鏈接方式,去找未經修飾的函數名 extern "C"{ #endif #include"..\..\Stack_C\Stack_C\stack.h" #ifdef __cplusplus } #endif //程序的代碼 //...
但是這樣的處理不太好,我們作為調用方自然是想可以直接通過頭文件包含的方式去使用庫里的函數,因此采用下列方法,更改庫的頭文件函數聲明為:
#ifdef __cplusplus//如果定義了宏__cplusplus就執(zhí)行#ifdef 到 #endif之間的語句 extern "C" { #endif void StackInit(struct Stack* s); void StackPush(struct Stack* s, DataType x); void StackPop(struct Stack* s); DataType StackTop(struct Stack* s); int StackSize(struct Stack* s); void StackDestory(struct Stack* s); bool StackEmpty(struct Stack* s); #ifdef __cplusplus } #endif
這樣的一段代碼,無論是C++程序還是C程序都可以直接#include就能去調用該靜態(tài)庫了。
創(chuàng)建C++靜態(tài)庫
步驟和創(chuàng)建C的靜態(tài)庫相同,只不過要將項目中的源文件后綴改為cpp,就會生成一個C++的靜態(tài)庫,因此不再闡述。
創(chuàng)建完成后,我們仍使用剛剛的項目,并且添加C++靜態(tài)庫路徑到庫目錄,添加C++靜態(tài)庫名稱到附加依賴項,仍然以括號匹配問題為例去調用該庫。(記得刪除C靜態(tài)庫的庫目錄和附加依賴項,否則我們的程序有可能還會去調用C的靜態(tài)庫,這樣我們就無法探究如何去調用C++靜態(tài)庫的問題了)
嘗試使用C程序調用C++靜態(tài)庫
我們不著急調用,經過先前的經驗,這里可以判斷,C++的程序去調用C++的庫一定是沒問題的,但是C程序就不好說了,因此我們要搞定C程序調用C++庫的情況,先搞清楚它們的差異:
這里的C++程序去調用函數是去尋找修飾后的函數名,C程序是去找未修飾的函數名,要想讓它們保持一致有兩個思路:
改變C程序的編譯和鏈方式為C++的規(guī)則;改變C++靜態(tài)庫的編譯方式為C的規(guī)則;
但是方法1是不行的,之前也說過,C語言中沒有extern “C++”這種東西,那么考慮方法2;
對庫的頭文件中的函數做如下處理:
//用C的規(guī)則去搞庫的編譯和鏈接方式 extern "C" { void StackInit(struct Stack* s); void StackPush(struct Stack* s, DataType x); void StackPop(struct Stack* s); DataType StackTop(struct Stack* s); int StackSize(struct Stack* s); void StackDestory(struct Stack* s); bool StackEmpty(struct Stack* s); }
那么現在C++的靜態(tài)庫的函數名都是沒有經過修飾的。(C的規(guī)則)
但是我們去編譯仍然報錯:
error C2059: 語法錯誤:“字符串”
"StackInit”未定義;假設外部返回int
“StackPush”未定義;假設外部返回int
“StackEmpty”未定義;假設外部返回int
“StackTop”未定義;假設外部返回int
“StackPop”未定義;假設外部返回int
這是因為我們使用C程序時也包含了此頭文件,但是C語言中無法識別extern“C”,因此報錯。
我們嘗試使用條件編譯來決定是否使用extern“C”,根據調用方的不同改變函數鏈接規(guī)則:
- 調用方是C++程序,那么需要使用extern“C”將C++程序的函數鏈接規(guī)則變?yōu)镃的;
- 調用方是C程序,不使用extern“C”語句;
因此我們做如下處理,將庫的頭文件中的函數聲明加上:
#ifdef __cplusplus//如果定義了宏__cplusplus就執(zhí)行#ifdef 到 #endif之間的語句 extern "C" { #endif void StackInit(struct Stack* s); void StackPush(struct Stack* s, DataType x); void StackPop(struct Stack* s); DataType StackTop(struct Stack* s); int StackSize(struct Stack* s); void StackDestory(struct Stack* s); bool StackEmpty(struct Stack* s); #ifdef __cplusplus } #endif
總結:C++和C之間的混合編譯,為了消除函數名修飾規(guī)則不同的的差別,我們需要使用extern ”C“來改變C++的編譯和連接方式。
但這樣問題也隨之而來:
C++的庫就失去了函數重載的特性,如果庫中有同名函數,那么就無法正確編譯,因為按照C的方式去編譯,函數名會沖突。
如何解決這個問題呢?
實際上這個問題無法解決,一旦選擇了將某個函數指定了按照C的方式去編譯鏈接,那么這個函數就已經失去了重載的特性了,不過Cpp的庫中未被指定按照C的規(guī)則去編譯和鏈接的那些函數,仍然可以被重載,并且具有C++的一切特性。
因此這個問題無解,只有通過避免“一刀切”的方法來保護那些我們想重載的函數,也就是說一部分庫里的函數那就是實現給C程序調用的,我們就通過extern“C”改變它的編譯和鏈接方式,而對于那些實現給C++程序調用的函數接口,我們不做任何處理,并且不暴露給C程序。
想要實現上述過程,我們需要在靜態(tài)庫項目中創(chuàng)建兩個頭文件libc.h
和libcpp.h
,libc.h
聲明那些需要暴露給C程序的函數接口,并且使用上面介紹的條件編譯和extern“C”,libcpp.h
聲明那些暴露給給Cpp程序的函數接口,這樣兩個頭文件的函數的鏈接規(guī)范互不相同,也互不干擾。只需要將lic.h
在C程序調用的地方使用#include
包含,libcpp.h
在C++程序調用的地方使用#include
包含即可使用。
因此C++庫中哪個接口需要暴露給C,我們就用extern“C”修飾哪個接口。
總之,C的庫可以給C程序和C++程序調用,而C++庫也可以被C程序和C++程序調用
如果要滿足這個庫中所有的函數都能同時被C++和C調用,那么無論是C的庫還是C++的庫,最終這個庫的編譯和鏈接方式都只能是C的規(guī)范,因為C++可以使用C的鏈接規(guī)范但是C不能使用C++的鏈接規(guī)范,也就導致了如果庫的鏈接規(guī)范是C++的,那么無論如何,C程序都無法調用。
值得一提的是C++程序中的函數可以使用兩種鏈接規(guī)范,因此我們可以針對函數的使用場景來選擇該函數的編譯和鏈接規(guī)范,使得一部分函數保留C++的特性,但一部分函數就只能為了兼容C而犧牲C++的特性,想要既兼容C又保留C++的特性,這是做不到的。
到此這篇關于C++和C的混合編譯的項目實踐的文章就介紹到這了,更多相關C++和C混合編譯內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
VS2019編寫C程序或者CUDA程序出現“無法啟動程序,系統(tǒng)找不到指定的文件”問題的詳細解決方法
這篇文章主要介紹了VS2019編寫C程序或者CUDA程序出現“無法啟動程序,系統(tǒng)找不到指定的文件”問題的詳細解決方法,文中通過圖文的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08