[英]Use of format specifiers for conversions
当我们使用格式说明符打印数据时,我无法推断出机器内部的内部情况。
我试图理解有符号和无符号整数的概念,发现以下内容:
unsigned int b=-12;
printf("%d\n",b); //prints -12
printf("%u\n\n",b); //prints 4294967284
我猜测b实际上将-12的二进制版本存储为11111111111111111111111111110100。
因此,由于b是无符号的,因此b在技术上存储4294967284。但是格式说明符%d仍然导致b的二进制值作为其有符号版本i,e -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。
为什么没有发生这种情况,以及在机器级别发生了什么?
格式说明符和参数类型不匹配(例如使用浮点说明符"%f"
打印int
值)会导致未定义的行为 。
请记住, 2
是整数值,而vararg函数(例如printf
)并不真正知道参数的类型。 printf
函数必须依靠格式说明符来假定参数为指定的类型。
为了更好地了解您如何获得结果,了解“内部事件”,我们首先必须做出两个假设:
int
类型 double
型 现在发生了什么
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
值取决于字节序 。 整数没有陷阱值,因此不会发生崩溃,只会打印出意外的值。
在机器级别究竟发生了什么?
stdio.h函数与计算机级别相差很远。 它们在各种OS API之上提供了标准化的抽象层。 而“机器级别”将指代生成的汇编器。 您遇到的行为主要与C语言而不是机器有关。
在计算机级别上,不存在带符号的数字,但是所有内容都被视为原始二进制数据。 编译器可以通过使用一条告诉CPU的指令将原始二进制数据转换为带符号的数字:“使用此位置存储的内容并将其视为带符号的数字”。 具体来说,在所有普通计算机上作为二进制补码签名。 但这在解释代码错误行为的原因时是无关紧要的。
整数常量12
的类型为int
。 当我们写-12
我们应用一元-
对运营商。 结果仍然是int
类型,但现在值为-12
。
然后,您尝试将此负数存储在unsigned int
。 这将触发对unsigned int
的隐式转换,应根据C标准执行此转换:
否则,如果新类型是无符号的,则通过重复添加或减去比新类型可表示的最大值多一个值来转换该值,直到该值在新类型的范围内为止
32位无符号int的最大值是2^32 - 1
4.29*10^9 - 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
类型),它将尝试从整数隐式转换为float,并可能还会发出转换警告。
您观察到的行为是printf
将赋予它的位解释为格式说明符指定的类型的结果。 特别是,至少对于您的系统:
int
参数和unsigned
参数的位将在同一位置传递,因此,当您给printf
一个位并告诉它格式化另一个位时,它将使用您给它的位,就好像它们是另一半。 int
参数和一个double
参数的位将在不同的地方传递-可能是一个int
参数的通用寄存器,一个用于double
参数的特殊浮点寄存器,所以当您给printf
一个并告诉它格式化另一个格式时,它没有获得用于int
的double
的位; 它获得了之前操作遗留下来的完全无关的位。 每当调用函数时,其参数的值都必须放在某些位置。 这些位置随所使用的软件和硬件而异,并且随参数的类型和数量而异。 但是,对于任何特定的参数类型,参数位置以及使用的特定软件和硬件,都有一个特定的位置(或位置组合),该参数的位应存储在该位置以传递给函数。 规则是所用软件和硬件的应用程序二进制接口(ABI)的一部分。
首先,让我们忽略编译器的任何优化或转换,并检查当编译器直接将源代码中的函数调用实现为汇编语言中的函数调用时会发生什么情况。 编译器将采用您为printf
提供的参数,并将其写入为这些类型的参数指定的位置。 当printf
执行时,它检查格式字符串。 当看到格式说明符时,它将找出应具有的参数类型,并在该参数类型的位置查找该参数的值 。
现在,可能发生两件事。 假设您传递了一个unsigned
但为int
使用了格式说明符,如%d
。 在我所看到的每个ABI中,一个unsigned
和一个int
参数(在参数列表中的相同位置)都在同一位置传递。 因此,当printf
寻找期望的int
位时,它将获得您传递的unsigned
位。
然后printf
将解释这些位,就好像它们对int
的值进行了编码一样,它将打印结果。 换句话说, unsigned
值的位将重新解释为int
的位。 1个
这解释了为什么在将unsigned
值4,294,967,284传递给printf
以使用%d
进行格式化时看到“ -12”的原因。 当位11111111111111111111111111111100被解释为unsigned
,它们表示值4,294,967,284。 当将它们解释为int
,它们表示系统上的值-12。 (此编码系统称为二进制补码。其他编码系统包括二进制补码和正负号,其中这些位分别代表-1和-2,147,483,636。这些系统如今在纯整数类型中很少见。)
那是可能发生的两件事中的第一件事,当您传递错误的类型时很常见,但是它在大小和性质上都与正确的类型相似-它在错误的位置处传递。 可能发生的第二件事是,您传递的参数在与期望的参数不同的地方传递。 例如,如果将double
用作参数,则在许多系统中,将其放在单独的一组浮点值寄存器中。 当printf
寻找%d
的int
参数时,它将根本找不到double
的位。 取而代之的是,它在查找int
参数的位置发现的内容可能是前一次操作在寄存器或存储器位置中剩下的任何位,或者可能是参数列表中下一个参数的位。 无论如何,这意味着%d
printf
打印值将与您传递的double
值无关,因为double
的位没有任何关系-使用了完全不同的位集。
这也是C标准表示在为printf
转换传递错误的参数类型时未定义行为的部分原因。 一旦通过将int
应当放在的double
弄乱了参数列表,那么以下所有参数也可能位于错误的位置。 它们可能与期望值位于不同的寄存器中,或者可能与期望值位于不同的堆栈位置。 printf
无法从此错误中恢复。
如上所述,以上所有内容都忽略了编译器优化。 C的规则来自于各种需求,例如适应上述问题并使C可移植到各种系统中。 但是,一旦编写了这些规则,编译器便可以利用它们进行优化。 只要更改后的程序在C标准的规则下具有与原始程序相同的行为,C标准就允许编译器对程序进行任何转换 。 此权限使编译器在某些情况下可以极大地加速程序。 但是结果是,如果您的程序具有C标准未定义的行为(并且编译器遵循的任何其他规则也未定义),则可以将程序转换为任何东西 。 多年来,编译器在优化方面变得越来越积极,并且还在继续增长。 这意味着,除了上述简单的行为之外,当您将不正确的参数传递给printf
,允许编译器产生完全不同的结果。 因此,尽管您通常可以看到我上面描述的行为,但是您可能并不依赖它们。
1请注意,这不是转换 。 转换是一种操作,其输入是一种类型,而输出是另一种类型,但具有相同的值(或在某种意义上,几乎等于可能,就像我们将double
3.5转换为int
3时一样)。 在某些情况下,转换不需要对位进行任何更改- unsigned
3和int
3使用相同的位表示3,因此该转换不会更改位,其结果与重新解释相同。 但是它们在概念上是不同的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.