C与C++的兼容性

C语言C++的关系相當密切,但是也存在许多显著的差异。C++标准起源于早期C标准, 并被设计为与当时的C语言在源代码编写和链接方面很大程度上兼容。[1][2] 因此,两种语言的开发工具(例如IDE编译器)通常被集成到单个产品中,程序员可以自己选择编写的是C还是C++,开发工具通常会根据程序员的选择使用不同的编译器链接器或不同的

但是,C并不是C++的子集[3], 一般的 C 代码不经修改很难被一些严格符合C++标准的C++编译器成功编译;同样,C++ 引入了许多 C 中没有的特性,所以,几乎所有用 C++ 编写的代码都无法被 C 编译器成功编译。在这篇文章中,我们主要讨论的是它们在公共部分的差异,比如在C语言中合法的代码到了C++中成为了不合法的代码,或一段代码在C和C++中表现出不同的行为。

C++的创始人Bjarne Stroustrup建议[4] C和C++应该尽可能减小差异,以最大限度地提高这两种语言的兼容性;而另一些人则认为C和C++毕竟是两种不同的语言——虽然C++起源于C——因此它们之间的兼容性并不是那么重要。 而ANSI是这样看的:「我们赞同保持C与C++的最大公共子集原则」,同时「保持它们的差别,使这两种语言继续独立发展」,「……委员会希望C++成为重要的和强有力的语言。」[5][6]

