为什么我十分喜欢C,却很不喜欢C++( 二 )


其次,实际上 C++不仅是多种语言,而且还是一种元语言(即模板) 。我了解 C++的创建初衷,也同意它对于与类型无关的代码的处理,比 C 预处理器更好 。但实际上,它产生的代码十分可怕,原本是“头文件仅包含声明,实现放在编译好的代码中”,变成了“头文件包含所有项目会用到的代码” 。我不喜欢过于冗长的编译时间,但这种方式只能让情况更糟 。
最后,我觉得 C++的出现反而给 C 带来了约束以及不良影响 。我不是在讨论 C/C++,也不是指 C 与 C++的共通之处,我讨论的是耦合对标准和编译器都有不良影响 。一方面,C++建立在 C 之上,从而得到了极大的发展;另一方面,如果 C++中没有 C 遗留下来的大多数功能的话,情况可能会更好(当然,C++曾设法通过淘汰的方式逐步放弃某些 C 功能,但对于旧功能的支持仍然存在) 。但是,C++ 24 能够在 C++ 21 的基础之上,发展成为一门独立的编程语言吗?大多数过时的功能都可以抛弃吗?我对此表示怀疑 。

为什么我十分喜欢C,却很不喜欢C++

文章插图
 
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 编译器) 。


推荐阅读