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

时间:2020-03-06 14:24:10  来源:igfitidea点击:

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

还有哪些其他未说明的行为可能会使无意识的程序员感到惊讶?

解决方案

使用变量之前,请务必始终对其进行初始化!当我刚开始使用C时,这使我头疼不已。

语言律师的问题。嗯

我的个人top3:

  • 违反严格的混叠规则
  • 违反严格的混叠规则
  • 违反严格的别名规则:-)

编辑这是一个小示例,它两次都会出错:

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

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

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

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

在上面的例子中,我做了两次。一次获取浮点数a的整数别名,一次将值转换回浮点数。

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

在转换过程中使用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;
}

我最喜欢的是:

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

为了回答一些意见,根据标准,它是未定义的行为。看到这一点,编译器就可以做任何事情,包括格式化硬盘。
例如,在此处查看此评论。关键不是我们可以看到对某些行为的合理预期。由于C ++标准和序列点的定义方式,因此这行代码实际上是未定义的行为。

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

x is incremented by 1

因此我们之后应该看到x == 2. 但是,实际上这不是真的,我们会发现某些编译器之后的x == 1,甚至x ==3. 我们必须仔细查看生成的程序集,以了解可能的原因,但由于不同潜在的问题。本质上,我认为这是因为允许编译器以它喜欢的任何顺序评估两个赋值语句,因此它可以先执行x ++或者首先执行x =。

使用宏版本的功能,例如" max"或者" isupper"。宏会对它们的参数进行两次评估,因此,当我们调用max(++ i,j)或者isupper(* p ++)时,我们将获得意外的副作用

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

忘记在头文件中添加" static float foo();",只是想获取返回0.0f时引发的浮点异常;

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

result = x/*y;

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

我怀疑这是真的,尽管我不会见过任何编译器会根据是否以换行符结尾来区别源文件,而不是发出警告。因此,除了可能使警告感到惊讶之外,这实际上不会使不知情的程序员感到惊讶。

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

  • char不一定是(未)签名的。
  • int可以是16位中的任何大小。
  • 浮点数不一定是IEEE格式或者一致的。
  • 整数类型不一定是二进制补码,并且整数算术溢出会导致未定义的行为(现代硬件不会崩溃,但是某些编译器优化会导致行为不同于环绕操作,即使这是硬件所做的操作。例如,if(x + 1当x具有签名类型时,<x)可能会被优化为始终为假:请参见GCC中的-fstrict-overflow选项。
  • " /","。" #include中的" .."和" .."没有定义的含义,可以由不同的编译器以不同的方式处理(这实际上有所不同,如果出错,将会毁了一天)。

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

  • POSIX线程和ANSI内存模型。并发访问内存的定义不如新手想象的那样。 volatile不会像新手那样做。内存访问的顺序没有新手认为的那样明确。可以在某些方向上跨存储屏障移动访问。不需要内存缓存一致性。
  • 分析代码并不像我们想的那么容易。如果测试循环无效,则编译器可以删除其中的一部分或者全部。内联没有定义的效果。

而且,正如我认为Nils提到的那样:

  • 违反严格的别名规则。

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

EE在这里刚刚发现a >>-2有点烦人。

我点点头,告诉他们那是不自然的。

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

炭是邪恶的。

  • 有符号的还是无符号的,取决于编译器的感觉
  • 未强制为8位