截止到C++20C2x,C++ 还是不支持部分C语言特性,如变长数组,原生复数支持和restrict类型修饰符。另一方面,与C89相比,C99通过合并C++功能(例如//注释,允许声明出现在代码中而不只是在函数头)减少了一些其他不兼容性。

在C中允许而在C++中不允许存在的代码语句

C++较C语言有更严格的类型转换和初始化规则[1][7] , 因此一些在C语言里合法的语句在C++里是不合法的。ISO C++附录C.1中列出了这些区别。[8]

  • 一个常见的区别是在C语言中的指针类型更加弱类型。具体来说,C语言允许将void* 指针赋值给任何类型的指针而无需强制转换,而C++则不允许;这个习惯用法经常出现在使用malloc 管理内存的C代码中,[9]POSIX线程库的上下文传递以及其他涉及回调的框架。例如,下面这个例子在C语言中是可行的,但在C++中不允许:
    void *ptr;
    /*从void *到int *的隐式转换*/
    int *i = ptr;
    
    或类似的:
    int *j = malloc(5 * sizeof *j);     /*从void *到int *的隐式转换 */
    
    为了使代码在C和C++中同时可用, 必须使用强制类型转换, 如下所示(然而这样在两种语言中都会报警告[10][11]):
    void *ptr;
    int *i = (int *)ptr;
    int *j = (int *)malloc(5 * sizeof *j);
    
    C++中提供了其它方法来实现指针类型转换。C++不推荐继续使用老式类型转换。老式类型转换在形式上来说不是特别清晰,容易被忽略。[12]
    void *ptr;
    auto i = reinterpret_cast<int *>(ptr);
    auto j = new int[5];
    
  • C语言允许在安全的前提下隐式添加除constvolatile的关键字,而C++则不会。
  • C++为C语言的部分函数添加了一个参数带const重载,例如strchrC语言中的声明为char *strchr(char *),而在C++中则有一个重载函数为const char *strchr(const char *)
  • C++在枚举方面也更加严格。在C++中,int不能被隐式转换为枚举类型。因为在C++标准中并没有规定枚举的类型为int(在C语言标准中则规定了),并且大小可能与C++中int的大小不同。从C++11开始,C++允许程序员将自定义的整数类型赋值给枚举类型。
  • 在C++中,const变量必须被初始化,但在C语言中不必:
    const int var1;
    const int var2 = 1;
    
    第一行代码在C语言中是允许的(在某些编译器中可能会报告“Warning”),但在C++中会报错;第二行代码在C和C++中都是合法的。
  • C++不允许goto和标签之间出现初始化语句。如下示例所示,该段代码在C语言中是允许的,但在C++中不允许。
    void fn(void)
    {
        goto flack;
        int i = 1;
    flack:
        ;
    }
    
  • 虽然语法上有效,但如果跳过的堆栈帧包含具有非平凡(nontrivial)析构函数的对象(即指针),则longjmp()函数会在C++中被认为是未定义的操作。[13] C++可以自由定义此时的行为,以便能够调用析构函数。但是,这样会使longjump()的一些用法失效, 否则就可以通过在单独的调用堆栈之间进行longjmp来实现线程协程——在全局地址空间中从较低的调用堆栈跳转到较高的调用堆栈时,将析构函数用于较低调用堆栈中的每个对象。这个问题在C语言中不存在。
  • C语言中允许使用与用structenumunion类型名称相同的名称定义新结构体。
    enum BOOL {FALSE, TRUE};
    typedef int BOOL;//在C语言中允许,在C++中不允许
    
  • C语言中,由structenumunion创建的类型在声明变量时必须在自定义的类型前加structenumunion(取决于定义时的类型),而在C++中不必。因为自定义类型在创建时已隐含typedef(这也是上一点区别形成的原因)。
    struct foo{
            int bar;
    };
    /*
    在C++中,以上形式被隐式转换为:
    typedef struct foo{
            int bar;
    } foo;
    */
    struct foo foobar;//在C与C++中均可
    foo foobar2; //在C语言中不允许,在C++中允许
    
  • 在C++中不允许使用旧式K&R声明(如下所示),这种声明在C语言中仍然可用,但是标准已将这种声明方式列为「过时」(「过时」是在ISO C标准中定义的术语,它表示委员会可以考虑在未来的标准中将其删除)。
    int add(a,b)
    int a;
    int b;
    {
        return a+b;
    }
    
  • 在C99标准出现之前,C语言允许隐式函数声明(即省略函数声明),而C++不允许;但在C99标准出现及以后,C与C++均不允许使用这种形式。
  • C语言中,如果函数原型中没有参数(如int foo();,表明该函数的参数不确定(这是一种标准不推荐的用法);而在C++中相同的形式等同于int foo(void);。如果想要在C语言中表示没有参数,应该使用int foo(void);
  • 在C和C++中,都可以嵌套定义struct类型,如下所示:
    struct foo{
        struct bar{
            int x;
        };
        int y;
    }
    
    然而,在使用时,C和C++采用不同的方法,假设此时已有struct foo类型变量a, 且x = 1,y = 2;如要将x赋值给变量b,c:
    //以下是C语言方法,在C++中不允许:
    int b = a.x;
    
    //以下是C++特有方法,在C中不允许:
    int c = a::x
    
  • C99和C11添加了一些未包含在标准C++中的附加功能,例如复数变长数组(值得注意的是,在C99中列为强制要求的VLA(变长数组)和复数在C11中被列为了可选项,这是由于部分特殊平台的C编译器无法有效率地实现或无法实现这两个功能导致的。目前基本所有平台都有对应的可使用VLA的编译器),灵活数组类型(Flexible array member,也叫伸缩型数组成员),restrict关键字,数组参数限定词,复合文字英语C_syntax#Compound_literals,指定初始化项目,可变参数宏,附加数学库,预定义的标识符(如__func__)和可移植整数类型[14]
  • C99中通过内建的关键字_Complex和由它定义的宏complex实现了虚数类型——float complexdouble complex;而C++通过一种完全不同的方式实现了它——使用虚数库。这两种方式是不兼容的。
  • VLA可能导致sizeof的值在运行时才能确定。[15]
    void foo(size_t x, int a[*]);  // 使用VLA的函数
    void foo(size_t x, int a[x]) 
    {
        printf("%zu\n", sizeof a); // 等同于sizeof(int*)
        char s[x*2];
        printf("%zu\n", sizeof s); // 将显示x*2
    }
    
  • 在C99中,如果一个结构体有多个变量且它的最后一个变量为数组,则它可以是一个柔性数组。它与VLA类似,但VLA不能出现在定义中。它不指定数组的长度。截止到C++20,C++中没有类似的功能。下面是它的一个例子:
    struct X
    {
        int n, m;
        char bytes[];
    }
    
  • 截止到C++20标准,restrict类型限定符并没有出现在C++里;但是很多编译器都提供了对它的支持(如GCC,MSVC,ICC,Clang等)。
  • 函数声明中的数组类型限定符在C++中是不允许的,下面是一个例子:
    int foo(int a[const]);     // 相当于int foo(int *const a);
    int bar(char s[static 5]); // 表示s的长度至少为5
    
  • C中的复合文字特性在C++中以一种叫做“列表初始化”的方法被实现——虽然它们在意义和作用上是不同的,但它们通常能起到相同的效果。
    struct X a = (struct X){4, 6};  // 在C++ 中等于 X{4, 6}.
    
  • 从C99开始,C语言支持结构的指定值初始化;在C++20以前,这是不允许的;但是从C++20标准开始,C++也支持了这个特性。
    struct X a = {.n = 4, .m = 6};  
    char s[20] = {[0] = 'a', [8]='g'};
    
  • 在C++中,可以使用“noreturn”来标记一个没有返回值的函数,但在C11以前没有这样一个关键字。在C11中新增了关键字“_Noreturn”。
  • C语言允许在函数原型中声明复合数据类型,而C++不允许。
  • C++较C语言增加了一些关键字,这使得如果一段C语言代码使用了C++中新增的关键字作为标识符,它将会是非法的。
    struct template 
    {
        int new;
        struct template* class;
    };
    
    以上代码在C语言中是允许的,但是在C++中不允许。

在C和C++中行为不同的语句

有一些语句在C和C++中都有效,但是在两种语言中会产生不同的结果。

  • char类型在C语言中的大小等于int类型的大小,而在C++中的大小就是“char”类型的大小,即1字节。因此当有语句sizeof(char)时,在C语言中的结果等于sizeof(int),在C++中的结果为1。而且这种差异导致在C语言中,无论声明形式为signed char还是 unsigned char,其所声明的都是一个有符号表达式,而在C++中,这取决于实现。
  • 在C++中,一个const变量除非被显式声明为extern,否则这个变量将为内部链接,这与C extern语言默认变量作用域为文件不同。实际上,这不会导致相同的C和C++代码产生不同的结果,而是会导致编译时或链接错误。
  • 在C语言中,如果不希望在一个文件中链接到一个不再此文件中的内联函数,则必须在函数声明中加extern;而C++则会自动处理这些问题。更详细地,C语言有两种inline函数定义:普通的外部定义(即显式添加extern)和内联版本。另一方面,C++仅为内联函数提供内联定义。在C语言中,内联定义与static定义类似,因为它可以与其他文件中有文件作用域的同名函数或同一翻译单元中任意数量的相同名称的内联或普通函数共存,这是合法的。当这些函数对编译器可见时,C编译器会选择一个函数(这取决于实现)。而在C++中,若一个inline函数具有外部作用域,则它在所有文件中的声明都必须相同。最后,静态内联函数在C和C++中的行为相同。
  • C和C++中都有布尔类型,但是它们有不同的定义。在C++中,boolfalsetrue作为内建类型关键字提供,在实现时,truefalse为“bool”类型。而在C99以前,C语言中没有布尔类型;C99提供了关键字_Bool类型,但没有“true”,“false”关键字。为了增加与C++的兼容性,C99提供了头文件<stdbool.h>[16],其中一部分代码如下(选自GCC编译器,著作权信息在注释中):
/* Copyright (C) 1998-2020 Free Software Foundation, Inc.

This file is part of GCC.

GCC is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

GCC is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

Under Section 7 of GPL version 3, you are granted additional
permissions described in the GCC Runtime Library Exception, version
3.1, as published by the Free Software Foundation.

You should have received a copy of the GNU General Public License and
a copy of the GCC Runtime Library Exception along with this program;
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
<http://www.gnu.org/licenses/>.  */

/*
 * ISO C Standard:  7.16  Boolean type and values  <stdbool.h>
 */
#ifndef __cplusplus

#define bool	_Bool
#define true	1
#define false	0
#else

...

需要注意的是,_Bool在C语言中被储存为int类型。

  • 下面这个函数在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(结构体)类型前需要添加“struct”,而在这个例子中,因为T前没有struct,所以它所代表的是在外部定义的int变量;而在C++中,因为可以省略struct,因此造成了歧义。然而,在C++中如果使用sizeof T这种形式,编译器会更倾向于认为T是一个表达式,因此编译会出错;而在C语言中则不会。


参考资料

  1. ^ 1.0 1.1 Stroustrup, Bjarne. An Overview of the C++ Programming Language in The Handbook of Object Technology (Editor: Saba Zamir). CRC Press LLC, Boca Raton. 1999. ISBN 0-8493-3135-8. (PDF): 4. [2009-08-12]. (原始内容存档 (PDF)于2012-08-16). 
  2. ^ B.Stroustrup. C and C++: Siblings. The C/C++ Users Journal. July 2002. (PDF). [2019-03-17]. (原始内容存档 (PDF)于2018-12-21). 
  3. ^ Bjarne Stroustrup's FAQ – Is C a subset of C++?. [22 Sep 2019]. (原始内容存档于2016-02-06). 
  4. ^ B. Stroustrup. C and C++: A Case for Compatibility. The C/C++ Users Journal. August 2002. (PDF). [18 August 2013]. (原始内容存档 (PDF)于2012-07-22). 
  5. ^ Rationale for International Standard—Programming Languages—C页面存档备份,存于互联网档案馆), revision 5.10 (2003.04).
  6. ^ Prata, Stephen. C Primer Plus第5版中文版. 北京: 人民邮电出版社. 2005: 12. ISBN 9787115130228. 
  7. ^ N4659: Working Draft, Standard for Programming Language C++ (PDF). §Annex C.1. (原始内容存档 (PDF)于2017-12-07).  ("It is invalid to jump past a declaration with explicit or implicit initializer (except across entire block not entered). … With this simple compile-time rule, C++ assures that if an initialized variable is in scope, then it has assuredly been initialized.")
  8. ^ N4659: Working Draft, Standard for Programming Language C++ (PDF). §Annex C.1. (原始内容存档 (PDF)于2017-12-07). 
  9. ^ IBM Knowledge Center. ibm.com. 
  10. ^ FAQ > Casting malloc - Cprogramming.com. (原始内容存档于2007-04-05). 
  11. ^ 4.4a — Explicit type conversion (casting). 16 April 2015. (原始内容存档于2016-09-25). 
  12. ^ Lippman, Stanley; Lajoie, Josee; Moo, Barbara. C++ Primer 5th. Addison-Wesley Professional. 2012: 165. ISBN 9780321714114. 
  13. ^ longjmp - C++ Reference. www.cplusplus.com. (原始内容存档于2018-05-19). 
  14. ^ Prata, Stephen. C Primer Plus 5th. ISBN 0672326965. 
  15. ^ Incompatibilities Between ISO C and ISO C++. (原始内容存档于2006-04-09). 
  16. ^ ISO/IEC 9899:1999 7.16 (PDF). [2020-07-02]. (原始内容存档 (PDF)于2018-01-27). 

外部链接