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

Ben*_*oit 63 c undefined-behavior language-lawyer unspecified-behavior

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

还有什么其他未指明的行为可以让不知情的程序员感到惊讶?

Nil*_*nck 72

语言律师问题.Hmkay.

我的个人top3:

  1. 违反严格的别名规则
  2. 违反严格的别名规则
  3. 违反严格的别名规则

    :-)

编辑这是一个错误的例子:

(假设32位整数和小尾数)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}
Run Code Online (Sandbox Code Playgroud)

该代码试图通过在float的表示中直接与符号位进行比特来获得浮点数的绝对值.

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

在上面的例子中,我做了两次.一旦获得float a的int-alias,并且一次将值转换回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;
}
Run Code Online (Sandbox Code Playgroud)

使用memcopy.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;
}
Run Code Online (Sandbox Code Playgroud)

第三种有效方式:使用工会.从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;
}
Run Code Online (Sandbox Code Playgroud)

  • C99 标准允许通过联合进行类型双关;见脚注 82,它是随 TC3 添加的:“如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则该值的对象表示的适当部分被重新解释为新类型中的对象表示,如 6.2.6 中所述(有时称为“类型双关”的过程)。这可能是陷阱表示。” (3认同)

Ste*_*sop 30

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

我怀疑这是真的,虽然没有编译器我会看到根据是否是换行符来处理源文件,而不是发出警告.因此,除了他们可能会对警告感到惊讶之外,这并不会让那些不知情的程序员感到惊讶.

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

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

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

  • POSIX线程和ANSI内存模型.对新内存的并发访问并没有像新手那样明确.volatile不会做新手的想法.内存访问顺序没有新手想象的那么明确.访问可以在某些方向上跨越记忆障碍.内存缓存一致性不是必需的.

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

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

  • 违反严厉的混乱规则.


Ada*_*rce 22

用指针划分东西.只是不会因某种原因编译... :-)

result = x/*y;
Run Code Online (Sandbox Code Playgroud)

  • 这是什么令人难以置信的搞笑和错误的答案?它根本没有回答这个问题,并且提出了 C 代码的错误假设。给定的代码是语法错误。它与未定义的行为无关。- 也许您打算将基本类型的值除以指针值,但这不是您所显示的。将基本类型的值除以取消引用的指针并不正确,例如:`double x = 2; int z = 1,*y;y = &amp;z; int result = x / *y;` - 该答案需要进行彻底编辑或紧急删除。-1 (3认同)

180*_*ION 19

我最喜欢的是:

// what does this do?
x = x++;
Run Code Online (Sandbox Code Playgroud)

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

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

x增加1

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

  • 在两个序列点之间多次修改变量在标准C和C++中明确表示为未定义的行为. (16认同)
  • 我正在开玩笑,想到有人在编写一个C编译器时会看到x = x ++,因为它在标准中是未定义的,因此格式化你的硬盘:-) (10认同)
  • +1,特别是"格式化硬盘部分".实际上,对于那些像这样编码的人来说,格式化硬盘可能会为后代的维护程序员带来很多悲痛...... (4认同)

itj*_*itj 10

我遇到的另一个问题(已定义,但绝对是意外).

char是邪恶的.

  • 签名或未签名取决于编译器的感受
  • 强制为8位

  • 好吧,如果你把它用于它的意思,那就不是邪恶的,即*字符*...... (3认同)
  • 实际上,有三种*不同*类型的char:`char`,`unsigned char`和`signed char`.它们是明确不同的类型. (2认同)

Jen*_*ens 8

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

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

  • 标准意味着(在非规范性脚注中)只要值在两种类型的范围内,将"int"传递给"%x"或"unsigned int"传递给"%d"即可.不过,我宁愿避免它. (3认同)

mba*_*768 7

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


Per*_*son 6

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

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


Kei*_*son 6

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

这个:

"x"
Run Code Online (Sandbox Code Playgroud)

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

这个:

'x'
Run Code Online (Sandbox Code Playgroud)

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

这个:

'xy'
Run Code Online (Sandbox Code Playgroud)

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

  • 问题是关于C,而不是C++.没有重载功能. (3认同)