回覆列表
-
1 # IT百科大學堂
-
2 # 任教主
c++是c語言的繼承和發展,c++是在c語言的基礎上添加了面向物件、模板等現代程式設計語言的特性而發展起來的。兩者無論是從語法規則上,還是從運算子的數量和使用上,都非常相似,而且三種基本結構的描述也一樣,尤其是學過c語言的人在用c++程式設計時習慣了用c中的格式輸出與輸入函式完成輸出與輸入,讓人就產生了一種錯覺,好像是兩種語言混用了,其實這也是屬於c++中的內容,它只是繼承了c中的一些內容。但是這兩種語言有本質的區別,它們的程式設計方法不同,c是面向過程的程式設計語言而c++是面向物件的程式設計語言。
在用C++的專案原始碼中,經常會不可避免的會看到下面的程式碼:
1
#ifdef __cplusplus
2
extern "C" {
3
#endif
4
5
/*...*/
6
7
#ifdef __cplusplus
8
}
9
#endif
它到底有什麼用呢,你知道嗎?而且這樣的問題經常會出現在面試or筆試中。下面我就從以下幾個方面來介紹它:
1、#ifdef _cplusplus/#endif _cplusplus及發散
2、extern "C"
2.1、extern關鍵字
2.2、"C"
2.3、小結extern "C"
3、C和C++互相呼叫 4、C和C++混合呼叫特別之處函式指標
3.1、C++的編譯和連線
3.2、C的編譯和連線
3.3、C++中呼叫C的程式碼
3.4、C中呼叫C++的程式碼
1、#ifdef _cplusplus/#endif _cplusplus及發散
在介紹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中的一段程式碼):
01
#ifndef MONGOOSE_HEADER_INCLUDED
02
#define MONGOOSE_HEADER_INCLUDED
03
04
#ifdef __cplusplus
05
extern "C" {
06
#endif /* __cplusplus */
07
08
/*.................................
09
* do something here
10
*.................................
11
*/
12
13
#ifdef __cplusplus
14
}
15
#endif /* __cplusplus */
16
17
#endif /* MONGOOSE_HEADER_INCLUDED */
然後叫你說明上面宏#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的內容就不會再次被讀取了。
2、extern "C"
首先從字面上分析extern "C",它由兩部分組成——extern關鍵字、"C"。下面我就從這兩個方面來解讀extern "C"的含義。
2.1、extern關鍵字
在一個專案中必須保證函式、變數、列舉等在所有的原始檔中保持一致,除非你指定定義為區域性的。首先來一個例子:
1
//file1.c:
2
int x=1;
3
int f(){do something here}
4
//file2.c:
5
extern int x;
6
int f();
7
void g(){x=f();}
在file2.c中g()使用的x和f()是定義在file1.c中的。extern關鍵字表明file2.c中x,僅僅是一個變數的宣告,其並不是在定義變數x,並未為x分配記憶體空間。變數x在所有模組中作為一種全域性變數只能被定義一次,否則會出現連線錯誤。但是可以宣告多次,且宣告必須保證型別一致,如:
1
//file1.c:
2
int x=1;
3
int b=1;
4
extern c;
5
//file2.c:
6
int x;// x equals to default of int type 0
7
int f();
8
extern double b;
9
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的編譯和連線規約來連結:
1
extern "C" char* strcpy(char*,const char*);
注意它與下面的宣告的不同之處:
1
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",它不會改變語義,但是會改變它的編譯和連線方式。
如果你有很多語言要加上extern "C",你可以將它們放到extern "C"{ }中。
2.3、小結extern "C"
透過上面兩節的分析,我們知道extern "C"的真實目的是實現類C和C++的混合程式設計。在C++原始檔中的語句前面加上extern "C",表明它按照類C的編譯和連線規約來編譯和連線,而不是C++的編譯的連線規約。這樣在類C的程式碼中就可以呼叫C++的函式or變數等。(注:我在這裡所說的類C,代表的是跟C語言的編譯和連線方式一致的所有語言)
3、C和C++互相呼叫
我們既然知道extern "C"是實現的類C和C++的混合程式設計。下面我們就分別介紹如何在C++中呼叫C的程式碼、C中呼叫C++的程式碼。首先要明白C和C++互相呼叫,你得知道它們之間的編譯和連線差異,及如何利用extern "C"來實現相互呼叫。
3.1、C++的編譯和連線
C++是一個面嚮物件語言(雖不是純粹的面嚮物件語言),它支援函式的過載,過載這個特性給我們帶來了很大的便利。為了支援函式過載的這個特性,C++編譯器實際上將下面這些過載函式:
1
void print(int i);
2
void print(char c);
3
void print(float f);
4
void print(char* s);
編譯為:
1
_print_int
2
_print_char
3
_print_float
4
_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關鍵字那節已經介紹)。它的程式碼如下:
1
#ifndef C_HEADER
2
#define C_HEADER
3
4
extern void print(int i);
5
6
#endif C_HEADER
相對應的實現檔案為cHeader.c的程式碼為:
1
#include <stdio.h>
2
#include "cHeader.h"
3
void print(int i)
4
{
5
printf("cHeader %d\n",i);
6
}
現在C++的程式碼檔案C++.cpp中引用C中的print(int i)函式:
1
extern "C"{
2
#include "cHeader.h"
3
}
4
5
int main(int argc,char** argv)
6
{
7
print(3);
8
return 0;
9
}
執行程式輸出:
3.4、C中呼叫C++的程式碼
現在換成在C中呼叫C++的程式碼,這與在C++中呼叫C的程式碼有所不同。如下在cppHeader.h標頭檔案中定義了下面的程式碼:
1
#ifndef CPP_HEADER
2
#define CPP_HEADER
3
4
extern "C" void print(int i);
5
6
#endif CPP_HEADER
相應的實現檔案cppHeader.cpp檔案中程式碼如下:
1
#include "cppHeader.h"
2
3
#include <iostream>
4
using namespace std;
5
void print(int i)
6
{
7
cout<<"cppHeader "<<i<<endl;
8
}
在C的程式碼檔案c.c中呼叫print函式:
1
extern void print(int i);
2
int main(int argc,char** argv)
3
{
4
print(3);
5
return 0;
6
}
注意在C的程式碼檔案中直接#include "cppHeader.h"標頭檔案,編譯出錯。而且如果不加extern int print(int i)編譯也會出錯。
4、C和C++混合呼叫特別之處函式指標
當我們C和C++混合程式設計時,有時候會用一種語言定義函式指標,而在應用中將函式指標指向另一中語言定義的函式。如果C和C++共享同一中編譯和連線、函式呼叫機制,這樣做是可以的。然而,這樣的通用機制,通常不然假定它存在,因此我們必須小心地確保函式以期望的方式呼叫。
而且當指定一個函式指標的編譯和連線方式時,函式的所有型別,包括函式名、函式引入的變數也按照指定的方式編譯和連線。如下例:
01
typedef int (*FT) (const void* ,const void*);//style of C++
02
03
extern "C"{
04
typedef int (*CFT) (const void*,const void*);//style of C
05
void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
06
}
07
08
void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
09
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
10
11
//style of C
12
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
13
14
int compare(const void*,const void*);//style of C++
15
extern "C" ccomp(const void*,const void*);//style of C
16
17
void f(char* v,int sz)
18
{
19
//error,as qsort is style of C
20
//but compare is style of C++
21
qsort(v,sz,1,&compare);
22
qsort(v,sz,1,&ccomp);//ok
23
24
isort(v,sz,1,&compare);//ok
25
//error,as isort is style of C++
26
//but ccomp is style of C
27
isort(v,sz,1,&ccopm);
28
}
注意:typedef int (*FT) (const void* ,const void*),表示定義了一個函式指標的別名FT,這種函式指標指向的函式有這樣的特徵:返回值為int型、有兩個引數,引數型別可以為任意型別的指標(因為為void*)。
最典型的函式指標的別名的例子是,訊號處理函式signal,它的定義如下:
1
typedef void (*HANDLER)(int);
2
HANDLER signal(int ,HANDLER);
上面的程式碼定義了信函處理函式signal,它的返回值型別為HANDLER,有兩個引數分別為int、HANDLER。 這樣避免了要這樣定義signal函式:
1
void (*signal (int ,void(*)(int) ))(int)
比較之後可以明顯的體會到typedef的好處。