extern c 解析

extern c 解析

背景

在很多項目中我們常看到這樣的代碼

#ifdef __cplusplus
extern "C" {
#endif

//

#ifdef __cplusplus
}
#endif
複製代碼

但是它到底有什麼用呢?下面我們來介紹這樣的用法。

流程

1. #ifdef _cplusplus / #endif _cplusplus

  • #ifdef/#endif、#ifndef/#endif用於條件編譯,表示如果(ifndef :沒有)定義了宏_cplusplus,就執行它們之間的語句,否則不執行
  • 那麼我們爲什麼需要#ifdef _cplusplus/#endif _cplusplus呢?因爲C語言中不支持extern "C"的聲明,所以如果在.c文件中包含了extern "C"時,編譯出錯,所以我們需要用條件編譯來避免。

舉個例子

// xdx.h

#ifndef XDX
#define XDX

#ifdef __cplusplus
extern “C” {
#endif /* __cplusplus */

/*…

  • do something here
    *…
    */

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* XDX */
複製代碼

如何說明上面宏#ifndef/#endif的作用?

  • 這個頭文件xdx.h可能再項目中被多個源文件包含(#include "xdx.h",而對於一個大型項目來說,這些冗餘可能導致錯誤,因爲一個頭文件包含類定義或inline函數,在一個源文件中xdx.h可能會被#include兩次(如a.h中包含xdx.h,而b.c中#include a.h 和 xdx.h),這就會出錯(在同一個源文件中一個結構體,類被定義了兩次)
  • 從邏輯觀點和減少編譯時間上,都要求取出這些冗餘。但有時我們需要這種冗餘來保證各個模塊的獨立性。

所以下面的代碼就是爲了解決這個問題

#ifndef xdx 
#define    xdx 
/*……………………………*/ 
#endif /* xdx */
複製代碼

如果定義了xdx,#ifndef / #endif之間的內容就被忽略掉。因此,編譯時第一次看到xdx.h,它的內容會被讀取且給定xdx這個值,之後再次看到xdx.h頭文件時,xdx已經被定義,xdx.h的內容就不會再次被讀取了。

2. extern "C"

2 - 1 .extern 關鍵字

項目中必須保證函數,變量,枚舉等在所有源文件中保持一致,除非你指定定義爲局部的。

//file1.c:
    int x=1;
    int f(){do something here}
//file2.c:
    extern int x;
    int f();
    void g(){x=f();}

複製代碼

在file2.c中g()使用的x和f()是定義在file1.c中的。extern關鍵字表明file2.c中x,僅僅是一個變量的聲明,其並不是在定義變量x,並未爲x分配內存空間。變量x在所有模塊中作爲一種全局變量只能被定義一次,否則會出現連接錯誤。但是可以聲明多次,且聲明必須保證類型一致,如:

//file1.c:
    int x=1;
    int b=1;
    extern c;
//file2.c:
    int x;// x equals to default of int type 0
    int f();
    extern double b;
    extern int c;
複製代碼

在這段代碼中存在着這樣的三個錯誤:

  • x被定義了兩次
  • b兩次被聲明爲不同的類型
  • c被聲明瞭兩次,但卻沒有定義

回到extern關鍵字,extern是C/C++語言中表明函數和全局變量作用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本模塊或其它模塊中使用。通常,在模塊的頭文件中對本模塊提供給其它模塊引用的函數和全局變量以關鍵字extern聲明。例如,如果模塊B欲引用該模塊A中定義的全局變量和函數時只需包含模塊A的頭文件即可。這樣,模塊B中調用模塊A中的函數時,在編譯階段,模塊B雖然找不到該函數,但是並不會報錯;它會在連接階段中從模塊A編譯生成的目標代碼中找到此函數。

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

2 - 2. "C" 典型的,一個C++ 程序包含其它語言編寫的部分代碼。類似的,C++編寫的代碼片段可能被使用在其它語言編寫的代碼中。不同語言編寫的代碼互相調用是困難的,甚至是同一種編寫的代碼但不同的編譯器編譯的代碼。例如,不同語言和同種語言的不同實現可能會在註冊變量保持參數和參數在棧上的佈局,這個方面不一樣。

爲了使它們遵守統一規則,可以使用extern指定一個編譯和連接規約。例如,聲明C和C++標準庫函數strcyp(),並指定它應該根據C的編譯和連接規約來鏈接:

extern "C" char* strcpy(char*,const char*);

注意它與下面的聲明的不同之處:

extern char* strcpy(char*,const char*);

下面的這個聲明僅表示在連接的時候調用strcpy()。

  • extern "C"指令非常有用,因爲C和C++的近親關係。

注意:extern "C"指令中的C,表示的一種編譯和連接規約,而不是一種語言。C表示符合C語言的編譯和連接規約的任何語言,如Fortran、assembler等。

  • extern "C"指令僅指定編譯和連接規約,但不影響語義。例如在函數聲明中,指定了extern "C",仍然要遵守C++的類型檢測、參數轉換規則。
  • 爲了聲明一個變量而不是定義一個變量,你必須在聲明時指定extern關鍵字,但是當你又加上了"C",它不會改變語義,但是會改變它的編譯和連接方式。

2 - 2. 小結extern "C"

  • extern "C"的真實目的是實現類C和C++的混合編程

在C++ 源文件中的語句前面加上extern "C",表明它按照類C的編譯和連接規約來編譯和連接,而不是C++ 的編譯的連接規約。這樣在類C的代碼中就可以調用C++的函數or變量等。(注:我在這裏所說的類C,代表的是跟C語言的編譯和連接方式一致的所有語言)

3.C和C++互相調用

首先要明白C和C++互相調用,你得知道它們之間的編譯和連接差異,及如何利用extern "C"來實現相互調用。

3 - 1. C++的編譯和連接

C++ 是一個面嚮對象語言(雖不是純粹的面嚮對象語言),它支持函數的重載,重載這個特性給我們帶來了很大的便利。爲了支持函數重載的這個特性,C++ 編譯器實際上將下面這些重載函數:

void print(int i);
void print(char c);
void print(float f);
void print(char* s);
複製代碼

編譯爲:

_print_int
_print_char
_print_float
_pirnt_string
複製代碼

這樣的函數名,來唯一標識每個函數。注:不同的編譯器實現可能不一樣,但是都是利用這種機制。所以當連接是調用print(3)時,它會去查找_print_int(3)這樣的函數。下面說個題外話,正是因爲這點,重載被認爲不是多態,多態是運行時動態綁定(“一種接口多種實現”),如果硬要認爲重載是多態,它頂多是編譯時“多態”。

C++ 中的變量,編譯也類似,如全局變量可能編譯g_xx,類變量編譯爲c_xx等。連接是也是按照這種機制去查找相應的變量。

3 - 2. C的編譯和連接

C語言中並沒有重載和類這些特性,故並不像C++ 那樣print(int i),會被編譯爲 _print_int,而是直接編譯爲_print等。因此如果直接在C++中調用C的函數會失敗,因爲連接是調用C中的print(3)時,它會去找_print_int(3)。因此extern "C"的作用就體現出來

3 - 3. C++中調用C的代碼

假設一個C的頭文件cHeader.h中包含一個函數print(int i),爲了在C++中能夠調用它,必須要加上extern關鍵字(原因在extern關鍵字那節已經介紹)。它的代碼如下:

#ifndef C_HEADER
#define C_HEADER

extern void print(int i);

#endif C_HEADER
複製代碼

相對應的實現文件爲cHeader.c的代碼爲:

#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}
複製代碼

現在C++ 的代碼文件C++.cpp中引用C中的print(int i)函數:

extern "C"{
#include "cHeader.h"
}
 
int main(int argc,char** argv)
{
    print(3);
    return 0;
}
複製代碼

3 - 3. C中調用C++的代碼

現在換成在C中調用C++ 的代碼,這與在C++ 中調用C的代碼有所不同。如下在cppHeader.h頭文件中定義了下面的代碼:

#ifndef CPP_HEADER
#define CPP_HEADER

extern “C” void print(int i);

#endif CPP_HEADER
複製代碼

相應的實現文件cppHeader.cpp文件中代碼如下:

#include "cppHeader.h"
 
#include <iostream>
using namespace std;
void print(int i)
{
    cout<<"cppHeader "<<i<<endl;
}
複製代碼

在C的代碼文件c.c中調用print函數:

extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return 0;
}
複製代碼

注意在C的代碼文件中直接#include "cppHeader.h"頭文件,編譯出錯。而且如果不加extern int print(int i)編譯也會出錯。

4. C和C++混合調用特別之處函數指針

當我們C和C++ 混合編程時,有時候會用一種語言定義函數指針,而在應用中將函數指針指向另一中語言定義的函數。如果C和C++ 共享同一中編譯和連接、函數調用機制,這樣做是可以的。然而,這樣的通用機制,通常不然假定它存在,因此我們必須小心地確保函數以期望的方式調用。

而且當指定一個函數指針的編譯和連接方式時,函數的所有類型,包括函數名、函數引入的變量也按照指定的方式編譯和連接。如下例:

typedef int (*FT) (const void* ,const void*);//style of C++

extern “C”{
typedef int (CFT) (const void,const void*);//style of C
void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}

void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C

//style of C
extern “C” void ysort(void* p,size_t n,size_t sz,FT cmp);

int compare(const void*,const void*);//style of C++
extern “C” ccomp(const void*,const void*);//style of C

void f(char* v,int sz)
{
//error,as qsort is style of C
//but compare is style of C++
qsort(v,sz,1,&compare);
qsort(v,sz,1,&ccomp);//ok

isort(v,sz,1,&amp;compare);//ok
//error,as isort is style of C++
//but ccomp is style of C
isort(v,sz,1,&amp;ccopm);

}
複製代碼

注意:typedef int (* FT) (const void* ,const void*),表示定義了一個函數指針的別名FT,這種函數指針指向的函數有這樣的特徵:返回值爲int型、有兩個參數,參數類型可以爲任意類型的指針(因爲爲void*)。

最典型的函數指針的別名的例子是,信號處理函數signal,它的定義如下:

typedef void (*HANDLER)(int);
HANDLER signal(int ,HANDLER);
複製代碼

上面的代碼定義了信函處理函數signal,它的返回值類型爲HANDLER,有兩個參數分別爲int、HANDLER。 這樣避免了要這樣定義signal函數:

void (*signal (int ,void(*)(int) ))(int)
複製代碼

本文參考C++項目中的extern "C" {},該文十分清晰易懂,爲了讓更多人看到而轉載。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章