C语言中未指定行为的一个示例是对函数自变量的求值顺序。您可能不知道它可能是从左到右或从右到左。这将如何影响foo(c++, c)
或foo(++c, c)
获取评估。
还有哪些其他未说明的行为可能会使无意识的程序员感到惊讶?
Answers:
语言律师的问题。嗯
我的个人top3:
违反严格的别名规则
违反严格的别名规则
违反严格的别名规则
:-)
编辑这是一个小示例,它两次出错:
(假设32位整数和小尾数)
float funky_float_abs (float a)
{
unsigned int temp = *(unsigned int *)&a;
temp &= 0x7fffffff;
return *(float *)&temp;
}
该代码试图通过在浮点数表示中直接与符号位进行位旋转来获取浮点数的绝对值。
但是,通过从一种类型转换为另一种类型来创建指向对象的指针的结果不是有效的C。编译器可能会假定指向不同类型的指针没有指向同一块内存。这对于除void *和char *之外的所有类型的指针都是正确的(符号无关紧要)。
在上面的例子中,我做了两次。一次获取float的整数别名,一次将值转换回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;
}
使用内存复制。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;
}
我个人最喜欢的未定义行为是,如果非空源文件未以换行符结尾,则行为未定义。
我怀疑这是真的,尽管我不会见过任何编译器会根据是否以换行符结尾来区别源文件,而不是发出警告。因此,除了警告可能会令他们惊讶之外,这实际上不会使不知情的程序员感到惊讶。
因此,对于真正的可移植性问题(主要是依赖于实现的,而不是未指定或未定义的,但我认为这属于问题的实质):
if (x+1 < x)
可能会始终错误地进行优化何时x
签署类型:请参阅-fstrict-overflow
GCC中的选项)。真正严重的行为,甚至在您开发的平台上也可能令人惊讶,因为行为只是部分未定义/未指定:
POSIX线程和ANSI内存模型。并发访问内存的定义不如新手想象的那样。volatile不会像新手那样做。内存访问的顺序没有新手认为的那样明确。可以在某些方向跨存储屏障移动访问。不需要内存缓存一致性。
分析代码并不像您想的那么容易。如果您的测试循环无效,则编译器可以删除其中的一部分或全部。内联没有定义的效果。
而且,正如我认为Nils提到的那样:
用指向某物的指针划分某物。只是由于某种原因不会编译... :-)
result = x/*y;
double x = 2; int z = 1, *y; y = &z; int result = x / *y;
-答案需要彻底编辑或紧急删除。-1
我最喜欢的是:
// what does this do?
x = x++;
为了回答一些意见,根据标准,它是未定义的行为。看到这一点,编译器就可以做任何事情,包括格式化硬盘。例如,在此处查看此评论。关键不是您可以看到对某些行为的合理预期。由于C ++标准和序列点的定义方式,这一行代码实际上是未定义的行为。
例如,如果我们x = 1
在上面的行之前,那么之后的有效结果是什么?有人评论说应该
x增加1
因此我们之后应该看到x == 2。但是,实际上并非如此,您将发现某些编译器之后的x == 1,甚至x ==3。您必须仔细查看生成的程序集,以了解可能的原因,但是差异是由于潜在的问题。本质上,我认为这是因为允许编译器以它喜欢的任何顺序评估两个赋值语句,因此它可以执行第x++
一个或第x =
一个。
我遇到的另一个问题(已定义,但绝对是意外的)。
炭是邪恶的。
char
,unsigned char
和signed char
。它们是明确不同的类型。
char
处理字符串的时候。许多标准库函数(像所有str *()函数一样)都使用了指向char的指针,而给它们提供任何其他内容都需要进行丑陋的强制转换。
如果函数原型不可用,则编译器不必告诉您您正在使用错误的参数数量/错误的参数类型来调用函数。
printf
仍然会出现问题。)
我已经看到许多相对缺乏经验的程序员被多字符常量所咬住。
这个:
"x"
是一个字符串文字(其类型为,char[2]
并衰减为char*
在大多数情况下)。
这个:
'x'
是一个普通字符常量(出于历史原因,它是类型 int
)。
这个:
'xy'
也是一个完全合法的字符常量,但是其值(仍为类型int
)是实现定义的。这是一种几乎没有用的语言功能,主要用于引起混乱。
'????'
。
EE在这里刚刚发现a >>-2有点烦人。
我点点头,告诉他们那是不自然的。
foo(c++, c)
和foo(++c, c)
都是未定义的行为,这完全胜过未指定的行为。