1、#ifdef _cplusplus/#endif _cplusplus
#ifdef/#endif、#ifndef/#endif用于条件编译。为什么需要#ifdef _cplusplus/#endif _cplusplus呢?
因为C语言中不支持extern "C"声明,如果你明白extern "C"的作用就知道在C中也没有必要这样做,
这就是条件编译的作用!在.c文件中包含了extern "C"时会出现编译时错误。
既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。
而对于一个大型项目来说,这些冗余可能导致错误,最重要的是有时候又需要这种冗余来保证各个模块的独立。
2、extern "C"
首先从字面上分析extern "C",它由两部分组成——extern关键字、"C"。从这两个方面来解读extern "C"的含义。
2.1、extern关键字
在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。首先来一个例子:
//file1.c:
int x=1;
int f(){do something here}
//file2.c:
extern int x;
int f();
void g(){x=f();}
在file2.c中g()使用的x和f()是定义在file1.c中的。extern关键字表明file2.c中x,仅仅是一个变量的声明,
其并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一种全局变量只能被定义一次,
否则会出现连接错误。但是可以声明多次,且声明必须保证类型一致。
回到extern关键字,extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,
其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数
和全局变量以关键字extern声明。
例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,
在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
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",
它不会改变语义,但是会改变它的编译和连接方式。
3、C和C++互相调用
3.1、C++的编译和连接
C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。
为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:
void print( int i ); void print( char c); void print( float f ); void print (char *s);
编译为:
_print_int,_print_char ,print_float,print_string;
这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用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关键字那节已经介绍)。它的代码如下:
#ifndef C_HEADER
#define C_HEADER
extern void print(int i);
#endif C_HEADER
现在C++的代码文件C++.cpp中引用C中的print(int i)函数:
extern "C"{
#include "cHeader.h"
}
3.4、C中调用C++的代码
现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:
#ifndef CPP_HEADER
#define CPP_HEADER
extern "C" void print(int i);
#endif CPP_HEADER
在C的代码文件c.c中调用print函数:
extern void print(int i);
int main(int argc,char** argv) {
print(3);
return 0;
}
注意在C的代码文件中直接#include "cppHeader.h"头文件,编译出错。而且如果不加extern void print(int i)编译也会出错。
4. 从C代码中访问C++的类
能否声明一个类似与C++类的Struct,从而调用其成员函数,达到C代码访问C++类的目的呢?答案是可以的,但是,为了保持可移植性
,必须要加入一个兼容的措施。修改C++类时,也要考虑到调用它的C代码。加入有一个C++类如下:
class M {
public:
virtual int foo(int);
// ...
private:
int i, j;
};
在C代码中无法声明Class M,最好的方式是采用指针。C++代码中声明如下:
extern "C" int call_M_foo(M* m, int i) { return m->foo(i); }
在C代码中,可以这样调用:
struct M; /* you can supply only an incomplete declaration */
int call_M_foo(struct M*, int); /* declare the wrapper function */
int f(struct M* p, int j) /* now you can call M::foo */
{ return call_M_foo(p, j); }
5. 混合IOstream和C标准I/O
C++程序中,可以通过C标准头文件使用C标准I/O,因为C标准I/O是C++的一部分。
程序中混合使用IOstream和标准I/O与程序是否含有C代码没有必然联系。
C++标准说可以在同一个目标stream上混合C标准I/O和IOstream流,例如标注输入流、标准输出流,这一点不同的C++编译器实现
却不尽相同,有的系统要求用户在进行I/O操作前显式地调用sync_with_stdio()。其它还有程序调用性能方面的考虑。
6. 如何使用函数指针
必须确定一个函数指针究竟是指向C函数还是C++函数。因为C和C++函数采用不同的调用约定。如果不明确指针究竟是C函数还是C++函数,
编译器就不知道应该生成哪种调用代码。如下:
typedef int (*pfun)(int); // line 1
extern "C" void foo(pfun); // line 2
extern "C" int g(int) // line 3
...
foo( g ); // Error! // line 5
第一行声明一个C++函数指针(因为没有link specifier);
第二行声明foo是一个C函数,但是它接受一个C++函数指针;
第三行声明g是一个C函数;
第五行出现类型不匹配;
解决这个问题可以如下:
extern "C" {
typedef int (*pfun)(int);
void foo(pfun);
int g(int);
}
foo( g ); // now OK
当把linkage specification作用于函数参数或返回值类型时,函数指针还有一个难以掌握的误区。
当在函数参数声明中嵌入一个函数指针的声明时,作用于函数声明的linkage specification 也会
作用到这个函数指针的声明中。如果用typedef声明的函数指针,那么这个声明可能会失去效果,如下:
typedef int (*pfn)(int);
extern "C" void foo(pfn p) { ... } // definition
extern "C" void foo( int (*)(int) ); // declaration
假定前两行出现在源程序中。
第三行出现在头文件中,因为不想输出一个私有定义的typedef。尽管这样做的目的是为了使函数声明和定义吻合,但结果却是相反的。
foo的定义是接受一个C++的函数的指针,而foo的声明却是接受一个C函数指针,这样就构成两个同名函数的重载。为了避免这种情况,
应该使typedef紧靠函数声明。例如,如果想声明foo接受一个C函数指针,可以这样定义:
extern "C" {
typedef int (*pfn)(int);
void foo(pfn p) { ... }
};
7. 处理C++异常
C函数调用C++函数时,如果C++函数抛出异常,应该怎样解决呢?可以在C程序使用用long_jmp处理,
只要确信long_jmp的跳转范围,或者直接把C++函数编译成不抛出异常的形式。
8. 程序的连接
过去大部分C++编译器要求把main()编译到程序中,目前这个需求已经不太普遍。如果还需要,可以通过更改C程序的main函数名,
在C++通过wrapper的方式调用。例如,把C程序的main函数改为
C_main,这样写C++程序:
extern "C" int C_main(int, char**); // not needed for Sun C++
int main(int argc, char** argv)
{
return C_main(argc, argv);
}