前两天想写一篇关于extern “C”的文章,就到wikipedia上面去搜了一下,wikipedia把我重定向到了这篇
http://en.wikipedia.org/wiki/Compatibility_of_C_and_C%2B%2B
文章上,仔细读了一下,发现文章里不但说了extern “C”的内容,还将C和C++的兼容性的问题全都说了个遍,现在将原文翻译并加入自己的理解,和大家一起分享。
C和C++是关系十分紧密的两种编程语言,C++是从C发展而来的,所以,C++被设计得在编码和链接上都和C兼容。也因此,针对这两种语言的开发工具,比如IDE和编译器,经常被整合成一个工具,而程序员在使用这些工具的时候,要自己向这些工具指明自己的源文件时C程序还是C++程序。但是,因为C和C++在上语义上的一些微小差别(就是对于同样的语法,C和C++是不同的对待它们的),大部分严格的C程序(就是说在写这些C程序的时候完全没有考虑和C++的兼容问题)在没有修改之前是不能够被当成C++程序编译通过的,也就是说,C++不是C语言的超集(C++在设计的时候是考虑了C的兼容性问题的,但是并不是说,C的所有特性在C++中都被支持了)。
同样的,C++引入了很多C语言中所没有的特性,在实际中,基本上所有的C++代码都不是合格的C代码(把这些代码个C编译器是不能编译通过的)。这篇文章主要关注是哪些C和C++的不同点导致规整的C代码不是规整C++代码,以及在C和C++中都是规整的代码为什么会有不用的表现。
Bjarne Stroustrup, C++的创始人,他建议C和C++的不兼容特性应该越少越好,这样这两种语言的互操作性就会最大。其他的人与他有不同的看法,他们认为C和C++是两种不同的编程语言,两者之间的兼容性是有用的但并不是至关重要的,这个阵营的人认为,为了减小C和C++的不兼容特性而付出的努力,不应该阻碍这两种语言自身独立的发展。C99的官方解释中肯定了在保持C和C++有相同的子集的情况下应当保证他们有不同的特性的原则,这样这两种语言就可以独立的发展,C99的标准委员会也对C++要成为一种庞大的,有雄心的语言感到高兴。
在C99中增加的几个特性在C++中是不支持的,甚至和C++是冲突的,比如可变参数宏,指定的初始化,变长数组,原生的负数类型。C99中的long long int数据类型和restrict限定符在当前的C++标准中是没有的,但是大部分主流的编译器,比如GNU的编译器套件,微软的visual C++和intel的C++编译器都以扩展的形式增加了这些特性。long long 数据类型和变参模板,通过它们可以实现变参宏的一些特性,在新的C++标准,C++11,中也提出来了。另一方面,通过整合C++的一些特性,C99中已经减少了一些不兼容元素,比如说可以使用//注释以及混合使用声明和代码。
Table of Contents
在C中有效但在C++中无效的结构
一个经常遇到的不同点是在C中void*指针可以不经过强制类型转换就赋值给任意其他类型的指针,但是在C++中却不行;这种语法经常在C的malloc函数中出现,比如,下面的代码在C中是有效的,但是在C++中是无效的
void* ptr; int *i = ptr; /* Implicit conversion from void* to int* */
相似的
int *j = malloc(sizeof(int) * 5); /* Implicit conversion from void* to int* */
为了使这些代码在C++中能够编译通过,我们必须使用强制类型转换
void* ptr; int *i = (int *) ptr; int *j = (int *) malloc(sizeof(int) * 5);
另外一个C到C++的移植性问题是C++中有好几个新引入的关键字,这使得如果C代码中使用了他们作为标识符,那么这些代码对C++就是无效的。比如
struct template { int new; struct template* class; };
是有效的C代码,但是C++编译器却会报错,因为new,template,class是C++中保留的关键字。
C++编译器禁止goto或者switch语句跨过初始化语句使用,比如下面的C99代码
void fn(void) { goto flack; int i = 1; flack: ; }
还有许多其他的C语法在C++中是非法或者有不同表现的:
- 在C++中,逗号操作符的结果可以是一个左值(当使用赋值操作符时,可以被放在赋值符的左边),在C中却不可以。也就是说,下面的语法在C++中是可以的,但是在C中却是不可以的。
int a,b; (a,b)=1;
- C中不允许相同的typedef在同一个作用域中重复定义,而C++允许。
- C++的标识符在任何位置都不出现两个及以上的连续的下划线。C的标识符在开始处不允许两个及以上的下划线,但是在其他位置可以。
我使用GCC4.4.1实验发现上面两者都有出入,这应该是GCC的扩展。
- enum的元素的值在C中只能是int型的,但是在C++中,根据其值得范围不同,可能不是int型的。
- C++改变了一些C标准库的函数实现,大部分改变都是重载了原来C里面的实现。比如,strchr在C中只有char * strchr ( const char *, int )这个实现,但是在C++中,有如下的两个实现。
const char * strchr ( const char * str, int character ); char * strchr ( char * str, int character );
- 在C和C++中,我们都可以定义嵌套的结构体,但是其作用域在C和C++中是不同的(在C++中,一个嵌套的结构体只能在外面的结构体的作用域/命名空间中定义)。
- 无原型的(K&R型)的函数声明在C++中是不允许的,在1990年后,在C语言中这个特性也废除了。相似的,隐函数声明(函数在声明之前使用)在C++中也是不允许的,在C99中,这个特性也被废除了。
- C允许struct,union,enum在函数原型中声明,但是C++不允许。
- 在C++中,struct,union,enum的声明隐含了一个同名的typedef,但是在C中却不是。
struct aa { int a; }; // typedef struct aa aa; int main() { aa bb; }
上面的代码在C++中是合法的,因为在struct后,编译器有一个隐式的typedef,而C中没有,所以上面的代码在C中不行。
- 在C中,如果一个函数原型没有输入参数,比如int foo(),那么这个函数的输入参数是没有指定的。因此,使用一个及以上的实参来调用这个函数都是合法的,比如,foo(4,”hello,world”).在C++中,相同的声明表示这个函数没有输入参数,如果调用这个函数的时候使用了实参的话,就是错误的。在C中,正确的声明一个函数没有输入参数的方法是使用’void’,即int foo(void);
- C++对待指针赋值时扔掉const限定符比C更加严格,比如,将const int *赋值给int *在C++中是非法的,而在C中却是可以的,虽然大部分的编译器会扔出一个warning。
解释一下为什么在C++中会有这个规定,如果一个指针是const的,那么这个指针指向的内容通过这个指针就是只读的,而一个指针不是const的,那么这个指针所指向的内容通过这个指针就是可读可写的;如果上述的赋值是合法的,那么本来通过一个指针我们并不可以写入那块内存,但是通过将这个指针赋值给另一个指针,那块内存就变成可写的了,这一点肯定是不安全的。
在C和C++中表现不同的结构
有一些在C和C++中都是有效的语法结构,但是在两种语言中将有不同的效果。
比如,字符常量,比如’a’,在C中是int型的,但是在C++中是char型的,也就是说,下面的代码在C中的结果是4,而在C++中的结果是1
#include <stdio.h> int main() { printf("%d\n",sizeof 'a'); }
作为这种类型差别的结果,在C中,不管char类型是有符号还是无符号的,’a’总是一个有符号表达式,而在C++中,这个就跟具体的编译器实现有关了。
C++隐式的将const 的全局变量视为文件内的全局变量,除非其被显式的声明为extern,而在C中extern是默认的。相反的,inline 函数在C中是文件作用域中的,而在C++中却可以被其他文件链接。
在前面讲的其他几个不同点可以自己编写代码并使用两种不同的编译器来编译运行来验证。比如,下面的例子在C和C++中将返回不同的值:
extern int T; int size(void) { struct T { int i; int j; }; return sizeof(T); /* C: return sizeof(int) * C++: return sizeof(struct T) */ }
这是因为C要求在结构体前面必须要有struct关键字(所以sizeof(T)指示的是外面的变量),但是C++允许结构体前面的sturct被忽略(所以sizeof(T)指示的是隐形的typedef)。需要注意的是当把extern int T;放到函数里面的时候,结果会变得不一样,因为现在在函数里出现的相同标识符抑制了C++中的隐形typedef起作用,所以这样它们的输出会是相同的(我只能说这个特性真的是太奇怪了)。需要注意上面会产生这样的歧义是因为我们使用sizeof操作符的时候使用了括号。当我们使用sizeof T是,我们会要求T是一个expression而不是一个数据类型(使用有括号的时候,两者都是可以的),那么在C++中上面的例子是不能编译通过的。
在C99和C++中都有boolean数据类型,这种数据类型只有true和false这两种取值,但是在C和C++中它们表现是不一样的。在C++中,bool是一个内建的数据类型并且是保留的关键字。在C99中,引入了一个新的关键字_Bool来表示boolean类型。在大部分情况下,其表现得像一个unsigned int,但是从其他整数类型和指针转换过来的值总是被限制成0和1.而对于其他数据类型,当你需要把其转为整形时,当且仅当表达式的值是0时其值才是0,其他情况下都是1.在stdbool.h中提供了bool,true,false几个宏。
链接C和C++代码
虽然C和C++的源代码有很大程度的兼容性,但是当混合使用C和C++代码的时候,它们各自的编译器生成的OBJ文件会有一些表现各自特性的重要不同之处。尤其是:
- 根据使用的编译器和平台的不同,这种语言的调用规定可能是不同的。
- C编译器不会像C++编译器那样将一个符号的名字弄乱。
由于这些原因,当C++代码要调用C的foo()函数的时候,C++代码必须用extern “C”进行foo()的原型声明。相同的,如果C代码要调用C++的函数bar(),那么C++代码中bar()必须被声明成extern “C”.
一个通用的做法是在头文件中使用extern“C”来保证C和C++代码的兼容性:
/* Header file foo.h */ #ifdef __cplusplus /* If this is a C++ compiler, use C linkage */ extern "C" { #endif /* These functions get C linkage */ void foo(); struct bar { /* ... */ }; #ifdef __cplusplus /* If this is a C++ compiler, end C linkage */ } #endif
注意上面的代码中使用了条件编译,那么有人就会问了,那么__cplusplus是在哪儿define的呢,至少在我写C++代码的时候,我是没有定义这个东西的。实际上,这个是C++编译器自己定义了的,因为调用C++编译器的时候有一对参数可以用来实现define和undefine的功能。上面的代码如果使用C编译,那么__cplusplus就没有被定义,代码和一般的C代码没有任何区别,如果使用C++编译,那么编译器就会看到__cplusplus的定义而将后面的函数和结构体用C的链接和调用准则而不是C++的来处理。
C和C++链接和调用准则的不同也会对使用函数指针的代码产生微妙的影响。如果一个函数指针声明为extren “C”而指向了一个没有被声明为extern “C”的C++函数,一些编译器会编译出不可工作的代码 。比如下面的代码:
void my_function(); extern "C" void foo(void (*fn_ptr)(void)); void bar() { foo(my_function); }
在Sun Microsystems 的C++编译器上将会产生下面的Warning:
$ CC -c test.cc "test.cc", line 6: Warning (Anachronism): Formal argument fn_ptr of type extern "C" void(*)() in call to foo(extern "C" void(*)()) is being passed void(*)().
这是因为my_function()没有被声明为使用C的链接和调用准则,却被传给了一个C函数foo().