c++中的 extern "C"


原文出处:http://songpengfei.iteye.com/blog/1100239


比如说你用C开发了一个DLL库,为了能够让C++语言也能够调用你的DLL输出(Export)的函数,你需要用extern"C"来强制编译器不要修改你的函数名。

通常,在C语言的头文件中经常可以看到类似下面这种形式的代码:

C代码   收藏代码
  1. #ifdef __cplusplus  
  2. extern "C" {  
  3. #endif  
  4.   
  5. /**** some declaration or so *****/  
  6.   
  7. #ifdef __cplusplus  
  8. }  
  9. #endif  
 

 

那么,这种写法什么用呢?实际上,这是为了让CPP能够与C接口而采用的一种语法形式。之所以采用这种方式,是因为两种语言之间的一些差异所导致的。由于CPP支持多态性,也就是具有相同函数名的函数可以完成不同的功能,CPP通常是通过参数区分具体调用的是哪一个函数。在编译的时候,CPP编译器会将参数类型和函数名连接在一起,于是在程序编译成为目标文件以后,CPP编译器可以直接根据目标文件中的符号名将多个目标文件连接成一个目标文件或者可执行文件。但是在C语言中,由于完全没有多态性的概念,C编译器在编译时除了会在函数名前面添加一个下划线之外,什么也不会做(至少很多编译器都是这样干的)。由于这种的原因,当采用CPP与C混合编程的时候,就可能会出问题。假设在某一个头文件中定义了这样一个函数:

int foo(int a, int b);

而这个函数的实现位于一个.c文件中,同时,在.cpp文件中调用了这个函数。那么,当CPP编译器编译这个函数的时候,就有可能会把这个函数名改成_fooii,这里的ii表示函数的第一参数和第二参数都是整型。而C编译器却有可能将这个函数名编译成_foo。也就是说,在CPP编译器得到的目标文件中,foo()函数是由_fooii符号来引用的,而在C编译器生成的目标文件中,foo()函数是由_foo指代的。但连接器工作的时候,它可不管上层采用的是什么语言,它只认目标文件中的符号。于是,连接器将会发现在.cpp中调用了foo()函数,但是在其它的目标文件中却找不到_fooii这个符号,于是提示连接过程出错。extern"C" {}这种语法形式就是用来解决这个问题的。本文将以示例对这个问题进行说明。

首先假设有下面这样三个文件:

C代码   收藏代码
  1. /* file: test_extern_c.h */  
  2.   
  3. #ifndef __TEST_EXTERN_C_H__  
  4. #define __TEST_EXTERN_C_H__  
  5.   
  6. #ifdef __cplusplus  
  7. extern "C" {  
  8. #endif  
  9.   
  10. /* 
  11. * this is a test function, which calculate 
  12. * the multiply of a and b. 
  13. */  
  14.   
  15. extern int ThisIsTest(int a, int b);  
  16.   
  17. #ifdef __cplusplus  
  18. }  
  19. #endif  
  20.   
  21. #endif   
 

 

在这个头文件中只定义了一个函数,ThisIsTest()。这个函数被定义为一个外部函数,可以被包括到其它程序文件中。假设ThisIsTest()函数的实现位于test_extern_c.c文件中:

C代码   收藏代码
  1. /* test_extern_c.c */  
  2.   
  3. #include "test_extern_c.h"  
  4.   
  5. int ThisIsTest(int a, int b)  
  6. {  
  7.   return (a + b);  
  8. }   
 

 

可以看到,ThisIsTest()函数的实现非常简单,就是将两个参数的相加结果返回而已。现在,假设要从CPP中调用ThisIsTest()函数:

Cpp代码   收藏代码
  1. /* main.cpp */  
  2.   
  3. #include "test_extern_c.h"  
  4.   
  5. #include <stdio.h>  
  6. #include <stdlib.h>  
  7.   
  8. class FOO {  
  9.   
  10. public:  
  11.   
  12.   int bar(int a, int b)  
  13.   
  14.     {  
  15.   
  16.         printf("result=%i\n", ThisIsTest(a, b));  
  17.   
  18.     }  
  19.   
  20. };  
  21.   
  22. int main(int argc, char **argv)  
  23. {  
  24.   
  25.   int a = atoi(argv[1]);  
  26.   
  27.   int b = atoi(argv[2]);  
  28.   
  29.   FOO *foo = new FOO();  
  30.   
  31.   foo->bar(a, b);  
  32.   
  33.   return(0);  
  34. }   
