C语言中未指定的行为示例是函数参数的求值顺序。它可能是从左到右或者从右到左,你不知道。这会影响foo(c++, c)
或 foo(++c, c)
的求值方式。
还有哪些未指定的行为会让不知情的程序员感到惊讶?
C语言中未指定的行为示例是函数参数的求值顺序。它可能是从左到右或者从右到左,你不知道。这会影响foo(c++, c)
或 foo(++c, c)
的求值方式。
还有哪些未指定的行为会让不知情的程序员感到惊讶?
一个语言律师的问题。好的。
我个人的前三:
违反了严格别名规则
违反了严格别名规则
违反了严格别名规则
:-)
编辑 这里有一个做错两次的小例子:
(假设32位整数和小端)
float funky_float_abs (float a)
{
unsigned int temp = *(unsigned int *)&a;
temp &= 0x7fffffff;
return *(float *)&temp;
}
该代码通过位处理浮点数的符号位来获取浮点数的绝对值。
然而,通过将一个类型强制转换为另一个类型来创建对象的指针是无效的C语言。编译器可能会认为不同类型的指针没有指向同一块存储器。这对所有类型的指针都适用,除了void*和char*(符号无关)。
在上面的情况中,我做了两次。一次是为了获得float a的int别名,一次是将值转换回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。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;
}
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;
}
temp[3] &= 0x7f;
时,它不会改变从函数返回的 temp_float
中的任何内容(temp_float
是一个副本,当你更改原始对象时它不能改变)。那么你认为这个函数是否达到了预期的效果? - Nawaz我的个人最喜欢的未定义行为是:如果一个非空源文件没有以换行符结尾,则其行为是未定义的。
我怀疑,除了发出警告之外,我所见过的编译器都没有按照源文件是否以换行符结尾来进行不同的处理。因此,这并不是一些不知情的程序员会感到惊讶的事情,除非他们会对警告感到惊讶。
因此,针对真正的可移植性问题(这些问题大多是与实现相关而不是未指定或未定义的,但我认为这符合问题的精神):
真正严重的问题可能会令人惊讶,即使在您开发的平台上,因为行为只部分为未定义或未指定:
POSIX线程和ANSI内存模型。并发访问内存没有像新手想象的那么好定义。易失性并不是新手想象的那样。内存访问的顺序并没有像新手想象的那么好定义。在某些方向上,访问可以跨越内存障碍移动。内存缓存一致性不是必需的。
代码调优并不像你想象的那么简单。如果您的测试循环没有效果,编译器可以删除部分或全部内容。 inline没有定义的效果。
而我认为尼尔斯顺便提到了:
我最喜欢的是这个:
// what does this do?
x = x++;
回答一些评论,根据标准它是未定义的行为。看到这一点,编译器可以做任何事情,甚至包括格式化您的硬盘。
例如,请参见此处的评论。重点不在于您可以看到某些行为可能会有合理的期望。由于C++标准和定义序列点的方式,这行代码实际上是未定义的行为。
例如,如果在上面的那行之前我们有x = 1
,那么之后的有效结果将是什么?有人评论说应该是
x增加了1
所以之后我们应该看到x == 2。然而事实并非如此,您将会发现有些编译器之后会出现 x == 1,或者甚至 x == 3。要想知道为什么会出现这种差异,您必须仔细查看生成的汇编代码,但这些差异源于底层问题。本质上,我认为这是因为编译器允许按任意顺序评估两个赋值语句,因此它可以先执行x ++
,或者先执行x =
。
将某个值除以指向该值的指针。出于某种原因,这段代码无法编译... :-)
result = x/*y;
double x = 2; int z = 1, *y; y = &z; int result = x / *y;
- 这个答案需要被彻底编辑或紧急删除。-1 - RobertS supports Monica Cellio我已经无法数清有多少次我为了匹配printf中的格式说明符而进行更正。 任何不匹配的操作都是未定义的行为。
int
(或long
)传递给%x
- 必须使用unsigned int
unsigned int
传递给%d
- 必须使用int
size_t
传递给%u
或 %d
- 应该使用%zu
%d
或 %x
打印指针 - 应该使用%p
并强制转换为void *
int
类型的值给 %x
,或者传递 unsigned int
类型的值给 %d
,只要这些值在两种类型的范围内是可以的。不过,我更喜欢避免这样做。 - Keith Thompson我遇到的另一个问题(已定义,但绝对是意想不到的)。
char类型很难处理。
char
、unsigned char
和signed char
。它们是明确不同的类型。 - Lstorchar
。许多标准库函数(如所有的str*()函数)都需要指向char的指针,给它们其他任何东西都需要丑陋的转换。 - Jens我见过很多经验不足的程序员因为多字符常量而受到影响。
这个例子:
"x"
这是一个字符串字面量(类型为char[2]
,在大多数情况下会衰减为char*
)。
以下是示例:
'x'
这是一个普通的字符常量(由于历史原因,它的类型是int
)。
下面是示例:
'xy'
也是一个完全合法的字符常量,但它的值(仍为int
类型)是由实现定义的。这几乎是一个无用的语言特性,主要是造成混淆。
'????'
。 - supercat如果函数原型不可用,编译器在调用带有错误参数数量/错误参数类型的函数时不一定会告诉您。
一段时间前,clang开发者发布了一篇每个C程序员都应该阅读的文章,其中包含一些很棒的例子。以下是一些之前未提到的有趣例子:
这里的EE们刚刚发现a>>-2有些棘手。
我点了点头并告诉他们这是不自然的。
foo(c++, c)
和foo(++c, c)
都是未定义行为,这比未指定的情况更严重。 - Pascal Cuoq