使用格式说明符进行转换

时间:2019-08-22 12:39:35

标签: c type-conversion

当我们使用格式说明符打印数据时,我无法推断机器内部的情况。

我试图理解有符号和无符号整数的概念,发现以下内容:

unsigned int b=-12;  
printf("%d\n",b);     //prints -12
printf("%u\n\n",b);   //prints 4294967284

我猜测b实际上将-12的二进制版本存储为11111111111111111111111111110110。

因此,由于b是无符号的,因此b在技术上存储4294967284。 但是格式说明符%d仍然导致b的二进制值作为其有符号版本即-12打印。

但是

printf("%f\n",2);    //prints 0.000000
printf("%f\n",100);   //prints 0.000000
printf("%d\n",3.2);    //prints 2147483639

printf("%d\n",3.1);    //prints 2147483637

根据类型转换规范,我有点希望2将打印为2.00000,而3.2将打印为3。

为什么不发生这种情况以及在计算机级别上发生了什么?

3 个答案:

答案 0 :(得分:2)

格式说明符和参数类型不匹配(例如使用浮点说明符"%f"打印int值)会导致不确定的行为

请记住,2是一个整数值,而vararg函数(例如printf)实际上并不知道参数的类型。 printf函数必须依靠格式说明符来假定为指定类型的参数。


要更好地了解您如何获得结果,并了解“内部事件”,我们首先必须做出两个假设:

  • 系统为int类型使用32位
  • 系统为double类型使用64位

现在

会发生什么
printf("%f\n",2);    //prints 0.000000

printf函数看到了"%f"说明符,并以64位double的值获取下一个参数。由于您在参数列表中提供的int值只有32位,因此double值中的一半位将是未知的。然后,printf函数将打印(无效)double值。如果您不走运,某些未知位可能会导致该值成为陷阱值,这可能会导致崩溃。

类似

printf("%d\n",3.2);    //prints 2147483639

printf函数获取下一个参数作为32位int值,而丢失作为实际参数提供的64位double值中的一半位。究竟将哪32位复制到内部int值取决于endianness。整数没有陷阱值,因此不会发生崩溃,只会打印出意外的值。

答案 1 :(得分:0)

  

究竟在计算机级别发生了什么?

stdio.h函数与计算机级别相差很远。它们在各种OS API之上提供了标准化的抽象层。而“机器级别”将指代生成的汇编器。您遇到的行为主要与C语言而不是机器有关。

在计算机级别上,不存在带符号的数字,但是所有内容都被视为原始二进制数据。编译器可以通过使用一条告诉CPU的指令将原始二进制数据转换为带符号的数字:“使用此位置存储的内容并将其视为带符号的数字”。具体来说,在所有普通计算机上作为二进制补码签名。但这在解释代码错误行为的原因时是无关紧要的。

整数常量12的类型为int。当我们编写-12时,我们在其上应用一元-运算符。结果仍为int类型,但现在为值-12

然后,您尝试将此负数存储在unsigned int中。这会触发到unsigned int的隐式转换,该转换应根据C标准进行:

  

否则,如果新类型是无符号的,则通过重复加或   比新类型可以表示的最大值多减去一个   直到该值在新类型的范围内

32位无符号整数的最大值为2^32 - 1,等于4.29*10^9 - 1。 “比最大值多一”表示4.29*10^9。如果我们计算-12 + 4.29*10^9,则会得到4294967284。这是一个无符号整数的范围,这是您稍后看到的结果。

现在,printf系列函数非常不安全。如果提供的错误格式说明符与类型不匹配,则它们可能会崩溃或显示错误的结果,等等-程序将调用未定义的行为。

因此,当您使用%d%i保留用于有符号的int,但传递无符号的int时,任何事情都会发生。 “任何内容”都包括编译器试图转换传递的类型以匹配传递的格式说明符。这就是您使用%d时发生的情况。

当您传递与格式说明符完全不匹配的类型值时,该程序只是打印出乱码。因为您仍在调用未定义的行为。

  

根据类型转换规范,我有点希望2将打印为2.00000,而3.2将打印为3。

printf系列之所以不能做任何聪明的事情,例如假设2应该转换为2.0,是因为它们是可变参数(可变参数)。意味着他们可以接受任意数量的参数。为了使之成为可能,参数实际上是作为原始二进制文件通过称为va_list的东西传递的,所有类型信息都将丢失。因此,printf实现中没有类型信息,只有给您的格式字符串。这就是为什么可变参数函数使用起来如此不安全的原因。

与具有更多类型安全性的常规函数​​不同-如果声明void foo (float f)并传递整数常量2(类型int),它将尝试从整数隐式转换为浮点型,并且可能还会发出转化警告。

