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

这样看起来是不是就清爽很多了~

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在C语言中,extern关键字用于声明一个在其他地方定义的变量。它用于告诉编译器该变量在其他地方已经被定义,并且可以在当前文件中使用。如果一个extern变量在当前文件中没有定义,那么编译器会在其他文件中查找该变量的定义。 在引用中的示例中,extern关键字用于声明一个在其他文件中定义的变量num。在main函数中,通过使用extern关键字,我们可以在当前文件中使用变量num,并将其打印出来。 引用中提到,extern变量是外部存储变量,用于声明在其他转换单元中定义的变量。换句话说,extern关键字用于引用其他文件或模块中定义的变量,以使得当前文件中的代码能够使用这些变量。 引用中提到,extern "C"是一种声明或定义C语言符号的方法,用于与C语言保持兼容。在C++中,由于函数名和变量名在编译时会被改编,extern "C"可以用来告诉编译器使用C语言的命名约定,以便在C++代码中使用C语言的函数和变量。 所以,c语言extern关键字用于声明一个在其他地方定义的变量,并且使其在当前文件中可用。同时,extern "C"用于在C++代码中使用C语言的函数和变量。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [C语言extern变量](https://blog.csdn.net/qq_49005497/article/details/126075937)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [C++中的extern “C”用法详解](https://download.csdn.net/download/weixin_38611812/14867636)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值