你遇到的 C 语言常见的未定义/未指定的行为是什么?

C 语言中未指定行为的一个例子是函数参数求值的顺序。可能是从左到右,也可能是从右到左,你不知道。这将影响如何评估 foo(c++, c)foo(++c, c)

还有什么其他未指定的行为会让不知情的程序员感到惊讶呢?

16925 次浏览

确保在使用变量之前总是初始化它们!当我刚开始学习 C 语言的时候,它给我带来了许多头疼的问题。

一个语言律师的问题,好吧。

我的个人看法3:

  1. 违反了严格的别名规则

  2. 违反了严格的别名规则

  3. 违反了严格的别名规则

    :-)

编辑 这里有一个错误两次的小例子:

(假设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 接受 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;
}

我最喜欢的是:

// what does this do?
x = x++;

回答一些评论,这是未定义的行为根据标准。看到这一点,编译器被允许做任何事情,包括格式化你的硬盘驱动器。 参见例子 这里的评论。问题的关键不在于你能看到对某些行为存在一种可能的合理预期。由于 C + + 标准和序列点的定义方式,这行代码实际上是未定义的行为。

例如,如果我们在上面的行之前有 x = 1,那么之后的有效结果是什么?有人评论说应该是

X 增加1

所以我们应该看到 x = = 2。然而,这实际上并不正确,您会发现一些编译器后面有 x = = 1,或者甚至 x = = 3。您必须仔细查看生成的程序集,以了解为什么会出现这种情况,但差异是由潜在的问题造成的。本质上,我认为这是因为编译器允许按照它喜欢的顺序计算两个赋值语句,所以它可以先执行 x++,或者先执行 x =

用指针除法,但由于某些原因无法编译... : -)

result = x/*y;

我个人最喜欢的未定义行为是,如果一个非空源文件没有以换行结束,行为就是未定义的。

我怀疑这是真的,虽然没有编译器,我将永远看到有不同的处理源文件根据是否换行结束,除了发出警告。所以这并不会让不知情的程序员感到惊讶,除了他们可能会对这个警告感到惊讶之外。

因此,对于真正的可移植性问题(这些问题大多依赖于实现,而不是未指定或未定义的,但我认为这符合问题的精神) :

  • Char 不一定(非)签名。
  • Int 可以是16位的任意大小。
  • 浮动不一定是 IEEE 格式的或符合。
  • 整数类型不一定是 two 的补充,整数算术溢出会导致未定义的行为(现代硬件不会崩溃,但是一些编译器优化会导致不同于包装的行为,即使这是硬件所做的。例如,当 x具有签名类型时,if (x+1 < x)可以优化为始终为 false: 请参见 GCC 中的 -fstrict-overflow选项)。
  • "/","."以及”。."在 # include 中没有定义的含义,可以被不同的编译器区别对待(这实际上是不同的,如果它出错了,它会毁了你的一天)。

真正严重的问题,甚至在你开发的平台上也会令人惊讶,因为行为只是部分未定义/未指明:

  • POSIX 线程和 ANSI 内存模型。并发访问内存并不像初学者想象的那样定义得那么好。挥发性不会像新手想的那样。内存访问的顺序并不像新手认为的那样明确。访问 可以可以跨越内存屏障向某些方向移动。不需要内存缓存一致性。

  • 分析代码并不像您想象的那样简单。如果测试循环没有效果,编译器可以删除部分或全部测试循环。内联没有确定的效果。

正如尼尔斯顺便提到的:

  • 违反了严格的别名规定。

如果函数原型不可用,编译器不必告诉您正在使用错误的参数数量/错误的参数类型调用函数。

EE 在这里刚刚发现 >-2有点令人担忧。

我点点头,告诉他们这不正常。

我遇到的另一个问题(已定义,但绝对出乎意料)。

Char 是邪恶的。

  • 有符号或无符号取决于编译器的感觉
  • Not 强制为8位

Clang 开发人员不久前发布了一些 很好的例子,这是每个 C 程序员都应该读到的文章。一些以前没有提到的有趣的事情:

  • 有符号整数溢出-不,有符号变量超过其最大值是不可以的。
  • 取消对 NULL 指针的引用-是的,这是未定义的,可能会被忽略,参见链接的第2部分。

我无法计算为了匹配参数而更正 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类型)是实现定义的。这是一个几乎无用的语言特性,主要用于引起混淆。