/* main.cpp */

#include "test_extern_c.h"

#include <stdio.h>
#include <stdlib.h>

class FOO {

public:

  int bar(int a, int b)

    {

        printf("result=%i\n", ThisIsTest(a, b));

    }

};

int main(int argc, char **argv)
{

  int a = atoi(argv[1]);

  int b = atoi(argv[2]);

  FOO *foo = new FOO();

  foo->bar(a, b);

  return(0);
} 

 

在这个CPP源文件中,定义了一个简单的类FOO,在其成员函数bar()中调用了ThisIsTest()函数。下面看一下如果采用gcc编译test_extern_c.c,而采用g++编译main.cpp并与test_extern_c.o连接会发生什么情况:

[cyc@cyc src]$ gcc -c test_extern_c.c

[cyc@cycsrc]$ g++ main.cpp test_extern_c.o

[cyc@cyc src]$ ./a.out 4 5         

result=9

可以看到,程序没有任何异常,完全按照预期的方式工作。那么,如果将test_extern_c.h中的extern"C" {}所在的那几行注释掉会怎样呢?注释后的test_extern_c.h文件内容如下:

C代码   收藏代码
  1. /* test_extern_c.h */  
  2.   
  3. #ifndef __TEST_EXTERN_C_H__  
  4. #define __TEST_EXTERN_C_H__  
  5.   
  6. //#ifdef   __cplusplus  
  7. //extern "C" {  
  8. //#endif  
  9.   
  10. /* 
  11. /* this is a test function, which calculate 
  12. * the multiply of a and b. 
  13. */  
  14.   
  15. extern int ThisIsTest(int a, int b);  
  16.   
  17. //#ifdef   __cplusplus  
  18. // }  
  19. //#endif  
  20.   
  21. #endif   
 

 

之外,其它文件不做任何的改变,仍然采用同样的方式编译test_extern_c.c和main.cpp文件:

[cyc@cyc src]$ gcc -c test_extern_c.c

[cyc@cyc src]$ g++ main.cpptest_extern_c.o

/tmp/cca4EtJJ.o(.gnu.linkonce.t._ZN3FOO3barEii+0x10):In function `FOO::bar(int, int)':

:undefined reference to `ThisIsTest(int, int)'

collect2: ld returned 1 exit status

在编译main.cpp的时候就会出错,连接器ld提示找不到对函数ThisIsTest()的引用。


为了更清楚地说明问题的原因,我们采用下面的方式先把目标文件编译出来,然后看目标文件中到底都有些什么符号:

[cyc@cyc src]$ gcc -c test_extern_c.c  

[cyc@cyc src]$ objdump -t test_extern_c.o

test_extern_c.o:  file format elf32-i386

SYMBOL TABLE:

00000000 l   df *ABS* 00000000test_extern_c.c

00000000 l   d .text 00000000

00000000 l   d .data 00000000

00000000 l   d .bss   00000000

00000000 l   d .comment    00000000

00000000 g   F .text 0000000bThisIsTest

[cyc@cyc src]$ g++ -c main.cpp     

[cyc@cyc src]$ objdump -t main.o     

main.o:   file format elf32-i386

MYMBOL TABLE:

00000000 l   df *ABS* 00000000main.cpp

00000000 l   d .text 00000000

00000000 l   d .data 00000000

00000000 l   d .bss   00000000

00000000 l   d .rodata    00000000

00000000 l   d.gnu.linkonce.t._ZN3FOO3barEii 00000000

00000000 l   d .eh_frame    00000000

00000000 l   d .comment    00000000

00000000 g   F .text 00000081 main

00000000       *UND*00000000 atoi

00000000       *UND*00000000 _Znwj

00000000       *UND*00000000 _ZdlPv

00000000 w   F.gnu.linkonce.t._ZN3FOO3barEii 00000027 _ZN3FOO3barEii

00000000       *UND*00000000 _Z10ThisIsTestii

00000000       *UND*00000000 printf

00000000      *UND* 00000000 __gxx_personality_v0

可以看到,采用gcc编译了test_extern_c.c之后,在其目标文件test_extern_c.o中的有一个ThisIsTest符号,这个符号就是源文件中定义的ThisIsTest()函数了。而在采用g++编译了main.cpp之后,在其目标文件main.o中有一个_Z10ThisIsTestii符号,这个就是经过g++编译器“粉碎”过后的函数名。其最后的两个字符i就表示第一参数和第二参数都是整型。而为什么要加一个前缀_Z10我并不清楚,但这里并不影响我们的讨论,因此不去管它。显然,这就是原因的所在,其原理在本文开头已作了说明。

