1. 程式人生 > 遊戲攻略 >《原神攻略》若水弓適合什麼角色?若水弓適用角色介紹

《原神攻略》若水弓適合什麼角色?若水弓適用角色介紹

簡介

C++ 語言的建立初衷是 "a better C",但是這並不意味著 C++ 中類似 C 語言的全域性變數和函式所採用的編譯和連線方式與 C 語言完全相同。作為一種欲與 C 相容的語言, C++ 保留了一部分過程式語言的特點(被世人稱為"不徹底地面向物件"),因而它可以定義不屬於任何類的全域性變數和函式。但是, C++ 畢竟是一種面向物件的程式設計語言,為了支援函式的過載, C++ 對全域性函式的處理方式與 C 有明顯的不同。

本文將介紹如何通過 extern "C" 關鍵字在 C++ 中支援 C 語言 和 在C語言中如何支援 C++

某企業曾經給出如下的一道面試題

為什麼標準標頭檔案都有類似以下的結構?

//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++的程式碼,但是它們各自的編譯和連結的規則是不同的。

函式名修飾

  1. 由於Windows下vs的修飾規則過於複雜,而Linux下gcc的修飾規則簡單易懂,下面我們使用了gcc演示了這個修飾後的名字。
  2. 通過下面我們可以看出gcc的函式修飾後名字不變。而g++的函式修飾後變成【_Z+函式長度+函式名+型別首字母】。

分別使用C的編譯器和C++的編譯器去編譯並獲得一個可執行檔案

  • 使用C語言(gcc)編譯器編譯後結果

使用objdump -S 命令檢視gcc生成的可執行檔案:

  • 使用C++編譯器(g++)編譯後結果

使用objdump -S 命令檢視g++生成的可執行檔案:

linux:修飾後的函式名= _Z + 函式名長度 + 形參型別首字母,Windows下也是相似的,細節上會有所不同,本質上都是通過函式引數資訊去修飾函式名。

C++的編譯和連結方式

採用g++編譯完成後,函式的名字將會被修飾,編譯器將函式的引數型別資訊新增到修改後的名字中,因此當相同函式名的函式擁有不用型別的引數時,在g++編譯器看來是不同的函式,而我們另一個模組中想要呼叫這些函式也就必須使用C++的函式名修飾規則去連結函式(找修飾後的函式名)才能找到函式的地址。

C的編譯和連結方式

對於C程式,由於不支援過載,編譯時函式是未加任何修飾的,而且連結時也是去尋找未經修飾的函式名。

C和C++直接混合編譯時的連結錯誤

在C++程式,函式名是會被引數型別資訊修飾的,這就造成了它們之間無法直接相互呼叫。

例如:

print(int)函式,使用g++編譯時函式名會被修飾為 _Z5printi,而使用gcc編譯時函式名則仍然是print,如果直接在C++中呼叫使用C編譯規則的函式,會連結錯誤,因為它會去尋找 _Z5printi而不是 print。

【C和C++的編譯和連結方式的不同】參考:

C++的函式過載 - 吳秦 - 部落格園

extern“C”的使用

extern "C"指令非常有用,因為C和C++的近親關係。注意:extern "C"指令中的C,表示的一種編譯和連線規約,而不是一種語言。

並且extern "C"指令僅指定編譯和連線規約,並不影響語義,編譯時仍是一個C++的程式,遵循C++的型別檢查等規則。

對於下面的程式碼它們之間是有區別的

extern "C" void Add(int a, int b);
//指定Add函式應該根據C的編譯和連線規約來連結
extern void Add(int a, int b);
//宣告在Add是外部函式,連結的時候去呼叫Add函式

如果有很多內容要被加上extern “C”,你可以將它們放入extern “C”{ }中。

通過上面的分析,我們知道extern "C"的真實目的是實現類C和C++的混合程式設計,在C++原始檔中的語句前面加上extern "C",表明它按照類C的編譯和連線規約來編譯和連線,而不是C++的編譯的連線規約。這樣在類C的程式碼中就可以呼叫C++的函式or變數等。

