在用c++的項目源碼中,經常會不可避免的會看到下面的代碼:
1
2
3
4
5
6
7
8
9
<code>#ifdef __cplusplus</code>
<code>extern</code> <code>"c"</code> <code>{</code>
<code>#endif</code>
<code>/*...*/</code>
<code>}</code>
它到底有什麼用呢,你知道嗎?而且這樣的問題經常會出現在面試or筆試中。下面我就從以下幾個方面來介紹它:
1、#ifdef _cplusplus/#endif _cplusplus及發散
2、extern "c"
2.1、extern關鍵字
2.2、"c"
2.3、小結extern "c"
3、c和c++互相調用
3.1、c++的編譯和連接配接
3.2、c的編譯和連接配接
3.3、c++中調用c的代碼
3.4、c中調用c++的代碼
4、c和c++混合調用特别之處函數指針
在介紹extern "c"之前,我們來看下#ifdef _cplusplus/#endif
_cplusplus的作用。很明顯#ifdef/#endif、#ifndef/#endif用于條件編譯,#ifdef
_cplusplus/#endif
_cplusplus——表示如果定義了宏_cplusplus,就執行#ifdef/#endif之間的語句,否則就不執行。
在這裡為什麼需要#ifdef _cplusplus/#endif _cplusplus呢?因為c語言中不支援extern
"c"聲明,如果你明白extern "c"的作用就知道在c中也沒有必要這樣做,這就是條件編譯的作用!在.c檔案中包含了extern
"c"時會出現編譯時錯誤。
既然說到了條件編譯,我就介紹它的一個重要應用——避免重複包含頭檔案。還記得騰訊筆試就考過這個題目,給出類似下面的代碼(下面是我最近在研究的一個開源web伺服器——mongoose的頭檔案mongoose.h中的一段代碼):
10
11
12
13
14
15
16
17
<code>#ifndef mongoose_header_included</code>
<code>#define mongoose_header_included</code>
<code>#endif /* __cplusplus */</code>
<code>/*.................................</code>
<code> </code><code>* do something here</code>
<code> </code><code>*.................................</code>
<code> </code><code>*/</code>
<code>#endif /* mongoose_header_included */</code>
然後叫你說明上面宏#ifndef/#endif的作用?為了解釋一個問題,我們先來看兩個事實:
這個頭檔案mongoose.h可能在項目中被多個源檔案包含(#include
"mongoose.h"),而對于一個大型項目來說,這些備援可能導緻錯誤,因為一個頭檔案包含類定義或inline函數,在一個源檔案中
mongoose.h可能會被#include兩次(如,a.h頭檔案包含了mongoose.h,而在b.c檔案中#include
a.h和mongoose.h)——這就會出錯(在同一個源檔案中一個結構體、類等被定義了兩次)。
從邏輯觀點和減少編譯時間上,都要求去除這些備援。然而讓程式員去分析和去掉這些備援,不僅枯燥且不太實際,最重要的是有時候又需要這種備援來保證各個子產品的獨立。
為了解決這個問題,上面代碼中的
#ifndef mongoose_header_included
#define mongoose_header_included
/*……………………………*/
#endif /* mongoose_header_included */
就起作用了。如果定義了mongoose_header_included,#ifndef/#endif之間的内容就被忽略掉。是以,編譯時第一
次看到mongoose.h頭檔案,它的内容會被讀取且給定mongoose_header_included一個值。之後再次看到mongoose.h
頭檔案時,mongoose_header_included就已經定義了,mongoose.h的内容就不會再次被讀取了。
首先從字面上分析extern "c",它由兩部分組成——extern關鍵字、"c"。下面我就從這兩個方面來解讀extern "c"的含義。
在一個項目中必須保證函數、變量、枚舉等在所有的源檔案中保持一緻,除非你指定定義為局部的。首先來一個例子:
<code>//file1.c:</code>
<code> </code><code>int</code> <code>x=1;</code>
<code> </code><code>int</code> <code>f(){</code><code>do</code> <code>something here}</code>
<code>//file2.c:</code>
<code> </code><code>extern</code> <code>int</code> <code>x;</code>
<code> </code><code>int</code> <code>f();</code>
<code> </code><code>void</code> <code>g(){x=f();}</code>
在file2.c中g()使用的x和f()是定義在file1.c中的。extern關鍵字表明file2.c中x,僅僅是一個變量的聲明,其并不
是在定義變量x,并未為x配置設定記憶體空間。變量x在所有子產品中作為一種全局變量隻能被定義一次,否則會出現連接配接錯誤。但是可以聲明多次,且聲明必須保證類型
一緻,如:
<code> </code><code>int</code> <code>b=1;</code>
<code> </code><code>extern</code> <code>c;</code>
<code> </code><code>int</code> <code>x;</code><code>// x equals to default of int type 0</code>
<code> </code><code>extern</code> <code>double</code> <code>b;</code>
<code> </code><code>extern</code> <code>int</code> <code>c;</code>
在這段代碼中存在着這樣的三個錯誤:
x被定義了兩次
b兩次被聲明為不同的類型
c被聲明了兩次,但卻沒有定義
回到extern關鍵字,extern是c/c++語言中表明函數和全局變量作
用範圍(可見性)的關鍵字,該關鍵字告訴編譯器,其聲明的函數和變量可以在本子產品或其它子產品中使用。通常,在子產品的頭檔案中對本子產品提供給其它子產品引用的
函數和全局變量以關鍵字extern聲明。例如,如果子產品b欲引用該子產品a中定義的全局變量和函數時隻需包含子產品a的頭檔案即可。這樣,子產品b中調用子產品
a中的函數時,在編譯階段,子產品b雖然找不到該函數,但是并不會報錯;它會在連接配接階段中從子產品a編譯生成的目标代碼中找到此函數。
與extern對應的關鍵字是 static,被它修飾的全局變量和函數隻能在本子產品中使用。是以,一個函數或變量隻可能被本子產品使用時,其不可能被extern “c”修飾。
典型的,一個c++程式包含其它語言編寫的部分代碼。類似的,c++編寫的代碼片段可能被使用在其它語言編寫的代碼中。不同語言編寫的代碼互相調用
是困難的,甚至是同一種編寫的代碼但不同的編譯器編譯的代碼。例如,不同語言和同種語言的不同實作可能會在注冊變量保持參數和參數在棧上的布局,這個方面
不一樣。
為了使它們遵守統一規則,可以使用extern指定一個編譯和連接配接規約。例如,聲明c和c++标準庫函數strcyp(),并指定它應該根據c的編譯和連接配接規約來連結:
<code>extern</code> <code>"c"</code> <code>char</code><code>*</code><code>strcpy</code><code>(</code><code>char</code><code>*,</code><code>const</code> <code>char</code><code>*);</code>
注意它與下面的聲明的不同之處:
<code>extern</code> <code>char</code><code>*</code><code>strcpy</code><code>(</code><code>char</code><code>*,</code><code>const</code> <code>char</code><code>*);</code>
下面的這個聲明僅表示在連接配接的時候調用strcpy()。
extern "c"指令非常有用,因為c和c++的近親關系。注意:extern "c"指令中的c,表示的一種編譯和連接配接規約,而不是一種語言。c表示符合c語言的編譯和連接配接規約的任何語言,如fortran、assembler等。
還有要說明的是,extern "c"指令僅指定編譯和連接配接規約,但不影響語義。例如在函數聲明中,指定了extern "c",仍然要遵守c++的類型檢測、參數轉換規則。
再看下面的一個例子,為了聲明一個變量而不是定義一個變量,你必須在聲明時指定extern關鍵字,但是當你又加上了"c",它不會改變語義,但是會改變它的編譯和連接配接方式。
如果你有很多語言要加上extern "c",你可以将它們放到extern "c"{ }中。
通過上面兩節的分析,我們知道extern "c"的真實目的是實作類c和c++的混合程式設計。在c++源檔案中的語句前面加上extern "c",表明它按照類c的編譯和連接配接規約來編譯和連接配接,而不是c++的編譯的連接配接規約。這樣在類c的代碼中就可以調用c++的函數or變量等。(注:我在這裡所說的類c,代表的是跟c語言的編譯和連接配接方式一緻的所有語言)
我們既然知道extern "c"是實作的類c和c++的混合程式設計。下面我們就分别介紹如何在c++中調用c的代碼、c中調用c++的代碼。首先要明白c和c++互相調用,你得知道它們之間的編譯和連接配接差異,及如何利用extern "c"來實作互相調用。
c++是一個面向對象語言(雖不是純粹的面向對象語言),它支援函數的重載,重載這個特性給我們帶來了很大的便利。為了支援函數重載的這個特性,c++編譯器實際上将下面這些重載函數:
<code>void</code> <code>print(</code><code>int</code> <code>i);</code>
<code>void</code> <code>print(</code><code>char</code> <code>c);</code>
<code>void</code> <code>print(</code><code>float</code> <code>f);</code>
<code>void</code> <code>print(</code><code>char</code><code>* s);</code>
編譯為:
<code>_print_int</code>
<code>_print_char</code>
<code>_print_float</code>
<code>_pirnt_string</code>
這樣的函數名,來唯一辨別每個函數。注:不同的編譯器實作可能不一樣,但是都是利用這種機制。是以當連接配接是調用print(3)時,它會去查找
_print_int(3)這樣的函數。下面說個題外話,正是因為這點,重載被認為不是多态,多态是運作時動态綁定(“一種接口多種實作”),如果硬要認
為重載是多态,它頂多是編譯時“多态”。
c++中的變量,編譯也類似,如全局變量可能編譯g_xx,類變量編譯為c_xx等。連接配接是也是按照這種機制去查找相應的變量。
c語言中并沒有重載和類這些特性,故并不像c++那樣print(int
i),會被編譯為_print_int,而是直接編譯為_print等。是以如果直接在c++中調用c的函數會失敗,因為連接配接是調用c中的
print(3)時,它會去找_print_int(3)。是以extern "c"的作用就展現出來了。
假設一個c的頭檔案cheader.h中包含一個函數print(int i),為了在c++中能夠調用它,必須要加上extern關鍵字(原因在extern關鍵字那節已經介紹)。它的代碼如下:
<code>#ifndef c_header</code>
<code>#define c_header</code>
<code>extern</code> <code>void</code> <code>print(</code><code>int</code> <code>i);</code>
<code>#endif c_header</code>
相對應的實作檔案為cheader.c的代碼為:
<code>#include <stdio.h></code>
<code>#include "cheader.h"</code>
<code>void</code> <code>print(</code><code>int</code> <code>i)</code>
<code>{</code>
<code> </code><code>printf</code><code>(</code><code>"cheader %d\n"</code><code>,i);</code>
現在c++的代碼檔案c++.cpp中引用c中的print(int i)函數:
<code>extern</code> <code>"c"</code><code>{</code>
<code>int</code> <code>main(</code><code>int</code> <code>argc,</code><code>char</code><code>** argv)</code>
<code> </code><code>print(3);</code>
<code> </code><code>return</code> <code>0;</code>
執行程式輸出:
現在換成在c中調用c++的代碼,這與在c++中調用c的代碼有所不同。如下在cppheader.h頭檔案中定義了下面的代碼:
<code>#ifndef cpp_header</code>
<code>#define cpp_header</code>
<code>extern</code> <code>"c"</code> <code>void</code> <code>print(</code><code>int</code> <code>i);</code>
<code>#endif cpp_header</code>
相應的實作檔案cppheader.cpp檔案中代碼如下:
<code>#include "cppheader.h"</code>
<code>#include <iostream></code>
<code>using</code> <code>namespace</code> <code>std;</code>
<code> </code><code>cout<<</code><code>"cppheader "</code><code><<i<<endl;</code>
在c的代碼檔案c.c中調用print函數:
注意在c的代碼檔案中直接#include "cppheader.h"頭檔案,編譯出錯。而且如果不加extern int print(int i)編譯也會出錯。
當我們c和c++混合程式設計時,有時候會用一種語言定義函數指針,而在應用中将函數指針指向另一中語言定義的函數。如果c和c++共享同一中編譯和連
接、函數調用機制,這樣做是可以的。然而,這樣的通用機制,通常不然假定它存在,是以我們必須小心地確定函數以期望的方式調用。
而且當指定一個函數指針的編譯和連接配接方式時,函數的所有類型,包括函數名、函數引入的變量也按照指定的方式編譯和連接配接。如下例:
18
19
20
21
22
23
24
25
26
27
28
<code>typedef</code> <code>int</code> <code>(*ft) (</code><code>const</code> <code>void</code><code>* ,</code><code>const</code> <code>void</code><code>*);</code><code>//style of c++</code>
<code> </code><code>typedef</code> <code>int</code> <code>(*cft) (</code><code>const</code> <code>void</code><code>*,</code><code>const</code> <code>void</code><code>*);</code><code>//style of c</code>
<code> </code><code>void</code> <code>qsort</code><code>(</code><code>void</code><code>* p,</code><code>size_t</code> <code>n,</code><code>size_t</code> <code>sz,cft cmp);</code><code>//style of c</code>
<code>void</code> <code>isort(</code><code>void</code><code>* p,</code><code>size_t</code> <code>n,</code><code>size_t</code> <code>sz,ft cmp);</code><code>//style of c++</code>
<code>void</code> <code>xsort(</code><code>void</code><code>* p,</code><code>size_t</code> <code>n,</code><code>size_t</code> <code>sz,cft cmp);</code><code>//style of c</code>
<code>//style of c</code>
<code>extern</code> <code>"c"</code> <code>void</code> <code>ysort(</code><code>void</code><code>* p,</code><code>size_t</code> <code>n,</code><code>size_t</code> <code>sz,ft cmp);</code>
<code>int</code> <code>compare(</code><code>const</code> <code>void</code><code>*,</code><code>const</code> <code>void</code><code>*);</code><code>//style of c++</code>
<code>extern</code> <code>"c"</code> <code>ccomp(</code><code>const</code> <code>void</code><code>*,</code><code>const</code> <code>void</code><code>*);</code><code>//style of c</code>
<code>void</code> <code>f(</code><code>char</code><code>* v,</code><code>int</code> <code>sz)</code>
<code> </code><code>//error,as qsort is style of c</code>
<code> </code><code>//but compare is style of c++</code>
<code> </code><code>qsort</code><code>(v,sz,1,&compare);</code>
<code> </code><code>qsort</code><code>(v,sz,1,&ccomp);</code><code>//ok</code>
<code> </code>
<code> </code><code>isort(v,sz,1,&compare);</code><code>//ok</code>
<code> </code><code>//error,as isort is style of c++</code>
<code> </code><code>//but ccomp is style of c</code>
<code> </code><code>isort(v,sz,1,&ccopm);</code>
注意:typedef int (*ft) (const void* ,const void*),表示定義了一個函數指針的别名ft,這種函數指針指向的函數有這樣的特征:傳回值為int型、有兩個參數,參數類型可以為任意類型的指針(因為為void*)。
最典型的函數指針的别名的例子是,信号處理函數signal,它的定義如下:
<code>typedef</code> <code>void</code> <code>(*handler)(</code><code>int</code><code>);</code>
<code>handler</code><code>signal</code><code>(</code><code>int</code> <code>,handler);</code>
上面的代碼定義了信函處理函數signal,它的傳回值類型為handler,有兩個參數分别為int、handler。 這樣避免了要這樣定義signal函數:
<code>void</code> <code>(*</code><code>signal</code> <code>(</code><code>int</code> <code>,</code><code>void</code><code>(*)(</code><code>int</code><code>) ))(</code><code>int</code><code>)</code>
比較之後可以明顯的體會到typedef的好處。