天天看點

extern "C"詳解導入原理舉例總結

導入

最近看公司項目源碼,發現每個C頭檔案中都包含 EXTERN_STDC_BEGIN 和 EXTERN_STDC_END 這兩個宏,如:

#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__

#include "msg_buffer.h"

EXTERN_STDC_BEGIN

void send(component *src, component *dest);
void receive(component *dest);

EXTERN_STDC_END

#endif
           

出于好奇,我檢視了一下這兩個宏的聲明,發現:

#ifdef __cplusplus
#define EXTERN_STDC_BEGIN extern "C" {
#define EXTERN_STDC_END }
#else
#define EXTERN_STDC_BEGIN
#define EXTERN_STDC_END
#endif
           

這與我們平時經常看到的#ifdef __cplusplus    extern "C" {     #endif其實是一樣的。

如果項目是純C語言編寫的,那 EXTERN_STDC_BEGIN 和 EXTERN_STDC_END 就是空宏,如上面代碼段5、6行所示的那樣。預處理過後,在使用#include指令包含了這個頭檔案的.c檔案中,對應的#include語句就會被替換成如下形式:

#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__

#include "msg_buffer.h"

void send(component *src, component *dest);
void receive(component *dest);

#endif
           

而如果項目檔案是由C和C++混合程式設計實作的,并且某些.cpp檔案以#include的形式将這個頭檔案包含在内,那麼在這些.cpp檔案内部對應的#include語句就會被替換成如下形式:

#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__

#include "msg_buffer.h"

extern "C" {

void send(component *src, component *dest);
void receive(component *dest);

}

#endif
           

原理

那為什麼需要有這個 extern "C" 呢?

這是因為在g++中有一種稱為“名字粉碎”的機制,當對一個.cpp檔案進行編譯的時候,g++會将檔案中的各個函數名進行“粉碎”,按照“_函數名_參數類型”的形式存儲到目标檔案符号表中。如:對于函數int add(int x, int y); 使用gcc編譯過後,目标檔案符号表中會生成類似_add的函數符号;而使用g++編譯後,則會生成_add_int_int的函數符号,這也從一定程度上解釋了C++中的函數重載機制。

此外,聯想一下makefile中工程檔案的編譯連結原理:編譯器會将源代碼檔案(.c或.cpp檔案)看做一個獨立的編譯單元生成目标檔案,随後,連結器通過目标檔案符号表将它們連結在一起得到一個最終的可執行檔案。

編譯和連結是兩個不同階段的事情,事實上,編譯器和連結器是兩個完全獨立的工具。一般來說,編譯器可以通過語義分析知道那些同名的符号之間的差别,而連結器則隻能通過目标檔案符号表中儲存的符号名來識别對象。是以,g++編譯器進行“名字粉碎”會将所有名字重新編碼,生成全局唯一的新名字,讓連結器能夠準确識别每個名字所對應的對象,進而避免連結器在工作時陷入困惑。

然而 C語言是一種隻有一個全局命名空間的語言,不允許進行函數重載。也就是說,在一個編譯和連結範圍之内,C語言不允許出現同名的函數或變量,因為C編譯器不會對名字進行任何複雜的處理(或者僅僅對名字進行簡單一緻的修飾,如在名字前面統一加一個下劃線_)。

C++的締造者Bjarne Stroustrup在一開始就把能向下相容C,即能夠複用大量已經存在的C庫作為C++的重要目标之一。然而,C和C++編譯器對函數處理方式的不一緻給連結的過程帶來了一丁點的“麻煩”。、

舉例

就拿上面那個頭檔案為例,此處我們假設檔案名為communication.h,其實作放在對應的.c 源檔案中。假定此時不使用extern "C",如下所示:

#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__

#include "msg_buffer.h"

void send(component *src, component *dest);
void receive(component *dest);

#endif
           

我們使用gcc對其進行編譯,生成目标檔案communication.o。由于C編譯器不會進行“名字粉碎”,是以在communication.o的符号表中,send和receive以_send和_receive的形式存放。

随着工程項目的進展,假設需要在另外一個.cpp檔案調用這個頭檔案中聲明的函數,是以,需要在這個.cpp檔案中以#include的形式包含頭檔案communication.h。此處我們假定這個.cpp檔案的名字為fcs.cpp,那麼在編譯時,C++編譯器會進行“名字粉碎”,使得在目标檔案fcs.o的符号表中會出現以下形式的函數符号名:_send_component_component和_receive_component。

要得到一個最終可執行的檔案,還需要将communication.o和fcs.o放在一起進行連結。然而,由于在兩個目标檔案對于同一個函數的命名不一緻,連結器将報告“符号未定義”的錯誤。

為了解決這一問題,C++引入了連結規範的概念,連結規範的作用是告訴C++編譯器:對所有使用連結規範進行修飾的聲明或定義,應該按照其指定語言的方式進行處理。連結規範的用法有兩種:

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

      2. 一組聲明的連結規範,如:

             extern "C"

             {

            void foo();

            int bar();

             }

現在我們按照上面的方法将頭檔案communication.h修改成如下的形式:

#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__

#include "msg_buffer.h"

extern "C" {
void send(component *src, component *dest);
void receive(component *dest);
}

#endif
           

然後使用g++重新對fcs.cpp進行編譯,所生成目标檔案fcs.o的符号表中兩個函數就會存儲為_send和_receive的形式。這樣,當再次把communication.o和fcs.o放在一起進行連結時,就不會出現“符号未定義”的錯誤了。

然而,此時如果重新發起整個工程的建構,編譯器就會對communication.c重新進行編譯,此時會報告“文法錯誤”,因為extern "C"是C++的文法,而communication.c檔案是由gcc編譯的。此時,可以按之前已經讨論的,使用__cplusplus對gcc和g++進行識别。修改後的communication.h的代碼如下所示:

#ifndef __COMMUNICATION_H__
#define __COMMUNICATION_H__

#include "msg_buffer.h"

#ifdef __cplusplus
extern "C" {
#endif

void send(component *src, component *dest);
void receive(component *dest);

#ifdef __cplusplus
}
#endif

#endif
           

這樣,不論工程項目是否包含.cpp檔案,編譯過後,函數符号名都能保持統一的形式,連結時就不會出現問題了。

總結

在工程項目中,為了避免出現“符号未定義”等問題,頭檔案都以下面的形式進行編寫:

#ifdef __cplusplus
extern "C" {
#endif

/*  函數聲明  */

#ifdef __cplusplus
}
#endif
           

如果覺着在每個頭檔案裡都把這6行寫一次比較麻煩,我們可以定義兩個宏,就像本文一開始的EXTERN_STDC_BEGIN和EXTERN_STDC_END宏那樣,此時頭檔案就按以下形式編寫:

EXTERN_STDC_BEGIN

/*  函數聲明  */

EXTERN_STDC_END
           

這樣看起來是不是就清爽很多了~