那麼混合編譯首先要處理的問題就是要讓我們所寫的C++程式和C程式函式的編譯時的修飾規則連結時的修飾規則保持一致。

總共就有下面四種情況,也就是說一個C的庫,應該能同時被C和C++呼叫,而一個C++的庫也應能夠同時相容C和C++。

為了展示如上四種情況,我們分別建立一個C靜態庫和C++靜態庫

C程式能呼叫C的庫,C++程式能呼叫C++的庫,這是理所應當的,因此我們關注的問題是如何交叉呼叫

用法舉例

靜態庫是什麼


庫是寫好的現有的,成熟的,可以複用的程式碼。現實中每個程式都要依賴很多基礎的底層庫,不可能每個人的程式碼都從零開始,因此庫的存在意義非同尋常,之所以稱為【靜態庫】,是因為在連結階段,會將彙編生成的目標檔案.o與引用到的庫一起連結打包到可執行檔案中。因此對應的連結方式稱為靜態連結。

試想一下,靜態庫與彙編生成的目標檔案一起連結為可執行檔案,那麼靜態庫必定跟.o檔案格式相似。其實一個靜態庫可以簡單看成是一組目標檔案(.o/.obj檔案)的集合,即很多目標檔案經過壓縮打包後形成的一個檔案。靜態庫特點總結:

  • 靜態庫對函式庫的連結是放在編譯時期完成的。
  • 程式在執行時與函式庫再無瓜葛,移植方便。
  • 浪費空間和資源,因為所有相關的目標檔案與牽涉到的函式庫被連結合成一個可執行檔案。

靜態庫在程式編譯時會被連線到目的碼中,程式執行時將不再需要該靜態庫,因此體積較大

建立C靜態庫

我們以一個棧的靜態庫為例:

  • 首先新建專案Stack_C
  • 新建原始檔和標頭檔案
  • 寫好棧的程式碼

注意一定是C程式,即原始檔字尾為c

  • 更改輸出檔案型別

右鍵專案名稱—>屬性

  • 更改為配置型別為靜態庫
  • 生成靜態庫
  • 檢視是否生成成功

VS一般在專案路徑下的x64\Debug路徑下:

至此,靜態庫已經可以成功建立了。

  • 再新建一個專案,寫一個去呼叫該靜態庫實現的棧的程式(以括號匹配問題為例)

不過對於VS我們的靜態庫是預設不去使用的,因此我們需要將靜態庫的路徑和庫的名稱分別新增到庫目錄和依賴項,才能讓程式能去呼叫該靜態庫。

  • 更改連結器配置

右鍵專案名—>點選屬性

“屬性面板“—>”配置屬性”—> “連結器”—>”常規”,附加依賴庫目錄中輸入,靜態庫所在目錄;

增加庫目錄(路徑為我們剛剛生成的靜態庫所在的Debug資料夾)

  • 增加附加依賴項

名稱為Stack_C專案生成的靜態庫名,一般是專案名 + .lib

“屬性面板”—>”配置屬性”—> “連結器”—>”輸入”,附加依賴庫中輸入靜態庫名StaticLibrary.lib。

我們先嚐試使用C程式來呼叫該靜態庫

新建專案

  1. 將原始檔字尾改為c;
  2. 包含上Stack_C專案(靜態庫專案)的標頭檔案;
  3. 點選生成解決方案;

成功生成,說明成功呼叫。

嘗試使用C++程式呼叫C靜態庫


  1. 將原始檔字尾改為cpp;
  2. 標頭檔案保持不變;
  3. 點選生成解決方法;

結果報錯了:

這說明在連結的過程中出現了問題,也就是在我們的程式找不到靜態庫中函式的地址,原因是我們的靜態庫是C語言的,沒有對函式進行修飾,但在我們的呼叫方是C++程式,在連結過程中找的是修飾過的函式名,因此無法找到函式的地址。

既然C語言的靜態庫只能按照C的規則去編譯這些函式(即不修飾函式名),那麼我們只要讓C++程式按照C語言的連結規則(即找未經修飾的函式名)去找到函式不就解決了?