答案 2 :(得分:0)

您观察到的行为是printf将赋予它的位解释为格式说明符指定的类型的结果。特别是,至少对于您的系统而言:

  • 参数列表内相同位置的int参数和unsigned参数的位将在同一位置传递,因此当您给printf一个并告诉它时格式化另一个文件时,它会使用您赋予它的位,就好像它们是另一个位一样。
  • int参数和double参数的位将在不同的地方传递-可能是int参数的通用寄存器和{的特殊浮点寄存器{1}}参数,所以当您给double一个参数并告诉它格式化另一个参数时,它不会获得printf的位用于double;它会得到之前操作遗留下来的完全无关的位。

每当调用一个函数时,其参数的值都必须放在某些位置。这些位置随所使用的软件和硬件而异,并且随参数的类型和数量而异。但是,对于任何特定的参数类型,参数位置以及使用的特定软件和硬件,都有一个特定的位置(或位置组合),该参数的位应存储在该位置以传递给函数。规则是所用软件和硬件的应用程序二进制接口(ABI)的一部分。

首先,让我们忽略任何编译器优化或转换,并检查当编译器直接将源代码中的函数调用作为汇编语言中的函数调用实现时会发生什么情况。编译器将采用您为int提供的参数,并将其写入针对这些类型的参数指定的位置。执行printf时,它将检查格式字符串。当它看到格式说明符时,便会确定其应具有的参数类型,并在该类型参数的位置中查找该参数的值。

现在,可能会发生两件事。假设您传递了printf,但为unsigned使用了格式说明符,例如int。在我所看到的每个ABI中,一个%d和一个unsigned自变量(在自变量列表中的相同位置)都在同一位置传递。因此,当int在寻找printf的位时,它将获得您传递的int的位。

然后unsigned会将这些位解释为好像它们对printf的值进行了编码,并将打印结果。换句话说,您的int值的位将重新解释为unsigned的位。 1

这说明了为什么将int值4,294,967,284传递给unsigned以使用printf进行格式化时会看到“ -12”的原因。当位11111111111111111111111111111100被解释为%d时,它们表示值4,294,967,284。当它们被解释为unsigned时,它们表示系统上的值-12。 (这种编码系统被称为二进制补码。其他编码系统包括二进制补码和正负号,其中这些位分别表示-1和-2,147,483,636。这些系统如今在纯整数类型中很少见。)

这是可能发生的两件事中的第一件事,当您传递错误的类型时很常见,但它在大小和性质上都与正确的类型相似-它在错误的位置处传递。可能发生的第二件事是,您传递的参数在与期望的参数不同的地方传递。例如,如果将int作为参数传递,则在许多系统中,它将其放置在用于浮点值的单独的寄存器集中。当double寻找printf的{​​{1}}参数时,它将根本找不到int的位。取而代之的是,它在寻找%d自变量的地方找到的内容可能是前一次操作在寄存器或存储器位置中剩余的任何位,或者可能是列表中下一个自变量的位论据。无论如何,这意味着为double打印的值int与您传递的printf值无关,因为%d的位不是以任何方式涉及–使用完整的不同位集。

这也是C标准表示未为double转换传递错误的参数类型时定义行为的部分原因。一旦通过传递double应该放在的printf弄乱了参数列表,以下所有参数也可能放在错误的位置。它们可能与期望值位于不同的寄存器中,或者可能与期望值位于不同的堆栈位置。 double无法从这个错误中恢复过来。

如上所述,以上所有内容都忽略了编译器优化。 C的规则来自于各种需求,例如适应上述问题并使C可移植到各种系统中。但是,一旦编写了这些规则,编译器便可以利用它们进行优化。只要更改后的程序在C标准的规则下具有与原始程序相同的行为,C标准就允许编译器对程序进行任何转换。此权限使编译器在某些情况下可以极大地加速程序。但是结果是,如果您的程序具有C标准未定义的行为(并且编译器未遵循其他任何规则定义),则可以将您的程序转换为任何内容。多年来,编译器在优化方面变得越来越积极,并且还在继续增长。这意味着,除了上述简单的行为之外,当您将错误的参数传递给int时,允许编译器产生完全不同的结果。因此,尽管您可能会经常看到我上面描述的行为,但您可能并不依赖它们。

脚注

1 请注意,这不是转换。转换是一种操作,其输入是一种类型,而输出是另一种类型,但是具有相同的值(或在某种意义上,几乎等于可能,就像我们将printf 3.5转换为{{ 1}} 3)。在某些情况下,转换不需要对位进行任何更改-printf 3和double 3使用相同的位来表示3,因此转换不会更改位,结果与重新解释相同。但是它们在概念上是不同的。