那么,为什么采用了extern "C"{}形式就不会有这个问题呢,我们就来看一下当test_extern_c.h采用extern"C" {}的形式时编译出来的目标文件中又有哪些符号:

[cyc@cyc src]$ gcc -c test_extern_c.c

[cyc@cyc src]$ objdump -t test_extern_c.o

test_extern_c.o:  file format elf32-i386

SYMBOL TABLE:

00000000 l   df *ABS* 00000000test_extern_c.c

00000000 l   d .text 00000000

00000000 l   d .data 00000000

00000000 l   d .bss   00000000

00000000 l   d .comment    00000000

00000000 g   F .text 0000000bThisIsTest

[cyc@cyc src]$ g++ -c main.cpp

[cyc@cyc src]$ objdump -t main.o

main.o:   file format elf32-i386

SYMBOL TABLE:

00000000 l   df *ABS* 00000000main.cpp

00000000 l   d .text 00000000

00000000 l   d .data 00000000

00000000 l   d .bss   00000000

00000000 l   d .rodata    00000000

00000000 l   d.gnu.linkonce.t._ZN3FOO3barEii 00000000

00000000 l   d .eh_frame    00000000

00000000 l   d .comment    00000000

00000000 g   F .text 00000081 main

00000000       *UND*00000000 atoi

00000000       *UND*00000000 _Znwj

00000000       *UND*00000000 _ZdlPv

00000000 w   F.gnu.linkonce.t._ZN3FOO3barEii 00000027 _ZN3FOO3barEii

00000000       *UND*00000000 ThisIsTest

00000000       *UND*00000000 printf

00000000       *UND*00000000 __gxx_personality_v0

注意到这里和前面有什么不同没有,可以看到,在两个目标文件中,都有一个符号ThisIsTest,这个符号引用的就是ThisIsTest()函数了。显然,此时在两个目标文件中都存在同样的ThisIsTest符号,因此认为它们引用的实际上同一个函数,于是就将两个目标文件连接在一起,凡是出现程序代码段中有ThisIsTest符号的地方都用ThisIsTest()函数的实际地址代替。另外,还可以看到,仅仅被extern"C"{}包围起来的函数采用这样的目标符号形式,对于main.cpp中的FOO类的成员函数,在两种编译方式后的符号名都是经过“粉碎”了的。

因此,综合上面的分析,我们可以得出如下结论:采用extern"C" {}这种形式的声明,可以使得CPP与C之间的接口具有互通性,不会由于语言内部的机制导致连接目标文件的时候出现错误。需要说明的是,上面只是根据我的试验结果而得出的结论。由于对于CPP用得不是很多,了解得也很少,因此对其内部处理机制并不是很清楚,如果需要深入了解这个问题的细节请参考相关资料。

 

注意:

用g++编译cpp程序时,编译器会定义宏 __cplusplus ,可根据__cplusplus是否定义决定是否需要extern "C"。

 

总结:

上面讲的都是理论,和一些程序,那么实际使用时有以下集中情况:

1. 现在要写一个c语言的模块,供以后使用(以后的项目可能是c的也可能是c++的),源文件事先编译好,编译成.so或.o都无所谓。头文件中声明函数时要用条件编译包含起来,如下:

 

C代码   收藏代码
  1. #ifdef __cpluscplus  
  2. extern "C" {  
  3. #endif  
  4.   
  5. //some code  
  6.   
  7. #ifdef __cplusplus  
  8. }  
  9. #endif  

也就是把所有函数声明放在some code的位置。

2. 如果这个模块已经存在了,可能是公司里的前辈写的,反正就是已经存在了,模块的.h文件中没有extern "C"关键字,这个模块又不希望被改动的情况下,可以这样,在你的c++文件中,包含该模块的头文件时加上extern "C", 如下:

Cpp代码   收藏代码
  1. extern "C" {  
  2. #include "test_extern_c.h"  
  3. }  
extern "C" {
#include "test_extern_c.h"
}

 

       3.上面例子中,如果仅仅使用模块中的1个函数,而不需要include整个模块时,可以不include头文件,而单独声明该函数,像这样:

 

Cpp代码   收藏代码
  1. extern "C" {  
  2. int ThisIsTest(intint);  
  3. }  
extern "C" {
int ThisIsTest(int, int);
}

 

然后就可一使用模块中的这个ThisIsTest函数了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值