extern “C”的用法和使用场景
--- 称呼:Amos email:huangxuangui@sina.com
目录
1 问题定义
在研究操作系统源代码或者在嵌入式系统中编写程序时,经常会发现下面这种用法:
#ifndef __OTHER_FILE_C_H__--------------------防止一个文件中多次包含这个头文件
#define __OTHER_FILE_C_H__--------------------防止一个文件中多次包含这个头文件
#ifdef __cplusplus------------------------如果使用的是C++编译器
extern "C" {
#endif
……
extern void c_main();-----这部分内容一般是函数声明或一些数据结构的定义等
……
#ifdef __cplusplus------------------------如果使用的是C++编译器
}
#endif
#endif
其实extern “C”可以用在函数定义之前,也可以用在函数声明之前。这两者的区别在后续内容中将会讲到,但是一般用在函数声明之前。
或许大家都知道,extern “C”的作用就是在C++环境中使函数按照C的标准来编译和链接,但这种说话不全面。比如说当extern “C”放在函数声明之前,就不会改变函数的编译方式,只是指定编译器按照C的标准链接,而不是按照C++的标准去链接函数。
其实在头文件.h中下面这种用法
extern “C” externvoid c_main();
等效与在 .cpp文件中直接用extern “C” void c_main();
但是有四个问题值得我们仔细思考:
1、 在C++编译环境(VC 6.0)中,工程文件中同时包含.c文件和.cpp文件,那么编译.c和.cpp按照什么编译规则来编译的,有什么区别?
2、 在C++编译环境中, extern “C”放在函数声明之前的作用是什么?
3、 加了cpp文件中在函数声明前加extern “C”的作用是什么?
4、 那么什么情况下才需要使用extern “C”呢?
除非有特别指出特定的编译器,否则以下采用的实验环境默认是VC 6.0。
2 原理
C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。
1. 被extern"C"限定的函数或变量是extern类型的;
2. extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:extern int a;仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;
未加extern “C”声明时的编译方式:
首先看看C++中对类似C的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void c_main();
该函数被C编译器编译后在符号库中的名字为_ c_main,而C++编译器则会产生像
?c_main2@@YAXXZ之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制)。
3 分析
3.1 VC中.c与.cpp的编译规则
问题:在C++编译环境(VC 6.0)中,工程文件中同时包含.c文件和.cpp文件,那么编译.c和.cpp按照什么编译规则来编译的,有什么区别?
解答:在VC 6.0工程文件中,可能含有.cpp文件和.c文件混合编译。在.c的源文件按照C的标准来编译,函数名之前加入”_”,并把“_函数名”放入到符号表中,以后链接时使用。.cpp的源文件按照C++的标准编译,会修改函数名,这个不同的编译器有不同的实现方式,在VC中,函数名被修改的有些乱,如下:
ü testC.c文件中的代码:
#include "stdio.h"
void c_main()
{
printf ("this isc program\n!");
}
通过objdump工具反编译testC.obj文件为汇编代码为:
00000000 <_c_main>:
0: 55 push %ebp
1: 8b ec mov %esp,%ebp
3: 83 ec 40 sub $0x40,%esp
6: 53 push %ebx
7: 56 push %esi
8: 57 push %edi
9: 8d 7d c0 lea -0x40(%ebp),%edi
c: b9 10 00 00 00 mov $0x10,%ecx
11: b8 cc cc cc cc mov $0xcccccccc,%eax
16: f3 ab rep stos %eax,%es:(%edi)
18: 68 00 00 00 00 push $0x0
1d: e8 00 00 00 00 call 22 <_c_main+0x22>
22: 83 c4 04 add $0x4,%esp
有上述汇编代码可见,c_main函数编译后,函数名为_c_main(), 可见.c文件在VC中采用的是标准C的编译方式。
ü testCPP.cpp文件中的代码
00000000 <?c_main2@@YAXHHH@Z>:
0: 55 push %ebp
1: 8b ec mov %esp,%ebp
3: 83 ec 40 sub $0x40,%esp
6: 53 push %ebx
7: 56 push %esi
8: 57 push %edi
9: 8d 7d c0 lea -0x40(%ebp),%edi
c: b9 10 00 00 00 mov $0x10,%ecx
11: b8 cc cc cc cc mov $0xcccccccc,%eax
16: f3 ab rep stos %eax,%es:(%edi)
18: 68 00 00 00 00 push $0x0
1d: e8 00 00 00 00 call 22 <?c_main2@@YAXHHH@Z+0x22>
22: 83 c4 04 add $0x4,%esp
上述汇编代码可见,c_main函数编译后,函数名为?c_main2@@YAXHHH@Z, 可见.cpp文件在VC中采用的是标准C++的编译方式。
3.2 extern “C”放在函数声明前的作用
问题:在C++编译环境中,用了(extern “C”+函数声明)与(不用extern “C”+函数声明),区别在哪?
解答:用(extern “C”+函数声明),表明CPP此文件中的函数,以C的标准来链接(“_函数名”), 如果在CPP文件中函数声明前不用extern “C”,则采用C++的标准来链接(比如函数名为”?c_main2@@YAXHHH@Z”)。
Ø 如果在test_a.cpp文件,有extern “C”放在函数声明之前,而其函数实现放在另外一个文件test_b.cpp中,则C++编译器不会改变test_b.cpp文件中函数的编译规则(还是按照c++的规则来编译),只是通知编译器在链接test_a.cpp中函数时采取C的标准方式链接函数。如:vc 6.0工程中有两个文件,一个为test_a.cpp, 另一个文件为test_b.cpp。 test_a.cpp文件中代码如下:
#include "stdio.h"
extern "C" void c_main2(int a, int b, int c);
void main(void)
{
c_main2(0,0,0);
return ;
}
test_b.cpp文件中的代码如下:
#include <stdio.h>
void c_main2(int a, int b, int c)
{
printf ("this iscpp program\n!");
}
结果:编译通过,链接失败。
1、 通过objdump工具查看test_a.obj文件的符号表,发现链接时需要查找_c_main2()。
如下:
[ 14](sec 3)(fl0x00)(ty 20)(scl 2) (nx 1) 0x00000000 _main
AUX tagndx 18 ttlsiz 0x37 lnnos 1224 next 0
_main :
1 : 00000018
3 : 00000026
[ 16](sec 0)(fl0x00)(ty 20)(scl 2) (nx 0) 0x00000000 _c_main2
[ 17](sec 0)(fl0x00)(ty 20)(scl 2) (nx 0) 0x00000000 __chkesp
[ 18](sec 3)(fl 0x00)(ty 0)(scl 101) (nx 1) 0x00000000 .bf
2、通过objdump工具反汇编test_b.obj,发现在c_main2()函数反汇编出来后,采用的是C++的编译方式进行的,函数名做了更改?c_main2@@YAXHHH@Z,如下:
00000000 <?c_main2@@YAXHHH@Z>:
0: 55 push %ebp
1: 8b ec mov %esp,%ebp
3: 83 ec 40 sub $0x40,%esp
6: 53 push %ebx
7: 56 push %esi
8: 57 push %edi
9: 8d 7d c0 lea -0x40(%ebp),%edi
c: b9 10 00 00 00 mov $0x10,%ecx
11: b8 cc cc cc cc mov $0xcccccccc,%eax
16: f3 ab rep stos %eax,%es:(%edi)
18: 68 00 00 00 00 push $0x0
1d: e8 00 00 00 00 call 22 <?c_main2@@YAXHHH@Z+0x22>
22: 83 c4 04 add $0x4,%esp
由于链接时在其他模块中找不到_c_main2()函数,故提示链接失败。
3.3 extern “C”放在函数定义前的作用
问题:在cpp文件中在函数定义前加了extern “C”后,此的函数定义的编译方式是否会改变(按照c的编译方式编译还是按照c++的编译方式编译)?
Ø 解答: 会改变,如果extern “C”放在函数定义之前,则C++编译器使得函数按照C的标准来编译和链接函数。
如VC6.0工程中有以文件test.cpp,文件中代码内容如下:
#include"stdio.h"
extern "C"void c_main2(int a, int b, int c)
{
printf ("this is cppprogram\n!");
}
void main(void)
{
c_main2(0,0,0);
return ;
}
结果:编译链接都通过。
通过objdump工具查看其生成的.obj文件,发现生成的c_main2()函数的汇编代码为:
00000000<_c_main2>:
0: 55 push %ebp
1: 8b ec mov %esp,%ebp
3: 83 ec 40 sub $0x40,%es
6: 53 push %ebx
7: 56 push %esi
8: 57 push %edi
9: 8d 7d c0 lea -0x40(%eb
c: b9 10 00 00 00 mov $0x10,%ec
11: b8 cc cc cc cc mov $0xcccccc
16: f3 ab rep stos%eax,%e
18: 6800 00 00 00 push $0x0
1d: e8 00 00 00 00 call 22 <_c_ma
22: 83 c4 04 add $0x4,%esp
虽然c_main2()函数在CPP文件中,但是在函数定义前加了extern “C”后,采用的是C标准编译方式,在函数名前加下划线,变成_c_main2()。该用GCC编译,结果一样,也使在函数名前加下划线。
3.4 使用场景
问题:那么什么情况下才需要使用extern “C”呢?
解答:
1、 由于系统内核一般是使用C语言来编写的,系统内核中用C语言实现了很多库。而上层应用程序有可能是用C++来开发,如果在内核库函数头文件中不用extern “C”来声明库函数的话,在编写C++应用程序时,包含库头文件,在C++文件链接时就会以C++标准来链接库的函数名,而在库文件实现时是用C来实现的,二者函数名不同,在链接时就会出现找不到函数的现象。
2、 在有些工程中,即包含.c文件有包含.cpp文件,如VC的工程。.cpp文件要调用.c文件中的函数时,需要extern “C” 声明.c文件中的函数,在CPP文件中让C++编译器使用C的标准来链接C文件中的函数
3.5 常用做法
在编写C代码的同时,为了防止被C++程序调用,通常会在C源代码对应的头文件中加入
#ifdef __cplusplus------------------------如果使用的是C++编译器
extern "C" {
#endif
……
……
extern void c_main();------------------------------------外部函数接口声明
#ifdef __cplusplus------------------------如果使用的是C++编译器
}
#endif
这样做节省了维护代码的开销,在CPP代码中需要调用C中的接口时,直接包含C的头文件即可
由于个人水平有限,如有不当之处,欢迎拍砖,共同学习成长。联系方式:huangxuangui@sina.com 姓名:Raymondamos