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

时间:2008-09-19 00:30:10

标签: c compiler-construction

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

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

13 个答案:

答案 0 :(得分:72)

语言律师问题。 Hmkay。

我的个人top3:

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

    : - )

  4. 编辑以下是两个错误的例子:

    (假设32位整数和小端)

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

    该代码试图通过直接在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;
    }
    

    使用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;
    }
    

    第三种有效方式:使用工会。由于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;
    }
    

答案 1 :(得分:30)

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

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

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

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

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

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

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

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

  • 违反严厉的混乱规则。

答案 2 :(得分:22)

用指针划分东西。只是因为某些原因不能编译...: - )

result = x/*y;

答案 3 :(得分:19)

我最喜欢的是:

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

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

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

  

x增加1

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

答案 4 :(得分:10)

我遇到的另一个问题(已定义但非常意外)。

char是邪恶的。

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

答案 5 :(得分:8)

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

  • 不,您不能将int(或long)传递给%x - 需要unsigned int
  • 不,您不能将unsigned int传递给%d - 需要int
  • 不,您不能将size_t传递给%u%d - 使用%zu
  • 不,您不得使用%d%x打印指针 - 使用%p并投射到void *

答案 6 :(得分:7)

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

答案 7 :(得分:6)

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

此:

"x"

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

此:

'x'

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

此:

'xy'

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

答案 8 :(得分:4)

clang开发人员在一段时间内发布了一些great examples,在每个C程序员应该阅读的帖子中。以前没有提到的一些有趣的:

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

答案 9 :(得分:2)

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

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

答案 10 :(得分:1)

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

答案 11 :(得分:0)

使用“max”或“isupper”等功能的宏版本。宏两次评估它们的参数,因此当你调用max(++ i,j)或isupper(* p ++)时会出现意想不到的副作用

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

答案 12 :(得分:-1)

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