extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。
工程中经常会遇到C和C++混合编程的情况,有时是C++的工程中需要使用C语言编写的库,有时是C的工程需要使用C++;但如果不加任何修饰,直接调用的话,就会遇到链接问题,提示找不到函数名称。
这是由于C++在编译时候,会将函数名做一些修饰,在函数名前加上函数名的长度,在函数名后面加上参数类型。链接的时候也使用相同的策略。C++这么做是由于C++语言支持函数重载,在函数名相同参数不同的几个函数也可以共存。C语言不支持重载,所以编译和链接时对函数名不会加修饰。加extern ”C“就是为了让编译器以C语言的函数名处理方式来编译。
使用场景主要有两种:
(1)C++的模块调用C语言写的库
C语言test.h, test.c创建的动态库libtest.so
#### test.h文件如下
#include "stdio.h"
void test_add(int a, int b);
在这里插入代码片 test.c文件如下
#include "test.h"
void test_add(int a, int b)
{
int num = a + b;
printf("%d + %d = %d\n", a, b, num);
}
#### 编译命令
gcc test.c -fPIC -shared -o libtest.so
C++的文件caller_cplusplus.cpp调用
1 #ifdef __cplusplus
2 extern "C"
3 {
4 #endif
5 #ifdef __cplusplus
6 #include "test.h"
7 }
8 #endif
9 int main()
10 {
11 test_add(3, 4);
12 return 0;
13 }
#### 编译命令
g++ -o callerCpp caller_cplusplus.cpp -L. -ltest
(2)C++头文件声明接口函数
在C中引用C++语言中的函数和变量时,C++的头文件需添加extern “C”,但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c*/
/* 这样会编译出错:#include “cExample.h” */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
上面介绍的是extern "C"的含义和使用方法,主要是跨C和C++的库调用时需要注意,那么除了extern "C"之外还有哪些是在动态库调用过程中经常遇到的坑呢?接下来的部分是个人总结的几个需要注意的点。
VS是比较常用的IDE,在公司中也经常要调用其他事业部提供的动态库;当不同组之间使用的VS版本不同时,如果接口中使用了不通用的特性,就会导致动态库无法调用或者调用过程中编译失败
比如说A.dll库是由VS2013开发的,A.dll的接口中包含了C++11的独特的数据结构或者新特性。VS2008开发的B.exe想要调用A.dll就会出现问题。另外不同版本的VS支持的MFC库也不同,如果调用的库在编译时依赖了MFC,那么自己的程序也要和调用库使用相同版本的VS
这两个是函数名修饰规则定义,也定义了参数的压栈方式和清理栈的规则;这里注意到和extern "C"不同,extern "C"是告知编译器如何解析函数名,主要用途是解决跨语言调用(C和C++)而__cdecl/__stdcall则是规定了函数名应该如何修饰,以及参数压栈方式,栈由谁清理
函数名修饰规则:
__stdcall :约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number(number即参数栈长度)
__cdecl :约定仅在输出函数名前加上一个下划线前缀,格式为_functionname
参数栈的管理:
两种方式定义的参数传递方式都是从右至左压,区别在于清理栈的角色不同。
__stdcall:是被调用函数清理(即函数自己清理)
__cdecl:是调用者清理;
所以__stdcall修饰函数名时加上参数栈的大小可以让函数自己进行栈的清理;对于可变参数的函数,无法在函数定义时确认参数长度,只能让调用者去清理参数栈,所以对于可变参数的函数应该用__cdecl修饰(可变参数的函数exp:printf)
一个原则,调用者和库的CRT链接选项要相同,尽量都使用/MD选项 (多线程动态链接)。
遇到过一个坑,调用的库和自己程序的CRT选项不同,库函数中返回了一个字符串指针,内存在库中申请,而释放时需要在程序中释放;由于程序和库使用的CRT不同,导致内存操作的堆不同,库函数申请的堆空间无法在自己程序中释放掉,出现了崩溃。