1. 程式人生 > >extern "C" 總結

extern "C" 總結

extern “C”總結

簡介

extern “C”的主要作用就是為了能夠正確實現C++程式碼呼叫其他C語言程式碼。加上extern “C”後,會指示編譯器這部分程式碼按C語言的規約進行編譯,而不是C++的。由於C++支援函式過載,因此編譯器編譯函式的過程中會將函式的引數型別也加到編譯後的程式碼中,而不僅僅是函式名;而C語言並不支援函式過載,因此編譯C語言程式碼的函式時不會帶上函式的引數型別,一般只包括函式名。

注意:extern “C”指令中的C,表示的是一種編譯和連線規約,而不是一種語言。C表示符合C語言的編譯和連線規約的任何語言,如Fortran、assembler等。還有要說明的是,extern “C”指令僅指定編譯和連線規約,但不影響語義。例如在函式宣告中,指定了extern “C”,仍然要遵守C++的型別檢測、引數轉換規則。再看下面的一個例子,為了宣告一個變數而不是定義一個變數,你必須在宣告時指定extern關鍵字,但是當你又加上了”C”,它不會改變語義,但是會改變它的編譯和連線方式。即extern宣告變數而非定義變數,extern “C”改變編碼和連線方式.如果你有很多程式碼要加上extern “C”,你可以將它們放到extern “C”{ }中。

參考https://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html

概述

從字面上看:extern “C”有兩部分內容:extern和“C”,下面分別說明:
(1)extern
學過C或C++的人都知道,extern是程式語言中的一種屬性,它表徵了變數、函式等型別的作用域(可見性)屬性,是程式語言中的關鍵字。當進行編譯時,該關鍵字告訴編譯器它所宣告的函式和變數等可以在本模組或者檔案以及其他模組或檔案中使用。通常,程式設計師都只是在“*.h”(標頭檔案)中使用該關鍵字以限定變數或函式等型別的屬性,然後在其他模組或本模組中使用,如:
file1.h

extern int i;

file2.c /*其他檔案呼叫該變數*/

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中的混合程式設計的編譯指令。
明白了這層意思,下面的程式碼就不難解釋了:

#ifdef __cplusplus /* 如果採用了C++,如下程式碼使用C編譯器 */
    extern "C" { /* 如果沒有采用C++,順序預編譯 */
#endif
/* 採用C編譯器編譯的C語言程式碼段 */
#ifdef __cplusplus /* 結束使用C編譯器 */
    }
#endif

編譯方式

未加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”的慣用法
(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”來說明。