複製鏈接
請複製以下鏈接發送給好友

extern "C"

鎖定
extern "C"的主要作用就是為了能夠正確實現C++代碼調用其他C語言代碼。加上extern "C"後,會指示編譯器這部分代碼按C語言的進行編譯,而不是C++的。由於C++支持函數重載,因此編譯器編譯函數的過程中會將函數的參數類型也加到編譯後的代碼中,而不僅僅是函數名;而C語言並不支持函數重載,因此編譯C語言代碼的函數時不會帶上函數的參數類型,一般只包括函數名。
外文名
extern "C"
所屬學科
C++
用    途
實現類C語言和C++語言的混合編程

extern "C"基本信息

從字面上看:extern "C"有兩部分內容:extern和“C”,下面分別説明:
(1)extern
學過C或C++的人都知道,extern是編程語言中的一種屬性,它表徵了變量、函數等類型的作用域(可見性)屬性,是編程語言中的關鍵字。當進行編譯時,該關鍵字告訴編譯器它所聲明的函數和變量已在其他文件中定義,可以在本模塊或者文件以及其他模塊或文件中使用。通常,程序員會在 "*.c" 文件中聲明定義變量或定義函數,並在“*.h”(頭文件)中使用該關鍵字限定變量或函數等,然後可在該頭文件或其他模塊中include使用,如:
file1.c
// 聲明並定義(定義時會為其分配空間)
int i = 32;
file1.h
// 使用 file1.c 中定義的 i
extern int i;
file2.c
/*其他文件調用該變量*/
// 此處作用等價於 extern int i;
#include "file1.h"
void welcome(void) {
	if(i > 0) {
		printf("Hello World!\n");
	}
}
(2)“C”
編程語言種類繁多各有優劣,因此在做軟件工程時,往往會出現不同語言嵌套或互相調用的情況,例如在C和C++之間就經常出現互相調用。雖然C++是C的超集,但是,它們之間的編譯器是不同的,這就產生了各自的編譯器在對C和C++進行編譯時要依據哪一個作為大家都認可的規範或者約定的問題。很幸運的是,通過一些基礎特徵我們制定了這樣的一個規約,這就是“C”的出處,這些基礎特徵就是:
(1)這種調用編譯是一種“超”鏈接;
(2)這種調用編譯不能影響現有的編譯規範;
(3)C++是C的拓展,是C的昇華。
根據(1),extern關鍵字可以表達這種“超”鏈接;根據(2)、(3)用“C”來規約在C++中對C的編譯。
因此,extern "C"表明了一種編譯規約,其中extern是關鍵字屬性,“C”表徵了編譯器鏈接規範。對於extern "C"可以理解成在C++/C中的混合編程的編譯指令。
明白了這層意思,下面的代碼就不難解釋了:
/* 如果採用了C++,下述代碼extern "C"有效,extern "C"{}中代碼使用C編譯器 */
#ifdef __cplusplus
		/* 如果沒有采用C++,順序預編譯 */
    extern "C" {
#endif

... /* 採用C編譯器編譯的C語言代碼段 */

#ifdef __cplusplus 		  
    }			/* 結束使用C編譯器 */
#endif

extern "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_int_int
如果在模塊A中函數聲明瞭foo為extern "C"類型,而模塊B中包含的是extern int foo( int x, int y ) ,則模塊B找不到模塊A中的函數;反之亦然。
所以,可以用一句話概括extern“C”這個聲明的真實目的:實現C++與C及其它語言的混合編程。(任何語言中的任何語法特性的誕生都不是隨意而為的,來源於真實世界的需求驅動。我們在思考問題時,不能只停留在這個語言是怎麼做的,還要問一問它為什麼要這麼做,動機是什麼,這樣我們可以更深入地理解許多問題):
明白了C++中extern "C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧。

extern "C"習慣用法

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

#include "cExample.h"

}
而在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.c"
}
int main(int argc, char* argv[]) {
	add(2,3);return 0;
}
如果C++調用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數時,應加extern "C" { }。
(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++實現文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y ) {
	return x + y;
}

// C實現文件 cFile.c
/* 這樣會編譯出錯:#include "cppExample.h" */

