C和C++之间的不兼容

之前提到过数次C和C++并不是一个语言,就算是C++中从C继承来的那部分也和ISO C有很大区别,以后我会逐渐整理一些它们之间不兼容的特性到这里来。

函数声明/定义

在C语言中,如果函数声明(包含定义)为:

1
2
3
4
5
6
7
8
9
// 代表接收不定参数的实参
int func(){
print("func()\n");
return 0;
}

int main(void){
func(1,2,3,4);// call func();
}

等同于C++中的:

1
2
3
4
5
6
int func(...){
cout<<"func"<<endl;
}
int main(void){
func(1,2,3,4);// call func();
}

在C语言中如果指定函数不接收任何参数,需要显式指定参数列表为void:

1
2
3
int func(void);
// 不等价
int func();

而C++中。参数列表为空和void均代表不接收任何参数。

1
2
3
int func();
// 等价
int func(void);

sizeof操作

在C语言中由于存在VLA(varible length array),所以在C语言中sizeof并不是完全的编译期行为:

1
2
3
4
void func(unsigned int n){
int iarr[n];
printf("%llu\n",sizeof(iarr));
}

在C++中是没有这个特性的,但是有一些编译器扩展是支持的,比如Using the GNU Compiler Collection(GCC)
如果你的C++编译器编译的过,那么你是编译器扩展的受害者(逃

enum

在C语言中enum等同于int类型,但是在C++中enum是一个不同于其基础类型的类型。

[ISO/IEC 9899:1999]The identifiers in an enumerator list are declared as constants that have type int and may appear wherever such are permitted.

在C语言中可以使用整型常量对枚举赋初值,但是在C++中就是错误:

1
2
3
enum color { red, blue, green };
enum color c = 1; // // valid C, invalid C++
// error: cannot initialize a variable of type 'enum color' with an rvalue of type 'int'

另外,在C++中,因为枚举对象和该枚举类型的基础类型是不同的类型,所以对两者分别sizeof结果不一定相等。

1
2
3
4
enum e { A };
sizeof(A) == sizeof(int) // in C
sizeof(A) == sizeof(e) // in C ++
/* and sizeof(int) is not necessarily equal to sizeof(e) */

void*不能隐式转换为其他类型的指针

1
2
int *x=malloc(4); // valid C, invalid C++
free(x);

C++中不能隐式转换void*为其他类型的指针,C语言中可以。

ISO C will accept this usage of pointer to void being assigned to a pointer to object type. C ++ will not.

类型定义在参数/返回类型

在C++中不允许将类型定义在函数的返回类型或参数类型,但是在C中是允许的。

1
2
void f( struct S { int a; } arg ) {} // valid C, invalid C ++
enum E { A, B, C } f() {} // valid C, invalid C ++

字符数组初始化

在C++中对char数组使用一个""初始化必须能够容纳该字符串字面值所有的的字符(包括末尾\0),如果一个字符数组不能够容纳初始化字符串字面值的所有元素,则是一个错误:

In C++ ,when initializing an array of character with a string, the number of characters in the string (including the terminating ’\0’) must not exceed the number of elements in the array.

1
2
char x[4]="abcd"; // valid C, invalid C++
// [clang++ 3.9]error: initializer-string for char array is too long

但是在C中会只初始化能够容纳的部分。

In C, an array can be initialized with a string even if the array is not large enough to contain the string-terminating ’\0’.

字符常量的基础类型

在C语言中字符常量是一个int类型,但是在C++中是一个char类型。

[ISO/IEC 14882:2014]An ordinary character literal that contains a single c-char representable in the execution character set has type char, with value equal to the numerical value of the encoding of the c-char in the execution character set.

[ISO/IEC 9899:1999]An integer character constant has type int. The value of an integer character constant containing a single character that maps to a single-byte execution character is the numerical value of the representation of the mapped character interpreted as an integer.

所以,下面的代码会在C和C++中具有两种行为(使用C编译器为no,使用C++编译器会为yes):

1
2
3
4
5
if(sizeof('a')==sizeof(char)){
printf("yes\n");
}else{
printf("no\n");
}

即:

1
2
// valid in C, invalid in C++
sizeof('a') == sizeof(int)

为什么呢?
试想一下,如果在C++里int和char是同一个类型,那么这个会输出什么?

1
std::cout<<'x';

在函数重载中也就无法区分整型和字符型,所以这也是C++中把int和char作为两个类型。

string literal type

在C语言中,字符串字面值是一个char*类型,即:

1
char *p = "abc";

[ISO/IEC 9899:1999]defines p with type "pointer to char" and initializes it to point to an object with type "array of char" with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.

在C++中字符串字面值是一个const char*:

A narrow string literal has type "array of n const char", where n is the size of the string as defined below, and has static storage duration.

上述规则同样适用于char16_t string literal/char32_t string literal.

字符串字面值的类型C/C++中的不同会使以下代码具有两种行为:

1
2
// valid in C, invalid in C++
char *str="hello";

如果你的C++编译器编译的过,说明你是编译器扩展(不标准)的受害者(逃。

C结构和C++类的成员要求

C语言的struct可以包含一个不完全类型(incomplete types)(但不是随意包含):

[ISO/IEC 9899:1999]A structure or union shall not contain a member with incomplete or function type (hence,A structure or union shall not contain a member with incomplete or function type (hence,a structure shall not contain an instance of itself, but may contain a pointer to an instance of itself), except that the last member of a structure with more than one named member may have incomplete array type; such a structure (and any union containing, possibly recursively, a member that is such a structure) shall not be a member of a structure or an element of an array.

意思就是:具有多个成员的结构的最后一个成员可以是一个不完全数组。

通过这个特性可以使用Array of length zero来动态扩充一个struct

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct A{
char tag;
char alz[];
}Astruct;

int main(void){

Astruct *x=(struct A*)malloc(sizeof(Astruct)+sizeof(char)*10);
free(x);

return 0;
}

虽然也可以使用一个指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct A{
char tag;
char *cp;
}Astruct;

int main(void){

Astruct *x=(Astruct*)malloc(sizeof(Astruct));
x->cp=(char*)malloc(sizeof(char*)*10);

free(x->cp);
free(x);

return 0;
}

但是使用Array of length zero可以节省一个sizeof(char*)的开销,而且第一种方法创建的是连续分配的内存,可以减轻内存碎片化的问题。

但是,注意上面那段引用的最后一句,这意味着在C++中这个特性是不可能存在的(因为C++存在继承(inheritance)),派生类中存在基类中的成员,但是C++标准并未规定类的内存布局怎样,所以不能保证继承来的基类的最后一个成员也是派生类的最后一个成员,在C++中也同样不能保证类的最后一个成员在内存布局中位于实例的尾部(不能确定虚函数表的位置,依赖实现)。

即,在C++中,class的成员(非static)是一个完全类型(complete types):

[ISO/IEC 14882:2014]Non-static (9.4) data members shall not have incomplete types. In particular, a class C shall not contain a non-static member of class C, but it can contain a pointer or reference to an object of class C.

所以就不能使用上面C中使用的Array of length zero的技巧(这个还是要依赖于实现),但是这个是未定义的行为(undefine behavior)。

虽然C++中也可以使用{}来对未知大小的数组进行初始化,但是不能使用空的初始化列表为一个未知大小的数组进行初始化。

[ISO/IEC 14882:2014]An array of unknown size initialized with a brace-enclosed initializer-list containing n initializer-clauses, where n shall be greater than zero, is defined as having n elements (8.3.4).

1
int x[] = { 1, 3, 5 };

declares and initializes x as a one-dimensional array that has three elements since no size was specified and there are three initializers. — end example ] An empty initializer list {} shall not be used as the initializer-clause for an array of unknown bound.

The syntax provides for empty initializer-lists, but nonetheless C++ does not have zero length arrays.

所以在C++中,这样的代码是UB行为:

1
2
int x[]={};
x[0]=123; // no compile error

以上代码在MinGW-W64 G++ 6.2.0以及Clang++ 3.9 x86_64-w64-windows-gnu编译通过,在VS2015中编译报错:

error C2466: 不能分配常量大小为 0 的数组

递归主函数

C语言中允许递归主函数(没有不允许):

Recursive function calls shall be permitted, both directly and indirectly through any chain of other functions.

但是C++中明确规定了不能递归主函数:

Recursive calls are permitted, except to the function named main (3.6.1).

如果你的C++编译器能够编译过下面的代码,那么你也是编译器扩展的受害者...

1
2
3
4
5
6
7
8
9
10
11
12
static int index=0;
int main(void)
{
printf("%d\n",index);
if(index==10){
return index;
}else{
++index;
main();
}
return 0;
}

const对象的文件作用域链接

在C和C++中,同样位于文件作用域(file scope)const对象的链接是不同的。
先来看一下文件作用域(file scope)的定义:

[ISO/IEC 9899:1999]If the declarator or type specifier that declares the identifier appears outside of any block or list of parameters, the identifier has file scope, which terminates at the end of the translation unit.

well,假设我们具有文件作用域的以下对象:

1
const int cival=123;

则上面代码使用C编译器和C++编译器具有不同的链接:

  • C语言中具有外部链接
  • C++中具有内部链接

来测试一下(手动通过编译器实现链接的方式请看C/C++编译和链接模型分析):

1
2
3
4
// test.c
const int cival=123;

int main(void){}

使用C语言编译器编译出目标文件:

1
2
3
4
5
$ gcc -c test.c -o test.o
$ nm test.o
-------- U __main
00000000 R cival
00000000 T main

而使用C++编译器编译:

1
2
3
4
$ g++ -c test.c -o test.o
$ nm test.o
-------- U __main
00000000 T main

可以看到目标文件中的cival符号不见了。

至于原因,C++标准中给了充足的描述:

Change: A name of file scope that is explicitly declared const, and not explicitly declared extern, has internal linkage, while in C it would have external linkage.
Rationale: Because const objects can be used as compile-time values in C ++ , this feature urges programmers to provide explicit initializer values for each const. This feature allows the user to put constobjects in header files that are included in many compilation units.

总的来说,在C中,全局const默认具有外部链接;在C++中则不是,必须进行初始化,除非显式声明了extern

函数定义的参数列表

在C语言中允许以下函数定义方式:

1
void f(a,b,c)int a;char b;double c;{/*...*/}

在C++中不允许这么写,必须改为:

1
void f(int a,char b,double c;){/*...*/}

全局变量定义

在C中,无需extern说明符即可在单一翻译单元中多次声明一个全局数据对象。只要至多只有一个提供了初始化器,次对象就被认为只定义了一次。

1
2
3
4
// 在C中可以编译通过
int i;
int i=123;
int main(void){}

而在C++中则是重定义错误:

1
2
3
int i;
int i=123; // error: redefinition of 'i'
int main(void){}

嵌套结构作用域

在C中,嵌套的结构的名字与外层结构的名字位于相同的作用域。

1
2
3
4
5
6
7
8
9
10
11
12
struct A{
int a;
struct B{
int b;
};
};

int main(void)
{
struct A aobj;
struct B bobj;
}

而在C++中因为有类作用域这个概念,则必须指定外层名字:

1
2
3
4
5
int main()
{
struct A aobj;
struct A::B bobj;
}
全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:C和C++之间的不兼容
文章作者:ZhaLiPeng
发布时间:2017年02月27日 15时25分
更新时间:2017年05月23日 10时46分
本文字数:本文一共有2.8k字
原始链接:https://imzlp.me/posts/14446/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!