4、C++编译器对C的影响
实际上,C 语言被当成了没有某些功能的 C++。比如微软的 C 编译器直到2015 版才开始支持 C99 功能(即便如此,它还是以 bug 修复 bug 的方式来支持兼容性,因为客户可能会震惊地发现可变参数宏居然可以运行)。但是,无论是标准的编译器还是其他编译器中都可以看到相同的方法,这些都是相关的问题。
主要问题在于,C 和 C++标准都是根据编译器开发人员的反馈而编写的,而且大多数都是 C++开发人员(有些人对现实世界编程一无所知,而且他们还认为现实世界的做法与自己的观点完全吻合,真是令人窒息的操作)。虽然我也没有遵循标准的开发程序,但是我很确定 C99 及其后版本中令人讨厌的诸多功能皆来自那些编译器开发人员。他们只从 C++的角度出发考虑,而且还将这些功能强加给了 C,还美其名曰简化编译器。
当然我指的是“未定义的行为”以及编译器的处理方式。这已成为一大毒瘤(只要你的代码依赖于二进制补码算术,就会被认定具有未定义的行为,编译器会抛弃整块代码)。
在我看来,以下四种行为尽管不值得提倡,但前两个也并非不可接受:
依赖于体系结构的行为(即依赖于 CPU 体系结构的行为)。包括绝大部分算术运算。例如,如果我知道目标及其使用了两个协处理器,为什么编译器会选择另一种方式,仅仅是为了获得理论上的优化?同样的问题也适用于移位运算。如果我知道 x86 会忽略移位偏移量的高比特,在 ARM 上负的左移相当于右移,那么为什么不能专门针对该体系结构编写程序呢?毕竟,连整数的大小在不同平台上都不一样。这种不可移植性只需警告就好,让用户自行处理。
指针魔法和类型双关。这似乎又是编译器优化带来的限制。我同意,在重叠的内存区域上使用 memcpy(),不同的实现可能会给出不同的行为(现代的 x86 实现会从区域尾部开始复制),而且还依赖于地址的相对位置,但其他的规则就没什么道理了。例如,无法使用两个不同类型的指针同时操作同一块内存区域。我无法想象为什么这种行为被禁止,其原因只可能是编译器优化。这样就不可能利用联合体将整数转换成浮点数。Linus 也曾吐槽过这一点,我就不用重复了。但在我看来,这样做的目的或者是更好的编译器优化,或者是出于 C++的要求(由于类型跟踪的要求)。
实现中定义的行为(即超出 C 标准规定的行为)。我常用的例子就是函数调用:根据调用的习惯约定和编译器的实现,函数的参数的求值顺序可能完全是随机的,因此 foo(*ptr++, *ptr++, *ptr++)的结果是未定义的,因此即使你知道目标体系结构,也不应该依赖于这种行为。
完全未定义的行为。最常见的例子就是在一条语句中改变变量状态,例如著名的 I++ + i++,或者更甚的 *ptr++ = *ptr++ +*ptr++。
由于 C++比 C 更高级(尽管它由许多来自 C 的特性,但都不建议使用,应该使用 reinterpret_cast<>代替类型转换,用引用代替指针,等等),所以不要期待 C++程序员能够像 C 程序员那样理解底层代码。当然,由于 C++程序员占绝大多数,C/C++的耦合也极其常见,所以 C 编译器通常会进行扩展以支持C++,并使用 C++重写,以适应其复杂度。所以很不幸,你不得不使用 C++编译器来编译 C 编译器(还好我们还有 LCC、PCC 和 TCC 等纯 C 编译器)。
5、总 结
总的来说,我喜欢C所处的中层位置,它既可以完成一些底层的实现,例如轻松地操作内存,同时又可以享受高级语言的好处。另一方面,我对C++强烈的不满来自其在设计上的选择,而且这些设计影响了C标准和编译器。
至少我不可能用 C90 特别版取代 C90,并假装原来的版本不存在。 |