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

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

有帮助吗?

解决方案

语言律师问题。嗯凯。

我个人的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* 之外的所有类型的指针都是如此(符号性并不重要)。

在上面的例子中,我这样做了两次。一次为 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;
}

其他提示

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

我怀疑这是真的,尽管我见过的编译器都没有根据源文件是否换行符来不同地处理它,除了发出警告之外。因此,这并不是真正会让不知情的程序员感到惊讶的事情,除了他们可能会对警告感到惊讶之外。

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

  • char 不一定是(未)签名的。
  • int 可以是 16 位以上的任意大小。
  • 浮点数不一定是 IEEE 格式或符合 IEEE 格式。
  • 整数类型不一定是二进制补码,并且整数算术溢出会导致未定义的行为(现代硬件不会崩溃,但某些编译器优化将导致与环绕不同的行为,即使这就是硬件所做的。例如 if (x+1 < x) 可能会被优化为总是错误的 x 已签名类型:看 -fstrict-overflow GCC 中的选项)。
  • “/”,“。”。和“ ..”在#include中没有定义的含义,可以通过不同的编译器对待不同的对待(实际上确实有所不同,如果出错,它将破坏您的一天)。

即使在您开发的平台上,也可能会令人惊讶,因为行为只是部分未定义/未指定:

  • POSIX 线程和 ANSI 内存模型。对内存的并发访问并不像新手想象的那么明确。挥发性并不像新手想象的那样。内存访问的顺序并不像新手想象的那么明确。访问权限 朝某些方向跨越记忆障碍。不需要内存缓存一致性。

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

而且,正如我认为尼尔斯顺便提到的:

  • 违反严格的别名规则。

将某物除以指向某物的指针。只是由于某种原因无法编译...:-)

result = x/*y;

我最喜欢的是这个:

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

回答一些评论,根据标准,这是未定义的行为。看到这一点,编译器就可以执行任何操作,包括格式化硬盘。参见示例 这条评论在这里. 。重点不在于您可以看到对某些行为可能有合理的期望。由于 C++ 标准和序列点的定义方式,这行代码实际上是未定义的行为。

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

x 加 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 *

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

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

这:

"x"

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

这:

'x'

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

这:

'xy'

也是一个完全合法的字符常量,但它的值(仍然是类型 int) 是实现定义的。这是一个几乎无用的语言功能,主要是造成混乱。

clang 开发者发布了一些 很好的例子 不久前,一篇文章是每个 C 程序员都应该阅读的。一些之前没有提到的有趣的:

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

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

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

请务必在使用变量之前对其进行初始化!当我刚开始接触 C 语言时,这让我很头疼。

使用“max”或“isupper”等函数的宏版本。宏对它们的参数求值两次,因此当您调用 max(++i, j) 或 isupper(*p++) 时,您会得到意想不到的副作用

以上是针对标准C。在 C++ 中,这些问题基本上消失了。max 函数现在是一个模板函数。

忘记添加 static float foo(); 在头文件中,仅在返回 0.0f 时抛出浮点异常;

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top