C语言中未指定行为的一个示例是对函数自变量的求值顺序。 您可能不知道它可能是从左到右或从右到左。 这将影响foo(c++, c)或foo(++c, c)的评估方式。
还有哪些其他未指定的行为可以使无意识的程序员感到惊讶?
语言律师的问题。嗯
我的个人top3:
违反严格的别名规则
违反严格的别名规则
违反严格的别名规则
:-)
编辑这是一个小示例,它两次出错:
(假设32位整数和小尾数)
1 2 3 4 5 6
| float funky_float_abs (float a)
{
unsigned int temp = *(unsigned int *)&a;
temp &= 0x7fffffff;
return *(float *)&temp;
} |
该代码试图通过在浮点数表示中直接与符号位进行位旋转来获取浮点数的绝对值。
但是,通过从一种类型转换为另一种类型来创建指向对象的指针的结果不是有效的C。编译器可能会假定指向不同类型的指针没有指向同一块内存。这对于除void *和char *之外的所有类型的指针都是正确的(符号无关紧要)。
在上面的例子中,我做了两次。一次获取float的整数别名,一次将值转换回float。
有三种有效的方法可以做到这一点。
在转换过程中使用char或void指针。这些总是别名,所以它们是安全的。
1 2 3 4 5 6 7 8
| 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指针,因此也会强制使用别名。
1 2 3 4 5 6 7 8 9
| 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以来,这显然不是未定义的:
1 2 3 4 5 6 7 8 9 10 11 12
| 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)可能已被优化与x具有签名类型一样,总是错误的:请参见GCC中的-fstrict-overflow选项)。
-
" /","。" #include中的" .."和" .."没有定义的含义,可以由不同的编译器以不同的方式处理(这实际上有所不同,如果出错,将会毁了您的一天)。
真正严重的行为,即使在您开发的平台上也可能令人惊讶,因为行为只是部分未定义/未指定:
而且,正如我认为Nils提到的那样:
用指向某物的指针划分某物。只是由于某种原因不会编译... :-)
我最喜欢的是:
1 2
| // what does this do?
x = x++; |
为了回答一些意见,根据标准,它是未定义的行为。看到这一点,编译器就可以做任何事情,包括格式化硬盘。
例如,在此处查看此评论。关键不是您可以看到对某些行为的合理预期。由于C ++标准和序列点的定义方式,因此这行代码实际上是未定义的行为。
例如,如果在上一行之前有x = 1,那么之后的有效结果是什么?有人评论说应该
x is incremented by 1
因此我们之后应该看到x == 2。但是,实际上这不是真的,您会发现某些编译器之后的x == 1,甚至x ==3。您必须仔细查看生成的程序集,以了解可能的原因,但是存在差异潜在的问题。本质上,我认为这是因为允许编译器以它喜欢的任何顺序评估两个赋值语句,因此可以先执行x++或首先执行x =。
我遇到的另一个问题(已定义,但绝对是意外的)。
炭是邪恶的。
-
有符号的还是无符号的,取决于编译器的感觉
-
未强制为8位
我无法计算已更正printf格式说明符以匹配其参数的次数。任何不匹配都是未定义的行为。
-
不可以,您不能将int(或long)传递给%x-需要unsigned int
-
不可以,您不能将unsigned int传递给%d-必须输入int
-
不,您不得将size_t传递给%u或%d-使用%zu
-
不,您不能使用%d或%x打印指针-使用%p并强制转换为void *
如果函数原型不可用,则编译器不必告诉您您正在使用错误的参数数量/错误的参数类型来调用函数。
我已经看到许多相对缺乏经验的程序员被多字符常量所咬住。
这个:
是字符串文字(其类型为char[2],在大多数情况下会衰减为char*)。
这个:
是一个普通字符常量(出于历史原因,其类型为int)。
这个:
也是一个完全合法的字符常量,但是其值(仍为int类型)是实现定义的。这是一种几乎没有用的语言功能,主要用于引起混乱。
lang的开发人员前不久发布了一些很棒的示例,每个C程序员都应该阅读该示例。一些以前没有提到的有趣的东西:
-
有符号整数溢出-不可以将有符号变量包装成超过其最大值。
-
取消引用NULL指针-是的,这是未定义的,可能会被忽略,请参阅链接的第2部分。
EE在这里刚刚发现a >>-2有点烦人。
我点点头,告诉他们那是不自然的。
使用变量之前,请务必始终对其进行初始化!当我刚开始使用C时,这使我头痛不已。