關於extern "C"(詳細剖析)

【目錄】html

引言ios

extern 「C」的前世此生c++

當心門後的未知世界安全

Q&A併發

c++調用c的方法函數

c調用c++的方法工具

 


 

在你工做過的系統裏,不知可否看到相似下面的代碼。測試

這好像沒有什麼問題,你應該還會想:「嗯⋯是啊,咱們的代碼都是這樣寫的,歷來沒有所以碰到過什麼麻煩啊~」。this

你說的沒錯,若是你的頭文件歷來沒有被任何C++程序引用過的話。google

這與C++有什麼關係呢? 看看__cplusplus(注意前面是兩個下劃線) 的名字你就應該知道它與C++有很大關係。__cplusplus是一個C++規範規定的預約義宏。你能夠信任的是:全部的現代C++編譯器都預先定義了它;而全部C語言編譯器則不會。另外,按照規範__cplusplus的值應該等於1 9 9 7 1 1 L ,然而不是全部的編譯器都照此實現,好比g++編譯器就將它的值定義爲1。

因此,若是上述代碼被C語言程序引用的話,它的內容就等價於下列代碼。

在這種狀況下,既然extern "C" { }通過預處理以後根本就不存在,那麼它和#include指令之間的關係問題天然也就是無中生有。

extern "C"的前世此生

在C++編譯器裏,有一位暗黑破壞神,專門從事一份稱做「名字粉碎」(name mangling)的工做。當把一個C++的源文件投入編譯的時候,它就開始工做,把每個它在源文件裏看到的外部可見的名字粉碎的面目全非,而後存儲到二進制目標文件的符號表裏。

之因此在C++的世界裏存在這樣一個怪物,是由於C++容許對一個名字給予不一樣的定義,只要在語義上沒有二義性就好。好比,你可讓兩個函數是同名的,只要它們的參數列表不一樣便可,這就是函數重載(function overloading);甚至,你可讓兩個函數的原型聲明是徹底相同的,只要它們所處的名字空間(namespace)不同便可。事實上,當處於不一樣的名字空間時,全部的名字都是能夠重複的,不管是函數名,變量名,仍是類型名。

另外,C++程序的構造方式仍然繼承了C語言的傳統:編譯器把每個經過命令行指定的源代碼文件看作一個獨立的編譯單元,生成目標文件;而後,連接器經過查找這些目標文件的符號表將它們連接在一塊兒生成可執行程序。

編譯和連接是兩個階段的事情;事實上,編譯器和連接器是兩個徹底獨立的工具。編譯器能夠經過語義分析知道那些同名的符號之間的差異;而連接器卻只能經過目標文件符號表中保存的名字來識別對象。

因此,編譯器進行名字粉碎的目的是爲了讓連接器在工做的時候不陷入困惑,將全部名字從新編碼,生成全局惟一,不重複的新名字,讓連接器可以準確識別每一個名字所對應的對象。

但 C語言倒是一門單一名字空間的語言,也不容許函數重載,也就是說,在一個編譯和連接的範圍以內,C語言不容許存在同名對象。好比,在一個編譯單元內部,不容許存在同名的函數,不管這個函數是否用static修飾;在一個可執行程序對應的全部目標文件裏,不容許存在同名對象,不管它表明一個全局變量,仍是一個函數。因此,C語言編譯器不須要對任何名字進行復雜的處理(或者僅僅對名字進行簡單一致的修飾(decoration),好比在名字前面統一的加上單下劃線_)。

C++的締造者Bjarne Stroustrup在最初就把——可以兼容C,可以複用大量已經存在的C庫——列爲C++語言的重要目標。但兩種語言的編譯器對待名字的處理方式是不一致的,這就給連接過程帶來了麻煩。

例如,現有一個名爲my_handle.h的頭文件,內容以下:

而後使用C語言編譯器編譯my_handle.c,生成目標文件my_handle.o。因爲C語言編譯器不對名字進行粉碎,因此在my_handle.o的符號表裏,這三個函數的名字和源代碼文件中的聲明是一致的。

