深入理解extern "C"

在用C/C++的项目源码中,经常会看到下面结构的代码:

#ifdef __cplusplus
extern "C" {
#endif
 
/*...*/
 
#ifdef __cplusplus
}
#endif

这段代码到底有什么用呢,你知道吗?而且这样的问题经常出现在面试/笔试中,下面我就深入介绍下它。


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"时会出现编译时错误。

既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。曾经腾讯笔试就考过这个题目,给出类似下面的代码(下面是我最近在研究的一个开源Json解析器——cJson的头文件cJSON.h中的一段代码):

#ifndef cJSON__h
#define cJSON__h

#ifdef __cplusplus
extern "C"
{
#endif

/*...*/

#ifdef __cplusplus
}
#endif

#endif
请说明上面宏#ifndef/#endif的作用?

答:这个头文件cJSON.h可能在一个项目中被多个源文件包含(#include "cJSON.h"),这些冗余可能导致错误,比如一个头文件包含结构体定义,在一个源文件中cJSON.h可能会被#include两次(如,a.h头文件包含了cJSON.h,而在b.c文件中#include a.h和cJSON.h)——这就会出错(在一个源文件中,一个结构体被定义了两次)。从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际。为了解决这个问题,上面代码中的

#ifndef cJSON__h
#define cJSON__h

/*...*/

#endif

就起作用了。如果定义了cJSON__h,#ifndef/#endif之间的内容就被忽略掉。因此,编译时第一次看到cJSON.h头文件,它的内容会被读取且定义cJSON__h。之后再次#includecJSON.h头文件时,cJSON__h就已经定义了,cJSON.h的内容就不会再次被读取了。


2. extern "C"

首先从字面上分析extern "C",它由两部分组成——extern关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。

(1) extern

首先看一个例子:

//file1.c:
    int x = 1;
    int f() { <span style="line-height: 25.2px; font-family: Georgia, "Times New Roman", Times, sans-serif;">/*...*/ </span><span style="line-height: 25.2px; font-family: Georgia, "Times New Roman", Times, sans-serif;">}</span>
//file2.c:
    extern int x;
    int f();
    void g() { x = f(); }
在file2.c中g()使用的x和f()是定义在file1.c中的。extern关键字表明file2.c中的x,仅仅是一个变量的声明,并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一个全局变量,只能被定义一次,否则会出现链接错误;但是可以声明多次,且声明必须保证类型一致。
//file1.c:
    int x = 1;
    int b = 1;
    extern c;
//file2.c:
    int x;
    int f();
    extern double b;
    extern int c;
上面这段代码中存在着以下三个错误:
1. x被定义了两次
2. b两次被声明为不同的类型
3. c被声明了两次,但却没有定义

extern是C/C++语言中表明函数全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用模块A中定义的全局变量和函数时,只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在链接阶段中从模块A编译生成的目标代码中找到此函数。

与extern对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

(2)  "C"

一个C++程序可能会包含其它语言编写的部分代码,同样的,C++编写的代码片段也可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至同一种语言编写的代码,经过不同的编译器编译,也不能互相调用。例如,不同语言和同种语言的不同实现可能会在注册变量和参数在栈上的布局,这两个方面不一样。

为了使它们遵守统一规则,可以使用extern指定一个编译和链接规约。例如,声明C和C++标准库函数strcyp(),并指定它应该根据C的编译和链接规约来链接:

extern "C" char* strcpy(char*,const char*);
注意它与下面的声明的不同之处(下面的这个声明仅表示在链接的时候调用strcpy()):
extern char* strcpy(char*,const char*);
extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的"C",表示的是一种编译和 链接规约,而不是一种语言。C表示符合C语言的编译和 链接规约的任何语言,如Fortran、assembler等。
还有要说明的是,extern "C"指令仅指定编译和 链接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。
如果你有很多代码要加上extern "C",你可以将它们放到extern "C"{ }中。

2.3 小结extern "C"

通过上面两节的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程。在代码前面加上extern "C",表明它按照类C的编译和链接规约来编译和链接,而不是按照C++的编译的链接规约。


3 C和C++互相调用

我们既然知道extern "C"是实现的C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。要理解C和C++互相调用,你得知道它们之间编译和链接的差异,及如何利用extern "C"来实现相互调用。

(1) C++的编译和链接

C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:
void print(int i);
void print(char c);
void print(float f);
void print(char* s);
编译为:
_print_int
_print_char
_print_float
_pirnt_string
这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当链接时调用print(3)时,它会去查找_print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
C++中的变量,编译时也是类似的,如全局变量可能编译为g_xx,类变量编译为c_xx等。链接是也是按照这种机制去查找相应的变量。

(2) C的编译和链接

C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。如果直接在C++中调用C的函数会失败,因为链接中调用C中的print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了。

(3) C++中调用C的代码

假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字。它的代码如下:
#ifndef C_HEADER
#define C_HEADER
 
extern void print(int i);
 
#endif C_HEADER
相对应的实现文件cHeader.c的代码为:
#include <stdio.h>
#include "cHeader.h"
void print(int i)
{
    printf("cHeader %d\n",i);
}

现在C++的代码文件C++.cpp中引用C中的print(int i)函数:

extern "C"{
#include "cHeader.h"
}
int main(int argc,char** argv)
{
    print(3);
    return 0;
}

 

(4) C中调用C++的代码

现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。在cppHeader.h头文件中定义了下面的代码:
#ifndef CPP_HEADER
#define CPP_HEADER
 
extern "C" void print(int i);
 
#endif CPP_HEADER

相应的实现文件cppHeader.cpp文件中代码如下:

#include "cppHeader.h"
 
#include <iostream>
using namespace std;
void print(int i)
{
    cout<<"cppHeader "<<i<<endl;
}

在C的代码文件c.c中调用print函数:
extern void print(int i);
int main(int argc,char** argv)
{
    print(3);
    return 0;
}

注意,在C的代码文件中直接#include "cppHeader.h"头文件,会编译出错;而且如果不加extern int print(int i),编译也会出错。


4 C和C++混合调用特别之处函数指针

当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一种语言定义的函数。如果C和C++共享同一种编译、链接、函数调用机制,这样做是可以的。然而,这样的通用机制,通常不假定它存在,因此我们必须小心地确保函数以期望的方式调用。
而且当指定一个函数指针的编译和链接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和链接。如下例:
typedef int (*FT) (const void* ,const void*);//style of C++
 
extern "C"{
    typedef int (*CFT) (const void*,const void*);//style of C
    void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
}
 
void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
 
//style of C
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
 
int compare(const void*,const void*);//style of C++
extern "C" ccomp(const void*,const void*);//style of C
 
void f(char* v,int sz)
{
    //error,as qsort is style of C
    //but compare is style of C++
    qsort(v,sz,1,&compare);
    qsort(v,sz,1,&ccomp);//ok
     
    isort(v,sz,1,&compare);//ok
    //error,as isort is style of C++
    //but ccomp is style of C
    isort(v,sz,1,&ccopm);
}
注意:typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,这种函数指针指向的函数有这样的特征:返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。
最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:
typedef void (*HANDLER)(int);
HANDLER signal(int, HANDLER);
上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。 这样避免了要这样定义signal函数:
void (*signal (int ,void(*)(int) ))(int)

比较之后可以明显的体会到typedef的好处。

5 Reference






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EnjoyCodingAndGame

愿我的知识,成为您的财富!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值