在嵌入式开发过程中,你是否经常看到类似下面的代码。

#ifdef __cplusplus
extern "C" {
#endif
……
#ifdef __cplusplus
}
#endif

下面我们就来深入剖析。

很明显​​#ifdef/#endif、#ifndef/#endif​​用于条件编译,​​#ifdef _cplusplus/#endif _cplusplus​​——表示如果定义了宏​​_cplusplus​​,就执行​​#ifdef/#endif​​之间的语句,否则就不执行。

在这里为什么需要​​#ifdef _cplusplus/#endif _cplusplus​​​呢?因为C语言中不支持​​extern "C"​​​声明,如果你明白​​extern "C"​​​的作用就知道在C中也没有必要这样做,这就是条件编译的作用!在.c文件中包含了​​extern "C"​​时会出现编译时错误。

1 extern "C"的前世今生

C++语言的创建初衷是​​“a better C”​​,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同:编译器把每一个通过命令行指定的源代码文件看做一个独立的编译单元,生成目标文件;然后,链接器通过查找这些目标文件的符号表将它们链接在一起生成可执行程序。编译和链接是两个阶段的事情;事实上,编译器和链接器是两个完全独立的工具。编译器可以通过语义分析知道那些同名的符号之间的差别;而链接器却只能通过目标文件符号表中保存的名字来识别对象。

所以,编译器进行名字粉碎的目的是为了让链接器在工作的时候不陷入困惑,将所有名字重新编码,生成全局唯一,不重复的新名字,让链接器能够准确识别每个名字所对应的对象。

作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同

但 C语言却是一门单一名字空间的语言,也不允许函数重载,也就是说,在一个编译和链接的范围之内,C语言不允许存在同名对象。比如,在一个编译单元内部,不允许存在同名的函数,无论这个函数是否用static修饰;在一个可执行程序对应的所有目标文件里,不允许存在同名对象,无论它代表一个全局变量,还是一个函数。所以,C语言编译器不需要对任何名字进行复杂的处理(或者仅仅对名字进行简单一致的修饰(decoration),比如在名字前面统一的加上单下划线_)。

C++的缔造者Bjarne Stroustrup在最初就把——能够兼容C,能够复用大量已经存在的C库——列为C++语言的重要目标。但两种语言的编译器对待名字的处理方式是不一致的,这就给链接过程带来了麻烦。

extern "C"的主要作用就是为了能够正确实现C++代码调用C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且底层的库也是C语言写的,为了更好的支持原来的和已经写好的C代码。

2深层揭密extern “C”

​extern "C"​​​包含双重含义,首先,被它修饰的目标是​​“extern”​​的;其次,被它修饰的目标是“C”的。

2.1 extern关键字

​extern​​是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。

在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。

首先来一个例子:

【file1.c】

int x=1;
int f(){do something here}

【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;// x equals to default of int type 0
int f();
extern double b;
extern int c;

在这段代码中存在着这样的三个错误:


1.x被定义了两次
2.b两次被声明为不同的类型
3.c被声明了两次,但却没有定义


通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。

例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

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

2.2 "C"修饰符

在编写C++代码时可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。

例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。

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

extern "C" char* strcpy(char*,const char*);

注意它与下面的声明的不同之处:

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"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern “C”,表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数或变量等。

3 extern "C"的惯用法

3.1 C++调用C

在包含C语言头文件时,需要使用extern "C"修饰。

【cfun.h】

#ifndef _C_FUN_H_
#define _C_FUN_H_

void cfun(int i);

#endif

【cfun.c】

#include <stdio.h>
#include "cfun.h"

void cfun(int i)
{
printf("cfun : %d\n", i);
}

【main.cpp】

* @file                main.cpp
* @author BruceOu
* @version V1.0
* @date 2021-08-06
* @blog https://blog.bruceou.cn/
* @Official Accounts 嵌入式实验楼
* @brief
******************************************************************************
*/

#include "cfun.h"

int main()
{
cfun(2);
return 0;
}

没有加extern "C"则会出现以下错误:

《C语言杂记》详解extern “C“_extern

C语言中并没有重载和类这些特性,不会被编译为_cfun_int,而是直接编译为_cfun等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的cfun(2)时,它会去找_cfun_int(2)。因此extern "C"的作用就体现出来了。

因此需要将C的头文件进行修饰。

/**
******************************************************************************
* @file main.cpp
* @author BruceOu
* @version V1.0
* @date 2021-08-06
* @blog https://blog.bruceou.cn/
* @Official Accounts 嵌入式实验楼
* @brief
******************************************************************************
*/
#include <iostream>

extern "C" {
#include "cfun.h"
}

int main()
{
cfun(2);

return 0;
}

再次编译:

《C语言杂记》详解extern “C“_extern_02

可以看到这里的cfun是按照C编译的。

运行结果如下:

《C语言杂记》详解extern “C“_c语言_03

C++调用C,extern “C” 的作用是:让C++连接器找调用函数的符号时采用C的方式。

3.2 C调用C++

在使用C调用C++之前,先看看C++重载的例子。

【cppfun.h】

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_

void print(int i);
void print(char c);
void print(float f);

#endif

【cppfun.cpp】

#include "cppfun.h"
#include <stdio.h>


void print(int i)
{
printf("cppfun : %d\n", i);
}

void print(char c)
{
printf("cppfun : %c\n", c);
}

void print(float f)
{
printf("cppfun : %f\n", f);
}

编译,然后查看编译情况:

《C语言杂记》详解extern “C“_#endif_04

可以看到print的几个函数被唯一标识。

注:不同的编译器实现可能不一样,但是都是利用这种机制。

C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。C语言中并没有重载和类这些特性。

C++的头文件需添加extern “C”,但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。

【cppfun.h】

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_

#ifdef __cplusplus
extern "C" {
#endif

void cppfun(int i);

#ifdef __cplusplus
}
#endif

#endif

【cppfun.cpp】

#include <stdio.h>
#include "cppfun.h"

void cppfun(int i)
{
printf("cppfun : %d\n", i);
}

【main.c】

* @file                main.c
* @author BruceOu
* @version V1.0
* @date 2021-08-06
* @blog https://blog.bruceou.cn/
* @Official Accounts 嵌入式实验楼
* @brief
******************************************************************************
*/

#include "cppfun.h"

int main()
{
cppfun(2);
return 0;
}

编译:

《C语言杂记》详解extern “C“_extern_05

运行:

《C语言杂记》详解extern “C“_extern_06

C调用C++,使用extern "C"则是告诉编译器依照C的方式来编译封装接口,当然接口函数里面的C++语法还是按C++方式编译。

总结

1.C++调用一个C语言编写的.so库时,包含描述.so库中函数的头文件时,应该将对应的头文件放置在​​extern “C”{}​​格式的{}中。

2.C中引用C++中的全局函数时,C++的头文件需要加​​extern “C”​​​,而C文件中不能用​​extern “C”​​​,只能使用​​extern​​关键字修饰。





欢迎访问我的网站

​BruceOu的哔哩哔哩​

​BruceOu的主页​

​BruceOu的博客​

​BruceOu的简书​


微信公众号

《C语言杂记》详解extern “C“_extern_07