隨後,咱們想讓一個C++程序調用這些函數,因此,它也包含了頭文件my_handle.h。假設這個C++源代碼文件的名字叫my_handle_client.cpp,其內容以下:

其中,粗體的部分就是那三個函數的名字被粉碎後的樣子。

而後,爲了讓程序能夠工做,你必須將my_handle.o和my_handle_client.o放在一塊兒連接。因爲在兩個目標文件對於同一對象的命名不同,連接器將報告相關的「符號未定義」錯誤。

爲了解決這一問題,C++引入了連接規範(linkage specification)的概念,表示法爲extern"language string",C++編譯器廣泛支持的"language string"有"C"和"C++",分別對應C語言和C++語言。

連接規範的做用是告訴C++編譯:對於全部使用了連接規範進行修飾的聲明或定義,應該按照指定語言的方式來處理,好比名字,調用習慣(calling convention)等等。

連接規範的用法有兩種:

1.單個聲明的連接規範,好比:extern "C" void foo();

2. 一組聲明的連接規範,好比:

 

extern "C"
{
  void foo();
  int bar();
}

對咱們以前的例子而言,若是咱們把頭文件my_handle.h的內容改爲:

而後使用C++編譯器從新編譯my_handle_client.cpp,所生成目標文件my_handle_client.o中的符號表就變爲:

從中咱們能夠看出,此時,用extern "C" 修飾了的聲明,其生成的符號和C語言編譯器生成的符號保持了一致。這樣,當你再次把my_handle.o和my_handle_client.o放在一塊兒連接的時候,就不會再有以前的「符號未定義」錯誤了。

但此時,若是你從新編譯my_handle.c,C語言編譯器將會報告「語法錯誤」,由於extern"C"是C++的語法,C語言編譯器不認識它。此時,能夠按照咱們以前已經討論的,使用宏__cplusplus來識別C和C++編譯器。修改後的my_handle.h的代碼以下:

當心門後的未知世界

在咱們清楚了 extern "C" 的來歷和用途以後,回到咱們原本的話題上,爲何不能把#include 指令放置在 extern "C" { ... } 裏面?

咱們先來看一個例子,現有a.h,b.h,c.h以及foo.cpp,其中foo.cpp包含c.h,c.h包含b.h,b.h包含a.h,以下:

現使用C++編譯器的預處理選項來編譯foo.cpp,獲得下面的結果:

正如你看到的,當你把#include指令放置在extern "C" { }裏的時候,則會形成extern "C" { } 的嵌套。這種嵌套是被C++規範容許的。當嵌套發生時,以最內層的嵌套爲準。好比在下面代碼中,函數foo會使用C++的連接規範,而函數bar則會使用C的連接規範。

若是可以保證一個C語言頭文件直接或間接依賴的全部頭文件也都是C語言的,那麼按照C++語言規範,這種嵌套應該不會有什麼問題。但具體到某些編譯器的實現,好比MSVC2005,卻可能因爲 extern "C" { } 的嵌套過深而報告錯誤。不要所以而責備微軟,由於就這個問題而言,這種嵌套是毫無心義的。你徹底能夠經過把#include指令放置在extern "C" { }的外面來避免嵌套。拿以前的例子來講,若是咱們把各個頭文件的 #include 指令都移到extern "C" { } 以外,而後使用C++編譯器的預處理選項來編譯foo.cpp,就會獲得下面的結果:

這樣的結果確定不會引發編譯問題的結果——即使是使用MSVC。

把 #include 指令放置在extern "C" { }裏面的另一個重大風險是,你可能會無心中改變一個函數聲明的連接規範。好比:有兩個頭文件a.h,b.h,其中b.h包含a.h,以下:

按照a.h做者的本意,函數foo是一個C++自由函數,其連接規範爲"C++"。但在b.h中,因爲#include "a.h"被放到了extern "C" { }的內部,函數foo的連接規範被不正確地更改了。

因爲每一條 #include 指令後面都隱藏這一個未知的世界,除非你刻意去探索,不然你永遠都不知道,當你把一條條#include指令放置於extern "C" { }裏面的時候,到底會產生怎樣的結果,會帶來何種的風險。或許你會說,「我能夠去查看這些被包含的頭文件,我能夠保證它們不會帶來麻煩」。但,何須呢?畢竟,咱們徹底能夠沒必要爲沒必要要的事情買單,不是嗎?

