72

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

还有哪些其他未指明的行为会让不知情的程序员感到惊讶?

4

11 回答 11

85

语言律师问题。嗯。

我的个人top3:

  1. 违反严格的别名规则

  2. 违反严格的别名规则

  3. 违反严格的别名规则

    :-)

编辑这里有一个小例子,它做错了两次:

(假设 32 位整数和小端)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

该代码试图通过直接在浮点表示中对符号位进行位旋转来获得浮点的绝对值。

但是,通过从一种类型转换为另一种类型来创建指向对象的指针的结果是无效的 C。编译器可能会假设指向不同类型的指针不指向同一块内存。这适用于除 void* 和 char* 之外的所有类型的指针(符号无关紧要)。

在上述情况下,我这样做了两次。一次获取浮点 a 的 int-alias,一次将值转换回浮点数。

有三种有效的方法可以做到这一点。

在强制转换期间使用 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;
}
于 2008-09-19T00:33:12.910 回答
32

我个人最喜欢的未定义行为是,如果非空源文件未以换行符结尾,则行为未定义。

我怀疑这是真的,尽管我见过的任何编译器都没有根据它是否以换行符终止来区别对待源文件,而不是发出警告。所以这并不是真正让不知情的程序员感到惊讶的事情,除了他们可能会对警告感到惊讶。

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

  • char 不一定是(未)签名的。
  • int 可以是 16 位以上的任何大小。
  • 浮点数不一定是 IEEE 格式的或符合标准的。
  • 整数类型不一定是二进制补码,整数算术溢出会导致未定义的行为(现代硬件不会崩溃,但某些编译器优化会导致与环绕不同的行为,即使这是硬件所做的。例如,if (x+1 < x)可能会优化为始终为假何时x签署类型:参见-fstrict-overflowGCC 中的选项)。
  • “/”、“.” #include 中的“..”和“..”没有明确的含义,并且可以被不同的编译器区别对待(这实际上是不同的,如果出错,它会毁了你的一天)。

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

  • POSIX 线程和 ANSI 内存模型。对内存的并发访问并没有新手想象的那么好。volatile 不符合新手的想法。内存访问的顺序并不像新手想象的那么好。访问可以在某些方向上跨内存屏障移动。不需要内存缓存一致性。

  • 分析代码并不像您想象的那么容易。如果您的测试循环无效,编译器可以删除部分或全部。inline 没有定义的效果。

而且,正如我认为 Nils 顺便提到的:

  • 违反严格的别名规则。
于 2008-09-19T01:18:39.307 回答
21

我最喜欢的是这个:

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

要回答一些评论,根据标准,这是未定义的行为。看到这一点,编译器可以做任何事情,包括格式化你的硬盘。例如,请参见此处的评论。关键不是您可以看到某些行为可能存在合理的期望。由于 C++ 标准和序列点的定义方式,这行代码实际上是未定义的行为。

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

x 增加 1

所以我们应该看到 x == 2 之后。然而这实际上不是真的,你会发现一些编译器之后有 x == 1,甚至可能有 x == 3。你必须仔细查看生成的程序集,看看为什么会这样,但差异是由于到根本问题。本质上,我认为这是因为允许编译器以它喜欢的任何顺序评估两个赋值语句,因此它可以执行第x++一个或第x =一个。

于 2008-09-19T00:33:22.240 回答
21

用指向某物的指针来划分某物。只是由于某种原因无法编译... :-)

result = x/*y;
于 2008-09-19T00:44:25.240 回答
11

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

char是邪恶的。

  • 有符号或无符号取决于编译器的感觉
  • 强制为 8 位
于 2008-09-19T07:58:06.523 回答
9

我无法计算我已更正 printf 格式说明符以匹配其参数的次数。任何不匹配都是未定义的行为

  • 不,您不能将int(或long)传递给%x-unsigned int是必需的
  • 不,您不得传递unsigned intto %d- anint是必需的
  • 不,您不能将 a 传递size_t%u%d- 使用%zu
  • 不,您不能使用%dor打印指针%x- 使用%p并强制转换为void *
于 2013-08-15T19:31:17.837 回答
8

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

于 2008-09-19T02:40:37.893 回答
8

我见过很多相对缺乏经验的程序员被多字符常量所困扰。

这:

"x"

是一个字符串文字(在大多数情况下它是类型char[2]并衰减为)。char*

这:

'x'

是一个普通的字符常量(由于历史原因,它的类型是int)。

这:

'xy'

也是一个完全合法的字符常量,但它的值(仍然是 type int)是实现定义的。这是一个几乎无用的语言功能,主要用于引起混淆。

于 2013-08-15T20:19:06.437 回答
5

不久前,clang 开发人员在每个 C 程序员都应该阅读的帖子中发布了一些很好的示例。一些之前没有提到的有趣的:

  • 有符号整数溢出 - 不,将有符号变量包装超过其最大值是不行的。
  • 取消引用 NULL 指针 - 是的,这是未定义的,可能会被忽略,请参阅链接的第 2 部分。
于 2012-02-01T22:18:51.200 回答
2

EE 刚刚发现 a>>-2 有点令人担忧。

我点点头,告诉他们这不自然。

于 2008-09-19T02:44:23.097 回答
1

请务必在使用变量之前始终初始化它们!当我刚开始使用 C 语言时,这让我很头疼。

于 2008-09-19T00:32:23.570 回答