天天看點

關于 C++ 中的 extern “C“,你真的了解嗎

簡介

C++ 語言的建立初衷是 "a better C",但是這并不意味着 C++ 中類似 C 語言的全局變量和函數所采用的編譯和連接配接方式與 C 語言完全相同。

作為一種欲與 C 相容的語言, C++ 保留了一部分過程式語言的特點(被世人稱為"不徹底地面向對象"),因而它可以定義不屬于任何類的全局變量和函數。

但是, C++ 畢竟是一種面向對象的程式設計語言,為了支援函數的重載, C++ 對全局函數的處理方式與 C 有明顯的不同。本文将介紹 C++ 中如何通過 extern "C" 關鍵字支援 C 語言。

問題的引出

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

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

//incvxworks.h
#ifndef __INCvxWorksh
#define __INCvxWorksh

#ifdef __cplusplus
extern "C" {
#endif

/*...*/

#ifdef __cplusplus
}
#endif

#endif /* __INCvxWorksh */      

問題分析 對于上面問題,顯然,頭檔案中的編譯宏 #ifndef __INCvxWorksh 、 #define __INCvxWorksh 、 #endif 的作用是防止該頭檔案被重複引用。

那麼,

#ifdef __cplusplus
extern "C" {
#endif
和
#ifdef __cplusplus
}
#endif      

的作用又是什麼呢?我們将在後面對此進行詳細說明。

關于 extern "C"

前面的題目中的 __cplusplus 宏,是用來識别編譯器的,也就是說,将目前代碼編譯的時候,是否将代碼作為 C++ 進行編譯。如果是,則定義了 __cplusplus 宏。更多内容,這裡就不詳細說明了。 

而題目中的 extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目标是 extern 的;其次,被它修飾的目标是 C 的。

具體如下:被 extern "C" 限定的函數或變量是 extern 類型的。

extern 是 C/C++ 語言中表明函數和全局變量作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本子產品或其它子產品中使用。

注意,語句 extern int a; 僅僅是對變量的聲明,其并不是在定義變量 a ,聲明變量并未為 a 配置設定記憶體空間。定義語句形式為 int a; ,變量 a

在所有子產品中作為一種全局變量隻能被定義一次,否則會出現連接配接錯誤。

在引用全局變量和函數之前,必須要有這個變量或者函數的聲明(或者定義)。通常,在子產品的頭檔案中對本子產品提供給其它子產品引用的函數和全局變量以關鍵字 extern 聲明。

例如,如果子產品 B 欲引用該子產品 A 中定義的全局變量和函數時隻需包含子產品 A 的頭檔案即可。這樣,子產品B中調用子產品 A 中的函數時,在編譯階段,子產品 B 雖然找不到該函數,但是并不會報錯;它會在連接配接階段中從子產品 A 編譯生成的目标代碼中找到此函數。

與 extern 對應的關鍵字是 static ,被它修飾的全局變量和函數隻能在本子產品中使用。是以,一個函數或變量隻可能被本子產品使用時,其不可能被 extern "C" 修飾。被 extern "C" 修飾的變量和函數是按照 C 語言方式編譯和連接配接的。

首先看看 C++ 中,在未加 extern "C" 聲明時,對類似 C 的函數是怎樣編譯的。

作為一種面向對象的語言, C++ 支援函數重載,而過程式語言 C 則不支援。是以,函數被 C++ 編譯後在符号庫中的名字與 C 語言的有所不同。例如,假設某個函數的原型為:void foo( int x, int y );

該函數被 C 編譯器編譯後在符号庫中的名字為 _foo ,而 C++ 編譯器則會産生像 _foo_int_int 之類的名字(不同的編譯器可能生成的名字不同,但是都采用了相同的機制,生成的新名字稱為 mangled name )。

_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 這樣的符号!對于上面例子,如果 B 子產品是 C 程式,而A子產品是 C++ 庫頭檔案的話,會導緻連結錯誤;

同理,如果B子產品是 C++ 程式,而A子產品是C庫的頭檔案也會導緻錯誤。再者,看看加 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 ) ,其結果,将會是 C 語言的編譯連接配接方式:子產品 A 編譯生成 foo 的目标代碼時,沒有對其名字進行特殊處理,采用了 C 語言的方式;連接配接器在為子產品 B 的目标代碼尋找 foo(2,3) 調用時,尋找的是未經修改的符号名 _foo 。

