C遇到的常见的未定义/未指定的行为是什么?

C语言中未指定行为的一个例子是函数参数的评估顺序。 它可能是从左到右,从右到左,你只是不知道。 这会影响foo(c++, c)foo(++c, c)的评估。

还有什么其他不明确的行为可以让不知道的程序员感到惊讶?

一个语言律师的问题。 Hmkay。

我个人top3:

  1. 违反严格的走样规则
  2. 违反严格的走样规则
  3. 违反严格的走样规则

    🙂

编辑这里有一个小错误两次的例子:

(假定32位整数和小端)

 float funky_float_abs (float a) { unsigned int temp = *(unsigned int *)&a; temp &= 0x7fffffff; return *(float *)&temp; } 

该代码试图通过位浮点的符号位直接获得浮点数的绝对值。

但是,通过从一个types转换为另一个types来创build指向对象的指针的结果是无效的C.编译器可能会认为指向不同types的指针不指向同一块内存。 对于除void *和char *之外的所有types的指针(符号无关紧要)都是如此。

在上面的情况下,我做了两次。 一旦获得float a的int-alias,并且一次将该值转换回float。

有三种有效的方法可以做到这一点。

在转换过程中使用char或void指针。 这些东西总是别名,所以它们是安全的。

 float funky_float_abs (float a) { float temp_float = a; // valid, because it's a char pointer. These are special. unsigned char * temp = (unsigned char *)&temp_float; temp[3] &= 0x7f; return temp_float; } 

使用memcopy。 Memcpy需要void指针,所以它也会强制别名。

 float funky_float_abs (float a) { int i; float result; memcpy (&i, &a, sizeof (int)); i &= 0x7fffffff; memcpy (&result, &i, sizeof (int)); return result; } 

第三种有效的方法是:使用联合。 自C99以来,这显然不是未定义的:

 float funky_float_abs (float a) { union { unsigned int i; float f; } cast_helper; cast_helper.f = a; cast_helper.i &= 0x7fffffff; return cast_helper.f; } 

我个人最喜欢的未定义行为是,如果一个非空的源文件没有以换行符结束,行为是不确定的。

我怀疑这是真的,虽然没有编译器,我会看到已经根据是否是新行终止不同的处理源文件,除了发出警告。 所以这不会让意识不到的程序员感到意外,除此之外他们可能会对这个警告感到惊讶。

因此,对于真正的可移植性问题(大多数是依赖于实现而不是未指定或未定义的,但我认为这属于问题的精神):

  • 字符不一定(未)签名。
  • int可以是16位的任何大小。
  • 浮动不一定是IEEE格式或符合。
  • 整数types不一定是二进制补码,整数算术溢出会导致未定义的行为(现代硬件不会崩溃,但是一些编译器优化会导致与环绕不同的行为,即使硬件是这样的,例如if (x+1 < x)可能会被优化,因为在x有符号types时总是为false:请参阅GCC中的-fstrict-overflow选项)。
  • “/”,“。” 和#include中的“..”没有定义的含义,可以通过不同的编译器来区别对待(这实际上是不一样的,如果出错了,它会毁了你的一天)。

真正严重的,甚至可以在你开发的平台上感到惊讶,因为行为只是部分未定义/未指定:

  • POSIX线程和ANSI内存模型。 并发访问内存不像新手想象的那样清晰。 不稳定的做不了新手的想法。 内存访问的顺序不像新手想象的那样明确。 访问可以在某些方向跨越内存障碍移动。 内存caching一致性不是必需的。

  • 分析代码并不像您想象的那么容易。 如果你的testing循环没有效果,编译器可以删除它的一部分或全部。 内联没有定义的效果。

而且,正如我认为尼尔斯提到的那样:

  • 违反严格的协调规则。

用指针划分东西。 只是不会因为某些原因编译… 🙂

 result = x/*y; 

我最喜欢的是:

 // what does this do? x = x++; 

回答一些评论,根据标准是未定义的行为。 看到这一点,编译器可以做任何事情,包括格式化硬盘驱动器。 在这里看到这个评论 。 重要的不是你可以看到有一些行为可能有合理的期望。 由于C ++标准和序列点的定义方式,这行代码实际上是未定义的行为。

例如,如果在上面的行之前有x = 1 ,那么后面的有效结果是什么? 有人评论说应该是

x增加1

所以之后我们应该看到x == 2。 然而,这不是真的,你会发现一些编译器,其后x == 1,甚至可能x == 3。你将不得不仔细看看生成的程序集,看看为什么这可能是,但差异是由于到底层的问题。 实质上,我认为这是因为编译器允许以任何顺序评估两个赋值语句,所以它可以先执行x++ ,或者先执行x =

我遇到的另一个问题(这是定义,但绝对意外)。

字符是邪恶的。

  • 这取决于编译器的感觉
  • 没有规定为8位

编译器不必告诉你,如果函数原型不可用,那么调用的参数个数错误或参数types错误。

我无法统计我修正了printf格式说明符以匹配它们的参数的次数。 任何不匹配都是未定义的行为

  • 不,你不能传递一个int (或long )到%x – 一个unsigned int是必需的
  • 不,你不能传递一个unsigned int%d – 一个int是必需的
  • 不,您不能将size_t传递给%u%d – 请使用%zu
  • 不,你不能打印%d%x的指针 – 使用%p并将其转换为void *

我见过很多相对缺乏经验的程序员被多字符常量咬住了。

这个:

 "x" 

是一个string文字(在大多数情况下,它是char[2]types和衰减char* )。

这个:

 'x' 

是一个普通的字符常量(由于历史原因,它是inttypes的)。

这个:

 'xy' 

也是一个完全合法的字符常量,但其值(仍然是inttypes)是实现定义的。 这几乎是无用的语言function,主要是为了造成混淆。

铿锵的开发者发布了一些很棒的例子 ,每个C程序员都应该阅读。 以前没有提到的一些有趣的:

  • 有符号整数溢出 – 不包括超过最大值的有符号variables。
  • 解引用NULL指针 – 是的,这是未定义的,可能会被忽略,请参阅链接的第2部分。

EE在这里刚刚发现一个>> – 2有点令人担忧。

我点点头,告诉他们这不是很自然。

在使用它们之前,一定要始终初始化variables! 当我刚刚开始使用C时,这让我头痛不已。

使用“max”或“isupper”等函数的macros版本。 macros评估他们的参数两次,所以当你调用max(++ i,j)或isupper(* p ++)时,你会得到意想不到的副作用,

以上是标准C.在C ++中,这些问题已经基本消失了。 最大function现在是模板function。

忘记添加static float foo(); 在头文件中,只有在返回0.0f时才会得到浮点exception;