首先可以確定的是,C的庫所遵守的規則必然是C的,那麼C程式來呼叫該庫是沒問題的,但是如果是C++呼叫該庫就會由於雙方遵守的規則的不同而連結錯誤。

解決的兩種思路:

  1. 改變C庫的編譯和連結方式為C++規則;
  2. 改變C++程式呼叫庫函式的編譯和連結方式為C的規則;

方法1是不行的,因為C語言中可沒有extern “C++”這種東西,那麼考慮方法2;

這時我們可以藉助extern“C”改變C++程式的連結規則,讓C++去按照C的規則去找函式名,即未經過任何修飾的函式名,那就一定能找到函式的地址,來去正確呼叫靜態庫。

在原始檔test.cpp使用extern “C”,去改變包含的標頭檔案中的函式的連結規則。

//呼叫庫的的模組的標頭檔案包含
extern "C"
{
	#include"..\..\Stack_C\Stack_C\stack.h"
}
//程式的程式碼
//...

那麼在test.cpp去連結這些庫函式時,就會直接去找未被修飾的原函式名。

這樣就解決了。

還有一個一步到位的解決方法,利用條件編譯,根據當前程式的型別,選擇是否去執行extern “C”指令。

  • 呼叫方是C程式,不做處理;
  • 呼叫方是C++程式,需要使用extern“C”將程式改為C的連結規則;
//呼叫庫的的模組的標頭檔案包含
#ifdef __cplusplus//如果是c++程式,就執行extern “C”,使用C的連結方式,去找未經修飾的函式名
extern "C"{
#endif
#include"..\..\Stack_C\Stack_C\stack.h"
#ifdef __cplusplus
}
#endif
//程式的程式碼
//...

但是這樣的處理不太好,我們作為呼叫方自然是想可以直接通過標頭檔案包含的方式就能使用庫裡的函式,因此採用下列方法,更改庫的標頭檔案函式宣告為:

#ifdef __cplusplus//如果定義了巨集__cplusplus就執行#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的靜態庫,條件編譯會忽略extern“C”,並且它的編譯和連結規則是無法改變的,只能是C的規則。

呼叫方的規則

  1. 若是C程式去呼叫,條件編譯忽略掉extern“C”,程式不會報錯,並且C程式所遵守的規則與庫的規則是一致的,可以正常呼叫;
  2. 若是C++程式去呼叫,條件編譯extern“C”就會生效,使得呼叫方去使用這幾個庫函式時的規則不再遵守C++的規則而是C的規則,從而可以正常呼叫;

這樣的一段程式碼,無論是C++程式還是C程式都可以直接#include標頭檔案路徑就能去呼叫該靜態庫了。

建立C++靜態庫


步驟和建立C的靜態庫相同,只不過要將專案中的原始檔字尾改為cpp,就會生成一個C++的靜態庫,因此不再闡述。

建立完成後,我們仍使用剛剛的專案,並且新增C++靜態庫路徑到庫目錄,新增C++靜態庫名稱到附加依賴項,仍然以括號匹配問題為例去呼叫該庫。(記得刪除C靜態庫的庫目錄和附加依賴項,否則我們的程式有可能還會去呼叫C的靜態庫,這樣我們就無法探究如何去呼叫C++靜態庫的問題了)

嘗試使用C程式呼叫C++靜態庫

我們不著急呼叫,經過先前的經驗,這裡可以判斷,C++的程式去呼叫C++的庫一定是沒問題的,但是C程式就不好說了,因此我們要搞定C程式呼叫C++庫的情況,先搞清楚它們的差異:

首先C++的庫若不經任何處理,那麼它編譯連結規則一定是遵守C++的規範的,但是這樣的話C程式呼叫它,無論如何也無法正常呼叫的。

那麼換種思路,使用extern“C”讓C++的庫的規則改為C的規則,那麼這樣C程式是一定可以呼叫該C++的庫的,而C++程式則會因為雙方所遵守的規則不同而連結錯誤。

