C++ 不是 C 的超集!

你可能听说过 C ++ 是 C 的超集。但如果你有两种编程语言的经验,你就会知道这根本不是真的。
当然,C ++ 有许多功能,C 没有;但也有一些功能只有 C 有,而 C++ 没有。 并且,也许最重要的是,有些代码可以在两种语言中编译,但却执行不同的操作。
你可以找到很多关于 C ++、C 之间异同的信息,但很多看起来很分散。在这里,我创建了一个简明的对比指南,并从 C、C++ 语言规范标准中摘录一些内容来支持这些异同。

注意事项:

本文主要针对 C、C++ 语言, 所以你需要熟悉 C 或 C ++ 中的其中之一,两个都熟悉则更好。
当我提到 C ++ 时,我指的是 C ++ 11 以上的版本,尽管本文大部分都适用于 C++ 早期的标准。 我也将引用 C ++ 17 标准 (目前 C++ 的最新标准)。
当我提到 C 时,我指的是 C99 标准,同时我也将参考 C11 标准 (目前 C 的最新标准)。

值得注意的是,许多编译器不完全兼容编程语言标准。这正是难以确定什么是标准,什么是不合规以及什么是实现定义的部分原因。如果你想要查看其他编译器的示例,我建议使用 Compiler Explorer
亲自动手实践一番,对比很有趣。

同样的代码,用两种语言编译,但结果不同

我认为这是最重要的差异类别方法策略。

const

关键字 const 在 C ++ 中与在 C 中具有不同的语义,实际上,它比我在第一次撰写此博客文章时的想法更为微妙。
差异归结为编程语言是否允许常量表达的编写,常量表达式可以在编译器编译通过。例如,这里通过常量来界定静态数组的大小,下面的示例将用 C ++ 编译,但它是否在 C 中编译将是实现定义的:


复制代码

1const size_t buffer_size =5;
2intbuffer[buffer_size];
3
4//intmain() {
5// ...
6// }

但是常量表达式在 C 中的表现如何呢?
在这里,我们引用 C11 标准的几个部分以阐述为什么如此实现,C11 6.6 第 6 段定义了一个整数常量表达式:
整数常量表达式应具有整数类型,并且只能具有整数常量的操作数、枚举常量、字符常量,结果为整数常量的 sizeof 表达式,以及作为强制转换的直接操作数的浮点常量。 整数常量表达式中的转换运算符只能将算术类型转换为整数类型,除非作为 sizeof 运算符的操作数的一部分。
但什么是“整数常数”? 从 6.4.4 开始,这些是字面值,而不是变量,例如 1。

这归结为只有像 1 或 5 + 7 这样的表达式可以是 C 中的常量表达式。 变量不能是常量表达式
。 正如我所料,此示例在 gcc 编译编译不通过
,但它确实可以在 Clang 编译通过
:为什么?
答案见 C11 6.6 第 10 段:
一种实现可以接受其他形式的常量表达式。
所以在 C 中,如果要编写可移植版本代码,上面的代码必须使用宏预处理器:


复制代码

1#define BUFFER_SIZE (5)
2intbuffer[BUFFER_SIZE];

关键字 const 是由 Bjarne Stroustrop 为 C++ 创建的:减少对宏的需求。 所以,C ++ 对于什么是常量表达式更加宽容,使得 const 变量更强大。
我惊讶地发现 const 起源于 C ++,然后由 C 所采纳。我假设 const 来自 C,而 C ++ 采用相同的概念并扩展它以减少对宏的需求。我理解 C 语言对宏的广泛使用,但在标准化 C 时故意减少 const 的使用似乎并不明智。

修改 const 变量

以下代码在 C 中使用导致约束违规:


复制代码

1constintfoo =1;
2int* bar = &foo;
3*bar =2;

C11 6.5.16.1 第 1 段列出了一些约束说明,其中一个约束必须为真,类型转换才有效。我们的例子的相关约束如下:
左操作数具有原子性,限定或非限定指针类型,并且(考虑左值操作数在左值转换后将具有的类型)两个操作数都是指向兼容类型的限定或非限定版本的指针,左侧指向的类型具有全部右边指出的类型的限定符。

为了符合要求,如果存在约束违规,编译器必须进行诊断,这可能是警告或错误。 我发现它通常是一个警告,这意味着它通常可以在 C 中编译
,但运行后会给出未定义的结果:

上述代码, 在 C ++ 中不会编译
。 我认为这是因为 const T 是与 T 不同的类型,并且不允许隐式转换。 而在 C 中,const 只是一个限定符。
C ++ 17 6.7.3:
类型的 cv 限定或 cv 非限定版本是不同类型。

无参的函数声明


复制代码

1intfunc();

在 C ++ 中,这声明了一个不带参数的函数。但同样的语法,在 C 中则声明了一个可以接受任意类型参数、任意数量参数的函数。
根据 C11 标准 6.7.6.3 第 10 和 14 段:

void 类型的未命名参数作为列表中唯一项的特殊情况指定该函数没有参数。
函数声明符中的空列表是该函数定义的一部分,指定该函数没有参数。函数声明符中的空列表不是该函数定义的一部分,它指定不提供有关参数数量或类型的信息。
所以在 C 中,以下代码将是合法的:


复制代码