Q&A

Q: 難道任何# i n c l u d e指令都不能放在e x t e r n "C"裏面嗎?

A: 正像這個世界的大多數規則同樣,總會存在特殊狀況。

有時候,你可能利用頭文件機制「巧妙」的解決一些問題。好比,#pragma pack的問題。這些頭文件和常規的頭文件做用是不同的,它們裏面不會放置C的函數聲明或者變量定義,連接規範不會對它們的內容產生影響。這種狀況下,你能夠沒必要遵照這些規則。

更加通常的原則是,在你明白了這全部的原理以後,只要你明白本身在幹什麼,那就去作吧。


Q: 你只說了不該該放入e x t e r n "C"的,但什麼能夠放入呢?

A: 連接規範僅僅用於修飾函數和變量,以及函數類型。因此,嚴格的講,你只應該把這三種對象放置於extern "C"的內部。

但,你把C語言的其它元素,好比非函數類型定義(結構體,枚舉等)放入extern "C"內部,也不會帶來任何影響。更不用說宏定義預處理指令了。

因此,若是你更加看重良好組織和管理的習慣,你應該只在必須使用extern "C"聲明的地方使用它。即便你比較懶惰,絕大多數狀況下,把一個頭件自身的全部定義和聲明都放置在extern"C"裏面也不會有太大的問題。

Q: 若是一個帶有函數/變量聲明的C頭文件裏沒有e x t e r n "C"聲明怎麼辦?

A: 若是你能夠判斷,這個頭文件永遠不可能讓C++代碼來使用,那麼就不要管它。

但現實是,大多數狀況下,你沒法準確的推測將來。你在如今就加上這個extern "C",這花不了你多少成本,但若是你如今沒有加,等到未來這個頭文件無心中被別人的C++程序包含的時候,別人極可能須要更高的成原本定位錯誤和修復問題。

Q: 若是個人C+ +程序想包含一個C頭文件a . h,它的內容包含了C的函數/變量聲明,但它們卻沒有使用e x t e r n "C"連接規範,該怎麼辦?

A: 在a.h裏面加上它。

某些人可能會建議你,若是a.h沒有extern "C",而b.cpp包含了a.h,能夠在b.cpp里加上 :

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

這是一個邪惡的方案,緣由在以前咱們已經闡述。但值得探討的是,這種方案這背後卻可能隱含着一個假設,即咱們不能修改a.h。不能修改的緣由可能來自兩個方面:

1. 頭文件代碼屬於其它團隊或者第三方公司,你沒有修改代碼的權限;
2. 雖然你擁有修改代碼的權限,但因爲這個頭文件屬於遺留系統,冒然修改可能會帶來不可預知的問題。

對 於第一種狀況,不要試圖本身進行workaround,由於這會給你帶來沒必要要的麻煩。正確的解決方案是,把它看成一個bug,發送缺陷報告給相應的團隊 或第三方公司。若是是本身公司的團隊或你已經付費的第三方公司,他們有義務爲你進行這樣的修改。若是他們不明白這件事情的重要性,告訴他們。若是這些頭文 件屬於一個免費開源軟件,本身進行正確的修改,併發布patch給其開發團隊。

在 第二種狀況下,你須要拋棄掉這種沒必要要的安全意識。由於,首先,對於大多數頭文件而言,這種修改都不是一種複雜的,高風險的修改,一切都在可控的範圍之 內;其次,若是某個頭文件混亂而複雜,雖然對於遺留系統的哲學應該是:「在它尚未帶來麻煩以前不要動它」,但如今麻煩已經來了,逃避不如正視,因此上策 是,將其視做一個能夠整理到乾淨合理狀態的良好機會。

Q: 咱們代碼中關於e x t e r n "C"的寫法以下,這正確嗎?

 

A: 不肯定。

按照C++的規範定義,__cplusplus 的值應該被定義爲199711L,這是一個非零的值;儘管某些編譯器並無按照規範來實現,但仍然可以保證__cplusplus的值爲非零——至少我到目前爲止尚未看到哪款編譯器將其實現爲0。這種狀況下,#if __cplusplus ... #endif徹底是冗餘的。

