【问题标题】:What are the common undefined/unspecified behavior for C that you run into? [closed]您遇到的 C 的常见未定义/未指定行为是什么? [关闭]
【发布时间】:2010-09-11 00:45:42
【问题描述】:

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

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

【问题讨论】:

  • foo(c++, c)foo(++c, c) 都是未定义的行为,完全胜过未定义的行为。

标签: c language-lawyer undefined-behavior unspecified-behavior


【解决方案1】:

语言律师问题。嗯。

我的个人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;
}

【讨论】:

  • 这听起来很有趣……你能扩展一下吗?
  • 嗯。我提到我假设 32 位整数和小端。顺便说一句 - 联合使用仍然是未定义的行为,不是因为 IEEE 位表示,而仅仅是因为(理论上)不允许您写入字段 f 并从字段 i 读取。
  • onebyone,即使实现使用 ieee,它也是未定义的行为。关键是它从上次写入的不同成员中读取。
  • csci.csusb.edu/dick/c++std/cd2/basic.html#basic.lval bullet 15 似乎暗示通过联合的类型双关语是安全的。 c 标准中的措辞是相同的。
  • C99 标准允许通过联合进行类型双关;请参阅 TC3 中添加的脚注 82:“如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分被重新解释为新类型中的对象表示,如 6.2.6 中所述(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。”
【解决方案2】:

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

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

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

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

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

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

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

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

  • 违反严格的别名规则。

【讨论】:

  • Steve - 我在 90 年代初使用 68K 系列的 Microtec 编译器遇到了您所描述的(结束换行问题)。我认为该工具有问题,但我只是添加了换行符“以解决愚蠢的工具”。与我过于自信的同事不同(请参阅我对这个主题的其他评论),我并没有那么确定我会写一份缺陷报告......幸好我没有。
  • 有符号整数溢出未定义不仅仅是迂腐;至少 GCC 在假设它永远不会发生的情况下应用优化,例如 'if (a + 1 > a)' 总是通过并且从不检测回绕。
  • @BCoates:我对产生部分不确定值的整数溢出没有任何问题,这足以证明 GCC 在指定情况下的优化是合理的。不幸的是,一些编译器编写者似乎认为整数溢出应该否定时间和因果律(如果代码在假设它不会溢出的情况下重新排序,我也许可以忍受的时间;恕我直言,否定因果关系应该被视为精神错乱,但是唉,不是每个人都同意。)
【解决方案3】:

我最喜欢的是这个:

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

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

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

x 加 1

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

【讨论】:

  • 在两个序列点之间多次修改变量在标准 C 和 C++ 中都明确表示为未定义行为。
  • 一想到有人写了一个 C 编译器,看到 x = x++ 就格式化你的硬盘驱动器,我现在要笑了,因为它在标准中是未定义的 :-)
  • +1,尤其是“格式化硬盘部分”。实际上,对于像这样编码的人来说,格式化硬盘驱动器可能会为后代的维护程序员节省很多痛苦......
  • 两件事:1)这是绝对未定义的行为;大约 15 年前,我和我小组中的某个人争论过,他写了一份缺陷报告给编译器供应商(哎呀!),当时他写了这个确切的代码(除了他使用“i”而不是“x”)并且“i”被困在1个; 2) 当我读到关于格式化硬盘驱动器的部分时我笑了,可能是因为我也会这么说。
  • 我会说 x 是递增的,然后分配它的先前值,因为 x++ 返回它并且优先于分配。但是是的,它是未定义的......语言中有很多东西(让人头疼......)
【解决方案4】:

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

result = x/*y;

【讨论】:

  • 哈哈不错,我写下来了:-)
  • 因为 '/*' 作为评论受到威胁,只需在 '/' 和 '*' 之间添加一个空格,它应该可以工作(至少它适用于我的 gcc 8.1.1)。
  • 这个令人难以置信的搞笑和错误答案是什么?它根本没有回答这个问题,并提出了 C 代码的错误假设。给定的代码是语法错误。它与未定义的行为无关。 - 也许您打算将基本类型的值除以指针值,但这不是您所展示的。将基本类型的值除以取消引用的指针并不是不正确的,例如:double x = 2; int z = 1, *y; y = &z; int result = x / *y; - 这个答案需要彻底编辑或紧急删除。 -1
【解决方案5】:

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

char 是邪恶的。

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

【讨论】:

  • 好吧,如果你将它用于它的用途,即字符...,它并不是邪恶的
  • 其实char有三种不同的类型:charunsigned charsigned char。它们是明确不同的类型。
  • 必须在处理字符串时使用(指向普通数组的指针或数组)char。许多标准库函数(就像所有的 str*() 函数一样)采用指向 char 的指针,并为它们提供其他任何东西都需要丑陋的强制转换。
  • 谁说过字符串?嵌入式程序员有时会使用可变大小来提高效率。假设任何关于 char 的东西都不能跨平台工作。调用针对字符串的库函数,但在字符串只是 char* 并且尚未发明 Unicode 时定义可能没问题,但如果我要直言不讳...不编写至少支持 unicode 字符的程序是愚蠢的
【解决方案6】:

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

  • 不,您不得将 int(或 long)传递给 %x - unsigned int 是必需的
  • 不,您不得将 unsigned int 传递给 %d - int 是必需的
  • 不,您不能将size_t 传递给%u%d - 使用%zu
  • 不,您不能打印带有%d%x 的指针 - 使用%p 并转换为void *

【讨论】:

  • 该标准暗示(在非规范性脚注中)将int 传递给%x,或将unsigned int 传递给%d,只要值在两种类型的范围。不过,我更愿意避免它。
【解决方案7】:

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

【讨论】:

  • 是的。然而,仁慈的编译器通常会帮助您发出警告......
  • 从 C99 开始,调用没有可见声明的函数需要诊断。该声明必须是原型(即,指定参数类型的声明),但它始终应该是。 (printf 之类的可变函数仍然存在问题。)
【解决方案8】:

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

这个:

"x"

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

这个:

'x'

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

这个:

'xy'

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

【讨论】:

  • 在 Macintosh 上编写 C 时很有用,Macintosh 经常使用 32 位整数来保存四个字符的文件类型、应用程序签名等,尽管三元组会很讨厌 '????' .
  • 这对于接收char*char 的重载函数尤其危险。我见过很多人被它咬过(example)
  • 问题是关于 C,而不是 C++。没有重载函数。
【解决方案9】:

clang 开发人员不久前发布了一些great examples,这是每个 C 程序员都应该阅读的帖子。一些之前没有提到的有趣的:

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

【讨论】:

    【解决方案10】:

    这里的EE刚刚发现a>>-2有点紧张。

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

    【讨论】:

      【解决方案11】:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-10-02
        • 1970-01-01
        • 1970-01-01
        • 2016-02-27
        • 1970-01-01
        • 2016-12-03
        • 2011-05-05
        相关资源
        最近更新 更多