extern int add( int x, int y );
int main( int argc, char* argv[]) {
	add( 2, 3 );
	return 0;
}
總結  C和C++對函數的處理方式是不同的.extern "C"是使C++能夠調用C寫的庫文件的一個手段,如果要對編譯器提示使用C的方式來處理函數的話,那麼就要使用extern "C"來説明。
探索C++的秘密之詳解extern "C"
時常在cpp的代碼之中看到這樣的代碼:
#ifdef __cplusplus
extern "C" {
#endif
//一段代碼
;
#ifdef __cplusplus
}
#endif
這樣的代碼到底是什麼意思呢?首先,__cplusplus是cpp中的自定義宏,那麼定義了這個宏的話表示這是一段cpp的代碼,也就是説,上面的代碼的含義是:如果這是一段cpp的代碼,那麼加入extern "C"{和}處理其中的代碼。
要明白為何使用extern "C",還得從cpp中對函數的重載處理開始説起。在c++中,為了支持重載機制,在編譯生成的彙編碼中,要對函數的名字進行一些處理,加入比如函數的返回類型等等.而在C中,只是簡單的函數名字而已,不會加入其他的信息.也就是説:C++和C對產生的函數名字的處理是不一樣的.
比如下面的一段簡單的函數,我們看看加入和不加入extern "C"產生的彙編代碼都有哪些變化:
int f(void) {
	return 1;
}
在加入extern "C"的時候產生的彙編代碼是:
.file "test.cxx"
.text
.align 2
.globl _f
.def _f; .scl 2; .type 32; .endef
_f:
pushl %ebp
movl %esp, %ebp
movl $1, %eax
popl %ebp
ret
但是不加入了extern "C"之後
.file "test.cxx"
.text
.align 2
.globl __Z1fv
.def __Z1fv; .scl 2; .type 32; .endef
__Z1fv:
pushl %ebp
movl %esp, %ebp
movl $1, %eax
popl %ebp
ret
兩段彙編代碼同樣都是使用gcc -S命令產生的,所有的地方都是一樣的,唯獨是產生的函數名,一個是_f,一個是__Z1fv。
明白了加入與不加入extern "C"之後對函數名稱產生的影響,我們繼續我們的討論:為什麼需要使用extern "C"呢?C++之父在設計C++之時,考慮到當時已經存在了大量的C代碼,為了支持原來的C代碼和已經寫好C庫,需要在C++中儘可能的支持C,而extern "C"就是其中的一個策略。
試想這樣的情況:一個庫文件已經用C寫好了而且運行得很良好,這個時候我們需要使用這個庫文件,但是我們需要使用C++來寫這個新的代碼。如果這個代碼使用的是C++的方式鏈接這個C庫文件的話,那麼就會出現鏈接錯誤.我們來看一段代碼:首先,我們使用C的處理方式來寫一個函數,也就是説假設這個函數當時是用C寫成的:
//f1.c
extern "C" {
  void f1() {
      return;
  }
}
編譯命令是:gcc -c f1.c -o f1.o 產生了一個叫f1.o的庫文件。再寫一段代碼調用這個f1函數:
// test.cxx
// 這個extern表示f1函數在別的地方定義,這樣可以通過編譯,
// 但是鏈接的時候還是需要鏈接上原來的庫文件.
extern void f1();
int main() {
	f1();
	return 0;
}
通過gcc -c test.cxx -o test.o 產生一個叫test.o的文件。然後,我們使用gcc test.o f1.o來鏈接兩個文件,可是出錯了,錯誤的提示是:
test.o(.text + 0x1f):test.cxx: undefine reference to 'f1()'
也就是説,在編譯test.cxx的時候編譯器是使用C++的方式來處理f1()函數的,但是實際上鍊接的庫文件卻是用C的方式來處理函數的,所以就會出現鏈接過不去的錯誤:因為鏈接器找不到函數。
因此,為了在C++代碼中調用用C寫成的庫文件,就需要用extern "C"來告訴編譯器:這是一個用C寫成的庫文件,請用C的方式來鏈接它們。
比如,我們有了一個C庫文件,它的頭文件是f.h,產生的lib文件是f.lib,那麼我們如果要在C++中使用這個庫文件,我們需要這樣寫:
extern "C" {
#include "f.h"
}

// 回到上面的問題,如果要改正鏈接錯誤,我們需要這樣子改寫test.cxx:

extern "C" {
	extern void f1();
}

int main() {
	f1();
	return 0;
}
重新編譯並且鏈接就可以過去了.
總結
C和C++對函數的處理方式是不同的.extern "C"是使C++能夠調用C寫作的庫文件的一個手段,如果要對編譯器提示使用C的方式來處理函數的話,那麼就要使用extern "C"來説明。
(3) 另一個常見宏替換使用:
#ifdef __cplusplus
#define __BEGIN_DECLS	extern "C"{
#define __END_DECLS	}
#else
#define __BEGIN_DECLS
#define __END_DECLS
#endif

__BEGIN_DECLS

...	// 你的C代碼, 將會以C語言的方式編譯

__END_DECLS