但,C++編譯器的廠商是如此之多,沒有人能夠保證某款編譯器,或某款編譯器的早期版本沒有將__cplusplus的值定義爲0。但即使如此,只要可以保證宏__cplusplus只在C++編譯器中被預先定義 ,那麼,僅僅使用#ifdef __cplusplus ⋯ #endif就足以確保意圖的正確性;額外的使用#if __cplusplus ... #endif反而是錯誤的。

只有在這種狀況下:即某個廠商的C語言和C++語言編譯器都預先定義了__cplusplus ,但經過其值爲0和非零來進行區分,使用#if __cplusplus ... #endif纔是正確且必要的。

既然現實世界是如此複雜,你就須要明確本身的目標,而後根據目標定義相應的策略。好比:若是你的目標是讓你的代碼可以使用幾款主流的、正確遵照了規範的編譯器進行編譯,那麼你只須要簡單的使用#ifdef __cplusplus ... #endif就足夠了。

但若是你的產品是一個雄心勃勃的,試圖兼容各類編譯器的(包括未知的)跨平臺產品, 咱們可能不得不使用下述方法來應對各類狀況 ,其中__ALIEN_C_LINKAGE__是爲了標識那些在C和C++編譯中都定義了__cplusplus宏的編譯器

這應該能夠工做,但在每一個頭文件中都寫這麼一大串,不只有礙觀瞻,還會形成一旦策略進行修改,就會處處修改的情況。違反了DRY(Don't Repeat Yourself)原則,你總要爲之付出額外的代價。 解決它的一個簡單方案是,定義一個特定的頭文件——好比clinkage.h,在其中增長這樣的定義:

 【說明】以上內容轉載自 http://code.google.com/p/effective-c/downloads/list 中的的effective C 文檔


如下舉例中c的函數聲明和定義分別在cfun.h 和 cfun.c 中,函數打印字符串 「this is c fun call」,c++函數聲明和定義分別在cppfun.h 和 cppfun.cpp中,函數打印字符串 "this is cpp fun call", 編譯環境vc2010

c++ 調用 c 的方法(關鍵是要讓c的函數按照c的方式編譯,而不是c++的方式)

(1) cfun.h以下:

 

#ifndef _C_FUN_H_
#define _C_FUN_H_

    void cfun();

#endif

 

   cppfun.cpp 以下:

//#include "cfun.h"  不須要包含cfun.h
#include "cppfun.h"
#include <iostream>
using namespace std;
extern "C"     void cfun(); //聲明爲 extern void cfun(); 錯誤

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

(2)cfun.h同上

       cppfun.cpp 以下:

extern "C"
{
    #include "cfun.h"//注意include語句必定要單獨佔一行;
}
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

(3)cfun.h以下:

#ifndef _C_FUN_H_
#define _C_FUN_H_

#ifdef __cplusplus
extern "C"
{
#endif

    void cfun();

#ifdef __cplusplus
}
#endif

#endif

cppfun.cpp以下:

#include "cfun.h"
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
    cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
    return 0;
}

 c調用c++(關鍵是C++ 提供一個符合 C 調用慣例的函數)

在vs2010上測試時,沒有聲明什麼extern等,只在在cfun.c中包含cppfun.h,而後調用cppfun()也能夠編譯運行,在gcc下就編譯出錯,按照c++/c的標準這種作法應該是錯誤的。如下方法兩種編譯器均可以運行

cppfun.h以下:

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_

extern "C" void cppfun();


#endif

cfun.c以下:

//#include "cppfun.h" //不要包含頭文件,不然編譯出錯
#include "cfun.h"
#include <stdio.h>

void cfun()
{
    printf("this is c fun call\n");
}

extern void cppfun();

int main()
{
#ifdef __cplusplus
    cfun();
#endif
    cppfun();
    return 0;
}

 【版權聲明】轉載請註明出處 http://www.cnblogs.com/TenosDoIt/p/3163621.html

相關文章
相關標籤/搜索