C遇到的常见的未定义/未指定的行为是什么?
C语言中未指定行为的一个例子是函数参数的评估顺序。 它可能是从左到右,从右到左,你只是不知道。 这会影响foo(c++, c)
或foo(++c, c)
的评估。
还有什么其他不明确的行为可以让不知道的程序员感到惊讶?
一个语言律师的问题。 Hmkay。
我个人top3:
- 违反严格的走样规则
- 违反严格的走样规则
-
违反严格的走样规则
🙂
编辑这里有一个小错误两次的例子:
(假定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'
是一个普通的字符常量(由于历史原因,它是int
types的)。
这个:
'xy'
也是一个完全合法的字符常量,但其值(仍然是int
types)是实现定义的。 这几乎是无用的语言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;