论未定义的行为

一般来说,UB被认为是必须避免的东西,目前的C标准本身列出了附录J中的不少例子。

但是,在某些情况下,除了牺牲可移植性之外,我认为利用UB没有任何害处。

考虑以下定义:

int a = INT_MAX + 1; 

评估该表达导致UB。 但是,如果我的程序打算在32位CPU上运行,模块化算术代表Two’s Complement中的值,我倾向于相信我可以预测结果。

在我看来,UB有时只是C标准告诉我的方式:“我希望你知道你在做什么,因为我们无法保证会发生什么。”

因此我的问题是:即使C标准认为它要调用UB,或者无论在什么情况下都要避免“UB”,有时依赖依赖于机器的行为是否安全?

,除非您还保持编译器相同, 并且您的编译器文档定义了其他未定义的行为。

未定义的行为意味着您的编译器可以出于任何原因忽略您的代码 ,使您认为应该是正确的。
有时这是为了优化, 有时是因为这样的架构限制 。


我建议你阅读这个 ,它解决了你的确切例子 。 摘录:

有符号整数溢出:

如果对int类型(例如)的算术溢出,则结果是未定义的。 一个例子是INT_MAX + 1不能保证是INT_MIN 。 此行为允许某些类别的优化对某些代码很重要。

例如,知道未定义INT_MAX + 1允许将X + 1 > X优化为true 。 知道乘法“不能”溢出(因为这样做将是未定义的)允许将X * 2 / 2优化为X 虽然这些看似微不足道,但这些事情通常都是通过内联和宏观扩展来揭示的。 这允许的更重要的优化是<=循环:

 for (i = 0; i <= N; ++i) { ... } 

在这个循环中,如果i在溢出时未定义,编译器可以假设循环将完全迭代N + 1次,这允许广泛的循环优化启动。另一方面,如果变量被定义为环绕在溢出时,编译器必须假设循环可能是无限的(如果NINT_MAX发生) - 然后禁用这些重要的循环优化。 这特别影响64位平台,因为很多代码使用int作为归纳变量。

没有。

编译器在优化代码时利用未定义的行为。 一个众所周知的例子是GCC编译器中的严格溢出语义 ( 在这里搜索strict-overflow )例如,这个循环

 for (int i = 1; i != 0; ++i) ... 

据说依赖于有符号整数类型的“机器相关”溢出行为。 但是, 严格溢出语义规则下的GCC编译器可以(并且将)假设递增int变量只能使它变大 ,而且永远不会变小。 这个假设将使GCC优化算法并产生无限循环

 for (;;) ... 

因为这是未定义行为的完美有效表现。

基本上,在C语言中没有“机器依赖行为”这样的东西。 所有行为都由实施决定, 实施水平是您可以达到的最低水平。 实施将您与原始机器隔离开来并完美隔离您。 除非实现明确允许您这样做,否则无法突破隔离并进入实际的原始机器。 有符号整数溢出通常不是允许您访问原始机器的上下文之一。

如果您知道您的代码只针对特定的体系结构,编译器和操作系统,并且您知道未定义的行为是如何工作的(并且不会发生变化),那么偶尔使用它本身并不是错误的。 在你的例子中,我想我也可以告诉你将会发生什么。

但是,UB很少是首选解决方案。 如果有更清洁的方式,请使用它。 使用未定义的行为绝对不是绝对必要的,但在少数情况下可能会很方便。 永远不要依赖它。 和往常一样,如果您依赖UB,请评论您的代码。

并且,请不要发布依赖于未定义行为的代码,因为当它们在具有与您所依赖的实现不同的实现的系统上编译时,它最终会在某人面前爆炸。

一般来说,最好完全避免它。 另一方面,如果您的编译器文档明确声明为该编译器定义了标准UB的特定事物,您可以利用它,可能添加一些#ifdef / #error机制来阻止编译以防另一个编译器用来。

如果C(或其他语言)标准声明某些特定代码在某些情况下将具有未定义行为,则这意味着C编译器可以生成代码以在该情况下执行任何所需操作, 同时保持符合该标准 。 许多特定的语言实现已经记录了超出通用语言标准所要求的行为。 例如,Whizbang Compilers Inc.可能会明确指定其memcpy的特定实现将始终按地址顺序复制单个字节。 在这样的编译器上,代码如下:

   unsigned char z [256];
   z [0] = 0x53;
   z [1] = 0x4F;
   memcpy(z + 2,z,254);

将具有由Whizbang文档定义的行为,即使此类代码的行为未由任何非供应商特定的C语言规范指定。 此类代码与符合Whizbang规范的编译器兼容,但可能与符合各种C标准但不符合Whizbang规范的其他编译器不兼容。

有许多情况,尤其是嵌入式系统,程序需要做一些C标准不需要编译器允许的事情。 编写这样的程序是不可能与所有符合标准的编译器兼容的,因为一些符合标准的编译器可能无法提供任何方法来完成需要完成的工作,甚至那些可能需要不同语法的编译器。 尽管如此,编写可由任何符合标准的编译器正确运行的代码通常具有相当大的价值。

如果标准说做某事是未定义的,那么它是未定义的。 您可能认为您可以预测结果会是什么,但您不能。 对于特定的编译器,您可能总是得到相同的结果,但对于编译器的下一次迭代,您可能不会。

未定义的行为是如此容易避免 – 不要写那样的代码! 那么为什么像你这样的人想搞砸呢?

没有! 仅仅因为它编译,运行并提供您希望的输出并不能使其正确。