为何 C++ 里有些东西是未定义的

因为机器的不同以及 C 里面也有很多未定义的东西。ISO C++ 标准里有以下术语的详细定义:“未定义”、“未指明(unspecified)”、“由实现定义”,以及“合乎语法的(well-formed)”。注意,这些术语的含义和 ISO C 标准里的定义不太相同,而且也和它们常见的用法不同。假若没有察觉到不同的人对这些术语的认识会有所偏差,讨论问题的时候常常会极度混乱。

这是一个正确的答案,虽然可能不尽人意。和 C 一样,C++ 力图榨干硬件的每一滴血。这就是说,C++ 必须使用各种特定机器的“自然”方式来和硬件实体(位、字节、字、地址、整数计算,以及浮点数计算等)打交道,而不是我们想怎么搞就怎么搞。注意,很多被人们称为“未定义”的“东西”,事实上都是“由实现定义”的,所以只要了解我们正在使用的机器,就可以编写出完美的专门代码。整数的大小以及浮点数的取整行为正是如此。

下面这个关于未定义行为的例子可能是最广为人知且臭名昭彰的:

C++(和 C)中数组和指针的概念是对机器中内存和地址概念的直接表述,所以没有任何额外开销。指针的基本操作直接被映射成机器指令,不会进行范围检测。进行范围检测会影响运行时效率以及生成代码的大小。C 是被设计来编写操作系统的,要和汇编代码拼速度,所以这么决定(不检测范围)是必须的。同样,和 C++ 不同的是,即使编译器生成了检测错误的代码,C 也没有报告错误的合适的方法:C 没有异常。C++ 跟随 C 是为了与之兼容以及直接和汇编竞赛(在 OS、嵌入式系统以及数值计算领域)。如果你需要范围检测,可用一个合适的带检测的类(vector、智能指针、string 等)。好的编译器可在编译时捕捉到 a[100] 越界了,然而,要判定 p[100] 是否越界就要困难得多。一般来说,在编译时是不可能捕捉到所有范围错误的。

其它关于未定义行为的例子起源于编译模型。编译器不能检测到各个单独的编译单元里,对象或者函数的定义是否不一致。例如:

在 C 和 C++ 里,编译 file1.c 和 file2.c 后,将它们链接成为同一个程序是非法的。链接器应该能捕捉到 S 的定义不一致,但它没有必须这么做的义务(大多数编译器都不捕捉)。很多情况下,很难捕捉各个单独的编译单元之间的不一致性。确保使用头文件的一致性有助于最大限度地减少这种问题。链接器也有正在不断改善的好兆头。注意,C++ 链接器捕捉几乎所有和函数声明不一致有关的错误。

最后,我们来看一些非常恼人的表达式的未定义行为(很明显,应该对这些行为进行定义)。例如:

j 的值是未定义的,这是为了允许编译器生成最优化的代码。据称,和确保“平常地从左到右进行求值”相比,让编译器拥有求值顺序的自由这种做法能生成明显高效的多的代码。我不这么认为,但目前无数的编译器都利用了这种自由,而且有不少人热烈地为这种自由呐喊,所以要改变它并非易事,而且可能需要数十年的时间才能被整个 C 和 C++ 世界的人接受。失望,并非所有编译器都能为类似 ++i+i++ 这样的代码发出警告。类似地,参数的求值顺序也是未指明的。

我觉得,未定义、未指明或者由实现定义等等的“东西”实在是太多了。然而,这说起来容易,甚至也很容易给出这样的例子,但是要修正却太难了。不过,避免这些问题从而编写出可移植的代码也并非什么难事。