概述
從字面上看:extern "C"有兩部分內容:extern和“C”,下面分別說明:
(1)extern
學過C或C++的人都知道,extern是程式語言中的一種屬性,它表征了變數、函式等類型的作用域(可見性)屬性,是程式語言中的關鍵字。當進行編譯時,該關鍵字告訴編譯器它所聲明的函式和變數等可以在本模組或者檔案以及其他模組或檔案中使用。通常,程式設計師都只是在“*.h”(頭檔案)中使用該關鍵字以限定變數或函式等類型的屬性,然後在其他模組或本模組中使用,如:
file1.h
file2.c /*其他檔案調用該變數*/
(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中的 混合編程的編譯指令。
明白了這層意思,下面的代碼就不難解釋了:
編譯方式
未加extern “C”聲明時的編譯方式
首先看看C++中對類似C的函式是怎樣編譯的。
作為一種面向對象的語言,C++支持函式重載,而過程式語言C則不支持。函式被C++編譯後在符號庫中的名字與C語言的不同。例如,假設某個函式的原型為:
該函式被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
在模組B中引用該函式:
// 模組B實現檔案 moduleB.cpp
實際上,在連線階段,連線器會從模組A生成的目標檔案moduleA.obj中尋找_foo_int_int這樣的符號!
加extern "C"聲明後的編譯和連線方式
加extern "C"聲明後,模組A的頭檔案變為:
// 模組A頭檔案 moduleA.h
在模組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"的慣用法
(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"來說明。