如果在子產品 A 中函數聲明了 foo 為 extern "C" 類型,而子產品 B 中包含的是 extern int foo( int x, int y ) ,則子產品 B 找不到子產品 A 中的函數(因為這樣的聲明沒有使用 extern "C" 指明采用C語言的編譯連結方式);反之亦然。

是以,綜上可知, extern "C" 這個聲明的真實目的,就是實作 C++ 與 C 及其它語言的混合程式設計。用法舉例

C++ 引用 C 函數的具體例子 在 C++ 中引用 C 語言中的函數和變量,在包含 C 語言頭檔案(假設為 cExample.h )時,需進行下列處理:

extern "C"
{
    #include "cExample.h"
}      

因為, C 庫的編譯當然是用 C 的方式生成的,其庫中的函數标号一般也是類似前面所說的 _foo 之類的形式,沒有任何參數資訊,是以當然在 C++ 中,要指定使用 extern "C" ,進行 C 方式的聲明(如果不指定,那麼 C++ 中的預設聲明方式當然是 C++ 方式的,也就是編譯器會産生 _foo_int_int 之類包含參數資訊的、 C++ 形式的函數标号,這樣的函數标号在已經編譯好了的、可以直接引用的 C 庫中當然沒有)。

通過頭檔案對函數進行聲明,再包含頭檔案,就能引用到頭檔案中聲明的函數(因為函數的實作在庫中呢,是以隻聲明,然後連結就能用了)。

而在 C 語言中,對其外部函數隻能指定為 extern 類型,因為 C 語言中不支援 extern "C" 聲明,在 .c 檔案中包含了 extern "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;
}      

,如果 C++ 調用一個 C 語言編寫的 .DLL 時,在包含 .DLL 的頭檔案或聲明接口函數時,應加 extern "C" { } 。

這個時候,其實 extern "C" 是在告訴 C++ ,連結 C 庫的時候,采用 C 的方式進行連結(即尋找類似 _foo 的沒有參數資訊的标号,而不是預設的 _foo_int_int 這樣包含了參數資訊的 C++ 标号了)。

C 引用 C++ 函數的具體例子在C中引用 C++ 語言中的函數和變量時, C++ 的頭檔案需添加 extern "C" ,但是在 C 語言中不能直接引用聲明了 extern "C" 的該頭檔案(因為C語言不支援 extern "C" 關鍵字,是以會報編譯錯誤),應該僅在 C 檔案中用 extern 聲明 C++ 中定義的 extern "C" 函數(就是 C++ 中用 extern "C" 聲明的函數。

在 C 中用 extern 來聲明一下,這樣 C 就能引用 C++ 的函數了,但是 C 中是不用用 extern "C" 的)。

下面是一個具體代碼:

//C++頭檔案 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif

//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;
}      

上面例子, C 實作檔案 cFile.c 不能直接用 #include "cExample.h"= 因為 =C 語言不支援 extern "C" 關鍵字。

這個時候,而在 cppExample.h 中使用 extern "C" 修飾的目的是為了讓 C++ 編譯時候能夠生成 C 形式的符号(類似 _foo 不含參數的形式),然後将其添加到對應的 C++ 實作庫中,以便被 C 程式連結到。

對 __BEGIN_DECLS  和  __END_DECLS  的了解在 C 語言代碼中頭檔案中,經常看到充斥着下面的代碼片段:

1. __BEGIN_DECLS
2. .....
3. .....
4. __END_DECLS      

其實,這些宏一般都是在标準庫頭檔案中定義好了的,例如我目前機器的 sys/cdefs.h 中大緻定義如下:

1. #if defined(__cplusplus)
2.        #define __BEGIN_DECLS extern "C" {
3.        #define __END_DECLS }
4.        #else
5.        #define __BEGIN_DECLS
6.        #define __END_DECLS
7. #endif      

這目的當然是擴充 C 語言在編譯的時候,按照 C++ 編譯器進行統一處理,使得 C++ 代碼能夠調用 C 編譯生成的中間代碼。 

由于 C 語言的頭檔案可能被不同類型的編譯器讀取,是以寫 C 語言的頭檔案必須慎重。總結extern "C" 隻是 C++ 的關鍵字,不是 C 的是以,如果在 C 程式中引入了 extern "C" 會導緻編譯錯誤。 

被 extern "C" 修飾的目标一般是對一個C或者 C++ 函數的聲明從源碼上看 extern "C" 一般對頭檔案中函數聲明進行修飾。