如下图中,在file2.cpp中f()使用的x和f()是定义在file1.h中的。extern关键字表明file2.cpp中x,仅仅是一个变量的 声明,其并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。但是可以声明多次,且声明 必须保证类型一致。注意,在file2.cpp中需包含file1.h。
经本人测试,上面的程序可以省略掉“extern int x”和“int f()”,而只包含"#include "file1.h""。但是反过来不行,也就是:不可以省略掉"#include "file1.h"",而仅仅只包含“extern int x”和“int f()”,这样file2.cpp会找不到变量x和函数f()。
该图file2.cpp中只包含了#include "file1.h"
回到extern关键字,extern是C/C++语言中表明函数和全局变量作用 范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函 数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块 A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
2.2、"C"
典型的,一个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",它不会改变语义,但是会改变它的编译和连接方式。如果你有很多语言要加上extern "C",你可以将它们放到extern "C"{ }中。
2.3、小结extern "C"
通过上面两节的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)
3、C和C++互相调用
我们既然知道extern "C"是实现的类C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白C和C++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。
3.1、C++的编译和连接
C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:
编译为:
这样的函数名,来唯一标识每个函数,这就是C++中的名字修饰,也有很多优势,可以确保类型安全连接。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找 _print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。
3.2、C的编译和连接
C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的 print(3)时,它会去找_print_int(3)。因此extern "C"的作用就体现出来了。
3.3、C++中调用C的代码
假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字(原因在extern关键字那节已经介绍)。它的代码如下:
4 | extern void print( int i); |
相对应的实现文件为cHeader.c的代码为:
5 | printf ( "cHeader %d\n",i); |
现在C++的代码文件C++.cpp中引用C中的print(int i)函数:
5 | int main( int argc, char** argv) |
linux执行上述文件的命令为:
首先执行gcc -c cHeader.c,会产生cHeader.o;
然后执行g++ -o C++ C++.cpp cHeader.o
执行程序输出:
图4 输出结果
如果把“extern "C"”删除,会出现无法解析的外部符号的错误,可见其作用。
在C++.cpp文件中可以不用包含函数声明的文件,即“extern
"C"
{
#include"cHeader.h"}”,而直接改用extern "C" void print(int i)的形式。那C++.cpp是如何找到C中的print函数,并调用的呢?
那是因为我们首先通过gcc -c cHeader.c 这个命令,产生了一个cHeader.c的目标文件。然后我们通过执行g++ -o C++ C++.cpp cHeader.o这个命令,该命令中指明了要链接的目标文件:cHeader.o,所以C++.cpp中只需要说明哪些函数(比如该题中的void print(int i))需要以C的形式调用,然后去其目标文件中找该函数即可。注:“.o文件”为目标文件,类似于windows下的obj文件。
下图给出了直接以函数的形式包含的形式:
3.4、C中调用C++的代码
现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:
4 | extern "C" void print( int i); |
相应的实现文件cppHeader.cpp文件中代码如下:
7 | cout<< "cppHeader "<<i<<endl; |
在C的代码文件c.c中调用print函数:
1 | extern void print( int i); |
2 | int main( int argc, char** argv) |
注意在C的代码文件中直接#include "cppHeader.h"头文件,编译出错,原因为:当以头文件的形式包含cppHeader.h时,会将cppHeader.h的内容展开,而cppHeader.h中包含“extern "C"”关键字,而c语言中没有这种用法,所以会出现编译错误。而且如果不加extern int print(int i)编译也会出错。
下面给出linux下的执行那个过程,程序如下图所示:
(1)首先执行命令:g++ cppHeader.cpp -fpic -shared -g -o cppHeader.so
该命令是将cppHeader.cpp编译成动态连接库,其中编译参数的解释如下:
-shared 该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件
-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
-g:为调试
(2)然后再执行命令:gcc c.c cppHeader.so -o cmain
该命令是编译c.c文件,同时链接cppHeader.so文件,然后产生cmain的可执行文件。
(3)最后执行命令:./cmain 来执行该可执行程序
4、C和C++混合调用特别之处函数指针
当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一中语言定义的函数。如果C和C++共享同一中编译和连接、函 数调用机制,这样做是可以的。然而,这样的通用机制,通常不然假定它存在,因此我们必须小心地确保函数以期望的方式调用。
而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:
01 | typedef int (*FT) ( const void * , const void *);//style of C++ |
04 | typedef int (*CFT) ( const void *, const void *);//style of C |
05 | void qsort ( void * p, size_t n, size_t sz,CFT cmp);//style of C |
08 | void isort( void * p, size_t n, size_t sz,FT cmp);//style of C++ |
09 | void xsort( void * p, size_t n, size_t sz,CFT cmp);//style of C |
12 | extern "C" void ysort( void * p, size_t n, size_t sz,FT cmp); |
14 | int compare( const void *, const void *);//style of C++ |
15 | extern "C" ccomp( const void *, const void *);//style of C |
19 | //error,as qsort is style of C |
20 | //but compare is style of C++ |
21 | qsort(v,sz,1,&compare); |
22 | qsort (v,sz,1,&ccomp);//ok |
24 | isort(v,sz,1,&compare);//ok |
25 | //error,as isort is style of C++ |
26 | //but ccomp is style of C |
注意:typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,这种函数指针指向的函数有这样的特征:返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。
最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:
1 | typedef void (*HANDLER)( int); |
2 | HANDLER signal ( int ,HANDLER); |
上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。 这样避免了要这样定义signal函数:
1 | void (* signal ( int , void (*)( int ) ))( int) |
比较之后可以明显的体会到typedef的好处。