這樣的話相當於C++庫被改為了C的庫,仍然使用C++程式呼叫C的庫的解決方案:

  • 改變C++程式呼叫庫函式的編譯和連結方式為C的規則;

如果對庫的標頭檔案中的函式做如下處理:

//用C的規則去搞 庫的編譯和連結方式
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++的靜態庫的函式名都是沒有經過修飾的。(C的規則)

但是我們去編譯仍然報錯:

error C2059: 語法錯誤:“字串”

"StackInit”未定義;假設外部返回int

“StackPush”未定義;假設外部返回int

“StackEmpty”未定義;假設外部返回int

“StackTop”未定義;假設外部返回int

“StackPop”未定義;假設外部返回int

這是因為我們使用C程式時也包含了此標頭檔案,標頭檔案展開後C語言中無法識別extern“C”,因此報錯。

我們嘗試使用條件編譯來決定是否使用extern“C”,根據呼叫方的不同改變函式連結規則:

  • 呼叫方是C++程式,那麼需要使用extern“C”將C++程式的函式連結規則變為C的;
  • 呼叫方是C程式,不使用extern“C”語句做處理;

因此我們做如下處理,將庫的標頭檔案中的函式宣告加上:

#ifdef __cplusplus//如果定義了巨集__cplusplus就執行#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語言的編譯和連結規則的。

呼叫方的規則

  1. 如果是C的程式來呼叫,呼叫方的extern“C”被條件編譯忽略,庫和呼叫方的規則是一致的,可以正常呼叫;
  2. 如果是C++的程式來呼叫,那麼呼叫方的extern “C”就會發揮作用,讓呼叫方也是遵守C的規則,與庫的規則一致,就可以正常呼叫了;

總結:C++和C之間的混合編譯,為了消除函式名修飾規則不同的的差別,我們需要使用extern ”C“來改變C++的編譯和連線方式。

但這樣問題也隨之而來:

被extern“C”的C++的庫函式就失去了函式過載的特性,如果庫的這些函式中有同名函式,那麼就無法正確編譯,因為按照C的方式去編譯,函式名會衝突。

如何解決這個問題呢?

實際上這個問題無法解決,一旦選擇了將某個函式指定了按照C的方式去編譯連結,那麼這個函式就已經失去了過載的特性了,不過Cpp的庫中未被指定按照C的規則去編譯和連結的那些函式,仍然可以被過載,並且具有C++的一切特性。

因此這個問題無解,只有通過避免“一刀切”的方法來保護那些我們想過載的函式,也就是說一部分庫裡的函式是實現給C程式呼叫的,我們就通過extern“C”改變它的編譯和連結方式,而對於那些實現給C++程式呼叫的函式介面,我們不做任何處理,並且不暴露給C程式。

想要實現上述過程,我們需要在靜態庫專案中建立兩個標頭檔案libc.hlibcpp.hlibc.h宣告那些需要暴露給C程式的函式介面,並且使用上面介紹的條件編譯和extern“C”,libcpp.h宣告那些暴露給給Cpp程式的函式介面,這樣兩個標頭檔案的函式的連結規範互不相同,也互不干擾。只需要將lic.h在C程式呼叫的地方使用#include 包含,libcpp.h在C++程式呼叫的地方使用#include包含即可使用。

因此C++庫中哪個介面需要暴露給C,我們就用extern“C”修飾哪個介面。

總之,C的庫可以給C程式和C++程式呼叫,而C++庫也可以被C程式和C++程式呼叫

如果要滿足這個庫中所有的函式都能同時被C++和C呼叫,那麼無論是C的庫還是C++的庫,最終這個庫的編譯和連結方式都只能是C的規範,因為C++可以使用C的連結規範但是C不能使用C++的連結規範,也就導致瞭如果庫的連結規範是C++的,那麼無論如何,C程式都無法呼叫。

值得一提的是C++程式中的函式可以使用兩種連結規範,因此我們可以針對函式的使用場景來選擇該函式的編譯和連結規範,使得一部分函式保留C++的特性,但一部分函式就只能為了相容C而犧牲C++的特性,想要既相容C又保留C++的特性,這是做不到的。