天天看點

C 語言裡面的 extern "C" ,并沒有那麼簡單!

作者:嵌入式小美老師

前言

本文詳細解析extern "C"的底層原理與實際應用。

在你工作過的系統裡,不知能否看到類似下面的代碼。

C 語言裡面的 extern "C" ,并沒有那麼簡單!

這好像沒有什麼問題,你應該還會想:“嗯⋯是啊,我們的代碼都是這樣寫的,從來沒有是以碰到過什麼麻煩啊~”。

你說的沒錯,如果你的頭檔案從來沒有被任何C++程式引用過的話。

這與C++有什麼關系呢? 看看__cplusplus(注意前面是兩個下劃線) 的名字你就應該知道它與C++有很大關系。__cplusplus是一個C++規範規定的預定義宏。

你可以信任的是:所有的現代C++編譯器都預先定義了它;而所有C語言編譯器則不會。另外,按照規範__cplusplus的值應該等于1 9 9 7 1 1 L ,然而不是所有的編譯器都照此實作,比如g++編譯器就将它的值定義為1。

是以,如果上述代碼被C語言程式引用的話,它的内容就等價于下列代碼。

C 語言裡面的 extern "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 語言裡面的 extern "C" ,并沒有那麼簡單!

然後使用C語言編譯器編譯my_handle.c,生成目标檔案my_handle.o。

由于C語言編譯器不對名字進行粉碎,是以在my_handle.o的符号表裡,這三個函數的名字和源代碼檔案中的聲明是一緻的。

C 語言裡面的 extern "C" ,并沒有那麼簡單!

随後,我們想讓一個C++程式調用這些函數,是以,它也包含了頭檔案my_handle.h。

假設這個C++源代碼檔案的名字叫my_handle_client.cpp,其内容如下:

C 語言裡面的 extern "C" ,并沒有那麼簡單!

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

然後,為了讓程式可以工作,你必須将my_handle.o和my_handle_client.o放在一起連結。由于在兩個目标檔案對于同一對象的命名不一樣,連結器将報告相關的“符号未定義”錯誤。

C 語言裡面的 extern "C" ,并沒有那麼簡單!

為了解決這一問題,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 語言裡面的 extern "C" ,并沒有那麼簡單!

然後使用C++編譯器重新編譯my_handle_client.cpp,所生成目标檔案my_handle_client.o中的符号表就變為:

C 語言裡面的 extern "C" ,并沒有那麼簡單!

從中我們可以看出,此時,用extern "C" 修飾了的聲明,其生成的符号和C語言編譯器生成的符号保持了一緻。這樣,當你再次把my_handle.o和my_handle_client.o放在一起連結的時候,就不會再有之前的“符号未定義”錯誤了。

但此時,如果你重新編譯my_handle.c,C語言編譯器将會報告“文法錯誤”,因為extern"C"是C++的文法,C語言編譯器不認識它。此時,可以按照我們之前已經讨論的,使用宏__cplusplus來識别C和C++編譯器。修改後的my_handle.h的代碼如下:

C 語言裡面的 extern "C" ,并沒有那麼簡單!

小心門後的未知世界

在我們清楚了 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 語言裡面的 extern "C" ,并沒有那麼簡單!

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

C 語言裡面的 extern "C" ,并沒有那麼簡單!

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

C 語言裡面的 extern "C" ,并沒有那麼簡單!

如果能夠保證一個C語言頭檔案直接或間接依賴的所有頭檔案也都是C語言的,那麼按照C++語言規範,這種嵌套應該不會有什麼問題。

但具體到某些編譯器的實作,比如MSVC2005,卻可能由于 extern "C" { } 的嵌套過深而報告錯誤。

不要是以而責備微軟,因為就這個問題而言,這種嵌套是毫無意義的。你完全可以通過把#include指令放置在extern "C" { }的外面來避免嵌套。

拿之前的例子來說,如果我們把各個頭檔案的 #include 指令都移到extern "C" { } 之外,然後使用C++編譯器的預處理選項來編譯foo.cpp,就會得到下面的結果:

C 語言裡面的 extern "C" ,并沒有那麼簡單!

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

把 #include 指令放置在extern "C" { }裡面的另外一個重大風險是,你可能會無意中改變一個函數聲明的連結規範。比如:有兩個頭檔案a.h,b.h,其中b.h包含a.h,如下:

C 語言裡面的 extern "C" ,并沒有那麼簡單!

按照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"的寫法如下,這正确嗎?

C 語言裡面的 extern "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宏的編譯器。

C 語言裡面的 extern "C" ,并沒有那麼簡單!

這應該可以工作,但在每個頭檔案中都寫這麼一大串,不僅有礙觀瞻,還會造成一旦政策進行修改,就會到處修改的狀況。違反了DRY(Don't Repeat Yourself)原則,你總要為之付出額外的代價。解決它的一個簡單方案是,定義一個特定的頭檔案——比如clinkage.h,在其中增加這樣的定義:

C 語言裡面的 extern "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;
}           

嵌入式物聯網需要學的東西真的非常多,千萬不要學錯了路線和内容,導緻工資要不上去!

無償分享大家一個資料包,差不多150多G。裡面學習内容、面經、項目都比較新也比較全!某魚上買估計至少要好幾十。

點選這裡找小助理0元領取:加微信領取資料

C 語言裡面的 extern "C" ,并沒有那麼簡單!

轉載自:洋芋居士

文章來源于C 語言裡面的 extern "C" ,并沒有那麼簡單!

原文連結:https://www.cnblogs.com/TenosDoIt/p/3163621.html

繼續閱讀