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; }
该代码试图通过在float的表示中直接与符号位进行比特来获得浮点数的绝对值.
但是,通过从一种类型转换为另一种类型来创建指向对象的指针的结果是无效的C.编译器可能会假设指向不同类型的指针不指向同一块内存.对于除void*和char*之外的所有类型的指针都是如此(符号无关紧要).
在上面的例子中,我做了两次.一旦获得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; }
我个人最喜欢的未定义行为是,如果非空源文件不以换行符结尾,则行为未定义.
我怀疑这是真的,虽然没有编译器我会看到根据是否是换行符来处理源文件,而不是发出警告.因此,除了他们可能会对警告感到惊讶之外,这并不会让那些不知情的程序员感到惊讶.
因此,对于真正的可移植性问题(主要是依赖于实现而不是未指定或未定义,但我认为这属于问题的精神):
char不一定(未)签名.
int可以是16位的任何大小.
浮点数不一定是IEEE格式的或符合标准的.
整数类型不一定是二进制补码,整数算术溢出会导致未定义的行为(现代硬件不会崩溃,但是一些编译器优化会导致行为与环绕行为不同,即使这是硬件的行为.例如,if (x+1 < x)
可能会优化为始终为false当x
已经签署类型:看到-fstrict-overflow
在GCC选项).
"/","." #include中的".."没有明确的含义,不同的编译器可以区别对待(这确实有所不同,如果出错,它将毁了你的一天).
即使在您开发的平台上也可能令人惊讶的非常严重的,因为行为只是部分未定义/未指定:
POSIX线程和ANSI内存模型.对新内存的并发访问并没有像新手那样明确.volatile不会做新手的想法.内存访问顺序没有新手想象的那么明确.访问可以在某些方向上跨越记忆障碍.内存缓存一致性不是必需的.
分析代码并不像您想象的那么容易.如果您的测试循环无效,编译器可以删除其中的部分或全部.内联没有明确的效果.
而且,正如我认为尼尔斯顺便提到的那样:
违反严厉的混乱规则.
用指针划分东西.只是不会因某种原因编译... :-)
result = x/*y;
我最喜欢的是:
// what does this do? x = x++;
要回答一些评论,根据标准,它是未定义的行为.看到这一点,编译器可以执行任何操作,包括格式化硬盘驱动器.例如,请参阅此评论.关键不在于您可以看到某种行为可能存在合理的期望.由于C++标准和定义序列点的方式,这行代码实际上是未定义的行为.
例如,如果我们x = 1
在上面的行之前,那么之后的有效结果是什么?有人评论说它应该是
x增加1
所以我们之后应该看x == 2.然而事实并非如此,你会发现一些编译器之后有x == 1,或者甚至可能是x == 3.你必须仔细查看生成的程序集,看看为什么会这样,但差异到期了对潜在的问题.本质上,我认为这是因为允许编译器以它喜欢的任何顺序评估两个赋值语句,因此它可以执行第x++
一个或第x =
一个.
我遇到的另一个问题(已定义,但绝对是意外).
char是邪恶的.
签名或未签名取决于编译器的感受
不强制为8位
我无法计算我更正printf格式说明符以匹配其参数的次数.任何不匹配都是未定义的行为.
不,你不能将int
(或long
)传递给%x
- unsigned int
是必需的
不,你不能传递unsigned int
给%d
- 一个int
是必需的
不,你不能传递size_t
给%u
或%d
使用%zu
不,你不能用%d
或打印指针%x
- 使用%p
并转换为void *
如果函数原型不可用,编译器不必告诉您正在调用具有错误数量的参数/错误参数类型的函数.
我见过很多相对缺乏经验的程序员被多字符常量所困.
这个:
"x"
是一个字符串文字(在大多数情况下是类型char[2]
并衰减char*
).
这个:
'x'
是一个普通的字符常量(由于历史原因,它是类型int
).
这个:
'xy'
也是一个完全合法的字符常量,但它的值(仍然是类型int
)是实现定义的.这是一种几乎无用的语言功能,主要是为了引起混乱.