你可能听说过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中编译将是实现定义的:
1 const size_t buffer_size = 5;
2 int buffer[buffer_size];
3
4 // int main() {
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)
2 int buffer[BUFFER_SIZE];
关键字const是由Bjarne Stroustrop为C++创建的:减少对宏的需求。 所以,C ++对于什么是常量表达式更加宽容,使得const变量更强大。
我惊讶地发现const起源于C ++,然后由C所采纳。我假设const来自C,而C ++采用相同的概念并扩展它以减少对宏的需求。我理解C语言对宏的广泛使用,但在标准化C时故意减少const的使用似乎并不明智。
修改const变量
以下代码在C中使用导致约束违规:
1 const int foo = 1;
2 int* 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非限定版本是不同类型。
无参的函数声明
1 int func();
在C ++中,这声明了一个不带参数的函数。但同样的语法,在C中则声明了一个可以接受任意类型参数、任意数量参数的函数。
根据C11标准6.7.6.3第10和14段:
void类型的未命名参数作为列表中唯一项的特殊情况指定该函数没有参数。
函数声明符中的空列表是该函数定义的一部分,指定该函数没有参数。函数声明符中的空列表不是该函数定义的一部分,它指定不提供有关参数数量或类型的信息。
所以在C中,以下代码将是合法的:
1 // func.h
2 int func();
1 // func.c
2 int func(int foo, int bar) {
3 return foo + bar;
4 }
1 // main.c
2 #include "func.h"
3
4 int main() {
5 return func(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
2 int func(int foo, int bar);
1 #include <stdio.h>
2
3 // func.c
4 int func(float foo, float bar) {
5 return printf("%f, %f\n", foo, bar);
6 }
1 // main.c
2 #include "func.h"
3
4 int main() {
5 return func(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。这可能是错误,但我从来没有找到一个编译器给它任何东西,只是一个关于隐式转换的警告:
1 int main() {
2 auto x = "actually an int";
3 return x;
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允许定义具有可变长度的自动存储的数组。例如:
1 void f(int n) {
2 int arr[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 ++采用。
1 typedef struct {
2 float red;
3 float green;
4 float blue;
5 } Colour;
6
7 int main() {
8 Colour c = { .red = 0.1, .green = 0.5, .blue = 0.9 };
9 return 0;
10 }
在C ++中,你必须像这样初始化:Colour c = {0.1,0.5,0.9}; 这对于Color的定义更改来说更难阅读并且不健壮。我听说指定的初始化程序未来将会在C ++ 20中实现,不过已经等了21年了。