1. 程式人生 > >C++中extern "C"含義深層探索

C++中extern "C"含義深層探索

內容整理from《C/C++精華》文稿,其具體作者不詳

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

面試題

為什麼標準標頭檔案都有類似以下的結構?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */

標頭檔案中的編譯巨集“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用
是防止該標頭檔案被重複引用。在下面討論

#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */

深層揭密 extern "C"
extern “C” 包含雙重含義,從字面上即可得到:

  • 被它修飾的目標是“extern”的:
    被 extern "C"限定的函式或變數是 extern 型別的,extern 是 C/C++語言中表明函式和全域性變數作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其宣告的函式和變數可以在本模組或其它模組中使用。
extern int a;

這僅僅是一個變數的宣告,其並不是在定義變數 a,並未為 a 分配記憶體空間。變數 a 在所有模組中作為一種全域性變數只能被定義一次,否則會出現連線錯誤。

通常,在模組的標頭檔案中對本模組提供給其它模組引用的函式和全域性變數以關鍵字 extern 宣告。例如,如果模組 B 欲引用該模組 A 中定義的全域性變數和函式時只需包含模組 A 的標頭檔案即可。這樣,模組 B 中呼叫模組 A 中的函式時,在編譯階段,模組 B 雖然找不到該函式,但是並不會報錯;它會在連線階段中從模組 A 編譯生成的目的碼中找到此函式。

與 extern 對應的關鍵字是 static,被它修飾的全域性變數和函式只能在本模組中使用。因此,一個函式或變數只可能被本模組使用時,其不可能被 extern “C”修飾。

  • 被它修飾的目標是“C”的:
    被 extern "C"修飾的變數和函式是按照 C 語言方式編譯和連線的

未加 extern “C”宣告時的編譯方式:
首先看看 C++中對類似 C 的函式是怎樣編譯的。作為一種面向物件的語言,C++支援函式過載,而過程式語言 C 則不支援。函式被 C++編譯後在符號庫中的名字與 C 語言的不同。例如,假設某個函式的原型為:

void foo( int x, int y );

該函式被 C 編譯器編譯後在符號庫中的名字為_foo,而 C++編譯器則會產生像_foo_int_int 之類的名字(不同的編譯器可能生成的名字不同,但是都採用了相同的機制,生成的新名字稱為“mangledname”)。_foo_int_int 這樣的名字包含了函式名、函式引數數量及型別資訊,C++就是靠這種機制來實現函式過載的。例如,在 C++中,函式 void foo( int x, int y )與 void foo( int x, float y )編譯生成的符號是不相同的,後者為foo_int_float。

同樣地,C++中的變數除支援區域性變數外,還支援類成員變數和全域性變數。使用者所編寫程式的類成員變數可能與全域性變數同名,我們以"."來區分。而本質上,編譯器在進行編譯時,與函式的處理相似,也為類中的變數取了一個獨一無二的名字,這個名字與使用者程式中同名的全域性變數名字不同。

未加 extern "C"宣告時的連線方式
假設在 C++中,模組 A 的標頭檔案如下:

// 模組 A 標頭檔案 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif

在模組 B 中引用該函式:

// 模組 B 實現檔案 moduleB.cpp
#include "moduleA.h"
foo(2,3);

實際上,在連線階段,聯結器會從模組 A 生成的目標檔案 moduleA.obj 中尋找_foo_int_int 這樣的符號!

加 extern "C"聲明後的編譯和連線方式
加 extern "C"聲明後,模組 A 的標頭檔案變為:

// 模組 A 標頭檔案 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif

在模組 B 的實現檔案中仍然呼叫 foo( 2,3 ),其結果是:

1.模組 A 編譯生成 foo 的目的碼時,沒有對其名字進行特殊處理,採用了 C 語言的方式;
2. 聯結器在為模組 B 的目的碼尋找 foo(2,3)呼叫時,尋找的是未經修改的符號名_foo。

可以用一句話概括 extern “C”這個宣告的真實目的(任何語言中的任何語法特性的誕生都不是隨意而為的,來源於真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎麼做的,還要問一問它為什麼要這麼做,動機是什麼,這樣我們可以更深入地理解許多問題):實現 C++與 C 及其它語言的混合程式設計。

extern "C"的慣用法
1.在 C++中引用 C 語言中的函式和變數,在包含 C 語言標頭檔案(假設為cExample.h)時,需進行下列處理:

extern "C"
{
#include "cExample.h"	//C到C++
}

而在 C 語言的標頭檔案中,對其外部函式只能指定為 extern 型別,C 語言中不支援 extern "C"宣告,在.c 檔案中包含了 extern "C"時會出現編譯語法錯誤。

而在 C 語言的標頭檔案中,對其外部函式只能指定為 extern 型別,C 語言中不支援 extern "C"宣告,在.c 檔案中包含了 extern "C"時會出現編譯語法錯誤。
C++引用 C 函式例子

/* c 語言標頭檔案:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c 語言實現檔案:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
	return x + y;
}
// c++實現檔案,呼叫 add:cppFile.cpp
extern "C"
{
	#include "cExample.h"
}
int main(int argc, char* argv[])
{
	add(2,3);
	return 0;
}

2.在 C 中引用 C++語言中的函式和變數時,C++的標頭檔案需新增 extern “C”,但是在 C 語言中不能直接引用聲明瞭 extern "C"的該標頭檔案,應該僅將 C 檔案中將 C++中定義的 extern "C"函式宣告為
extern 型別。

C 引用 C++函式例子

//C++標頭檔案 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif	//C++到C
//C++實現檔案 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
	return x + y;
}
/* C 實現檔案 cFile.c
/* 這樣會編譯出錯:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
	add( 2, 3 );
	return 0;
}