为什么未定义的行为如此一致?

时间:2016-10-13 00:23:34

标签: c pointers printf undefined-behavior

我一直在玩指针,不小心输入错误的参数printf

#include <stdio.h>

int
main (void)
{
  double * p1;
  double * p2; 
  double d1, d2;

  d1 = 1.2345;
  d2 = 2.3456;
  p1 = &d1;
  p2 = &d2;

  printf ("p1=%p\n, *p1=%g\n", (void *)p1, *p1);
  printf ("p2=%p\n, *p2=%g\n", (void *)p2,  p2); /* third argument should be *p2 */

  return 0;
}

输出

  

警告:格式'%g'需要类型为'double'的参数,但参数3的类型为'double *'   p1 = 0x7ffc9aec46b8,* p1 = 1.2345
  p2 = 0x7ffc9aec46c0,* p2 = 1.2345

为什么在这种情况下p2的输出始终等于*p1的输出?

我使用gcc(v5.4.0)编译器及其C(gnu11)的默认标准。

6 个答案:

答案 0 :(得分:6)

调用未定义行为的代码可以执行任何操作 - 这就是未定义的原因。

也就是说,人们可以很好地猜测为什么在使用你的特定编译器 你的特定机器上这个特定的东西 正是您使用的选项并在一年的同一个工作日以6 编译,您明白了,对吧?它的未定义,即使您认为自己知道所有变量,也可以依赖无解释。有一天,湿度下降,或者其他什么,你的程序可能决定做一些不同的事情。即使没有重新编译。甚至在同一循环的两次迭代中。这就是未定义的行为。

无论如何,在您的平台上,浮点参数可能在专用浮点寄存器(或专用浮点堆栈)中传递,而不是在主堆栈上传递。 printf(&#34;%g&#34;)需要一个浮点参数,因此它在浮点寄存器中查找。但你没有在浮点寄存器中传递任何东西;所有你传递的都是两个指针参数,它们都在堆栈上(或指针参数去的地方;这也超出了C标准的范围)。因此,第二次printf调用会在上次加载时获取该特定浮点寄存器中的垃圾。碰巧的是,在最后一次printf调用中,您加载到该寄存器中的最后一件事是*p1的值,因此该值会被重用。

确定(以及其他内容)函数参数的位置的规则,因此函数知道在哪里查找它们统称为调用约定。您可能正在使用x86或衍生产品,因此您可能会发现Wikipedia page on x86 calling conventions很有趣。但是如果你想知道你的编译器在做什么,请让它发出汇编语言(gcc -S)。

答案 1 :(得分:4)

定义 - 这就是重点。您所看到的可能是旧值保留在用于传递浮点参数的任何寄存器中的结果。

答案 2 :(得分:3)

  

未定义的行为 - 对行为没有任何限制   程序。未定义行为的示例是外部的内存访问   数组边界,有符号整数溢出,空指针取消引用,   在表达式中多次修改相同的标量   没有序列点,通过指针访问对象   不同的类型等。编译器不需要诊断undefined   行为(虽然许多简单的情况被诊断出来),而且   编译程序不需要做任何有意义的事情。1

未定义的行为并不意味着随机行为,而是“未被标准”行为所涵盖。所以它可能是实现者选择用它做的任何事情。

该标准指定UB,因为它允许编译优化,否则可能无法实现。

答案 3 :(得分:3)

在语言层面,这种研究通常没什么价值。

但是,一种可能的实际情况可能如下所示:

编译器使用不同的传递约定(内存区域,堆栈,寄存器)来传递不同类型的参数。指针以一种方式传递(例如,CPU堆栈),而double值以不同的方式传递(例如,FPU寄存器堆栈)。你传了一个指针,但告诉printf它是doubleprintf进入该区域以传递double s(例如,FPU寄存器堆栈的顶部)并读取之前printf调用留在那里的“垃圾”值。

答案 4 :(得分:0)

其他答案涵盖了未定义的行为。这是一篇有趣的文章,描述为什么在C中存在如此多的未定义行为,以及可能带来的好处。这不是因为K&amp; R很懒惰或者不在乎。

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

简而言之,未定义的行为和实现定义的行为可以为优化提供机会,并在不同平台上实现更高效的实现。

答案 5 :(得分:0)

C语言在C89标准发布之前就已广泛使用,标准的作者并不希望合规编译器无法完成现有编译器可以像他们已经做的那样高效地完成的所有工作。 。如果要求所有编译器实现某些行为会使某些编译器不适合其任务,那么这将是保持行为未定义的理由。即使该行为在99%的平台上有用且常用,但标准的作者认为没有理由相信保留未定义的行为会影响到这一点。如果编译器编写者认为在任何标准强制要求之前的日子里支持行为是实际和有用的,那么就没有理由期望他们需要授权来维持这种支持。可以在关于将短无符号整数类型提升为签名的基本原理中找到该视图的证据。

不知何故,一种奇怪的观点认为,一切都必须由标准规定或不可预测。该标准描述了未定义行为的常见后果,1989年最常见的一个原因是实现将以实现的文档时尚特征表现。

如果您的实现指定了将浮点值传递给可变参数函数的方法,并且它使用的方法是创建临时值并传递其地址,那么代码的行为可能在特定的实现上定义,在这种情况下,它的工作原理就不足为奇了。如果实现以这种方式处理参数但是没有足够好地记录它们以保证行为,那么实现的行为不会受到缺少文档的影响就不足为奇了。