1// func.h
2intfunc();


复制代码

1// func.c
2intfunc(intfoo,intbar) {
3returnfoo + bar;
4}


复制代码

1// main.c
2#include"func.h"
3
4intmain() {
5returnfunc(5,6);
6}

不过,同样代码将导致 C ++ 中的编译器报错:

main.c:5:12: error: no matching function for call to ‘func’  return func(5, 6);
^~~~  ./func.h:2:5: note: candidate function not viable:  requires 0 arguments, but 2 were provided

名称解析

有一些常见的实现细节,使我们可以进一步阐明这一点。 假如我在 Linux 机器上使用 Clang 编译器,则以下代码可以在 C 下编译和链接:


复制代码

1// func.h
2intfunc(intfoo,intbar);


复制代码

1#include 
2
3// func.c
4intfunc(float foo, float bar) {
5returnprintf("%f, %f\n", foo, bar);
6}


复制代码

1// main.c
2#include"func.h"
3
4intmain() {
5returnfunc(5,6);
6}

但是上述代码却不能在 C ++ 中编译通过。
因为,C ++ 编译器通常使用名称来进行函数重载。它们“破坏”函数的名称以便对它们的参数进行编码,例如:通过将参数类型附加到函数中。通常,C 编译器只将函数名称存储为符号。我们可以通过反编译 C 和 C ++,来比较 func.o 的符号表看看这些区别。
C 编译的 func.o 解析如下:

╰─λ objdump -t func.o

func.o: file format elf64-x86-64
SYMBOL TABLE:

0000000000000000 l df ABS
0000000000000000 foo.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata.str1.1 0000000000000000 .rodata.str1.1
0000000000000000 g F .text 000000000000002e func

0000000000000000 UND
0000000000000000 printf
C++ 编译的 func.o 解析如下:

╰─λ objdump -t func.o

func.o: file format elf64-x86-64
SYMBOL TABLE:

0000000000000000 l df ABS
0000000000000000 foo.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .rodata.str1.1 0000000000000000 .rodata.str1.1
0000000000000000 g F .text 000000000000003b _Z4funcff

0000000000000000 UND
0000000000000000 printf

auto

auto 在 C ++ 中用于类型自动推断,但同时 auto 也是一个 C 关键字,只是我从未真正看到工程实践的应用。
以下 C 具有约束违规,即未指定 type。这可能是错误,但我从来没有找到一个编译器给它任何东西,只是一个关于隐式转换的警告:


复制代码

1intmain() {
2autox ="actually an int";
3returnx;
4}

在 C99 之前,如果没有类型说明符是合法的,并且类型将被假定为 int。当我使用 Clang
gcc
编译它时会发生这种情况,因此我们得到一个警告,因为隐式将 char 数组转换为 int。
在 C ++ 中,直接显示编译不通过,因为 x 的类型被推断为,

error: cannot initialize return object of type ‘int’ with an lvalue of type ‘const char *’  return x;

一些 C 有,但 C ++ 没有的功能

尽管 C 是一种非常短小精悍的编程语言,并且 C ++ 很庞大,但 C 语言中有一些 C ++ 没有的有用功能。

可变长度数组

VLA 允许定义具有可变长度的自动存储的数组。例如:


复制代码

1void f(intn) {
2intarr[n];
3// ......
4}

实际上,VLA 在 C11 标准中是可选的,这使得它们无法移植。
但这些却不是 C ++ 的一部分,部分可能是因为 C ++ 标准库在很大程度上依赖于动态内存分配来创建使用 std::vector 类似的容器。

受限的指针

C 定义了第三种类型限定符(除了 const 和 volatile):restrict。这仅用于指针。使指针受限制告诉编译器“我将只通过此指针访问底层对象以获取此指针的范围”,因此它不能混淆。如果你打破这个约束,你将得到未定义的行为。
这有助于优化。一个典型的例子是 memmove,你可以告诉编译器 src 和 dst 不重叠。
引用 C11 6.7.3 第 8 段:

通过限制限定指针访问的对象与该指针具有特殊关联。这种关联在下面的 6.7.3.1 中定义,要求对该对象的所有访问都直接或间接地使用该特定指针的值.135)。
限制限定符(如寄存器存储类)的预期用途是促进优化,并从构成符合程序的所有预处理翻译单元中删除限定符不会改变其含义(即可观察行为)。
受限的指针不是 C ++ 标准的一部分,但实际上被许多编译器扩展支持。

我对 受限的指针
感到疑惑,因为它看起来好像玩火。有趣的是,在使用它时遇到编译器优化错误似乎很常见,因为我从未在真正使用过的代码中应用过它。

特定初始化程序

C99 引入了一种非常有用的初始化结构的方法,但我不明白它为什么没有被 C ++ 采用。


复制代码

1typedefstruct {
2floatred;
3floatgreen;
4floatblue;
5} Colour;
6
7intmain() {
8Colour c = { .red =0.1, .green =0.5, .blue =0.9};
9return0;
10}

在 C ++ 中,你必须像这样初始化:Colour c = {0.1,0.5,0.9}; 这对于 Color 的定义更改来说更难阅读并且不健壮。我听说指定的初始化程序未来将会在 C ++ 20 中实现,不过已经等了 21 年了。

原文链接:
C++ is not a superset of C