浮点类型字面量和变量

本文探讨了浮点数在C/C++中的表示和转换,包括浮点数转化为二进制的过程、浮点数字面量的识别和存储、浮点数比较的精度问题,以及整数与浮点数间的隐式转换。文章强调了由于精度限制,浮点数的比较和运算可能存在不准确的情况,并给出了应对策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、浮点数转化为二进制

计算机存储和处理数据最终是转化为二进制形式,之前提到过整数字面量如何转化为二进制串(包括0、正整数和正整数取反加1)。
浮点数转化为二进制有它自己的规则:
1.浮点数的整数部分,仍按照整数转二进制的规则,除2取余(直到余数为0),倒序排列
2.浮点数的小数部分,采用乘2取整(直到小数部分为0),正序排列
例如123.45整数部分和小数部分分别转化为二进制串:
在这里插入图片描述
可见,整数部分一定能完全转化为二进制串(余数总会归0),但小数部分不一定能完全转化为二进制串(小数部分不一定归0)。因此,计算机是不能完全表示浮点数的(显然当小数部分是1 / 2^n时可以完全转化,但仍要考虑存储宽度的问题,因为可能要很多位才能完全表示这个小数部分),这就涉及到表示的浮点数的精度问题。
实际上一个浮点数通过上述方法分别计算出整数部分和小数部分二进制串之后并不能直接存储到计算机中,因为这样至少要能标志小数点的位置。
实际存储到计算机中的浮点数二进制串是经过IEEE754标准生成的,二进制串包括符号位、指数部分、位数部分,具体可以参考IEEE754标准
这里我们并不关心IEEE754标准如何生成浮点数的二进制串,只需要知道它并非是整数部分和小数部分分别转换成二进制串的结果。

二、浮点数字面量

C/C++中可以直接使用带小数点的浮点数字面量,如:

1.23;
0.005;

除此之外也能以科学计数法形式来使用浮点数字面量,如:

1.23E2; // 1.23*100 = 123.0
5.2E-3; // 5.2 / 1000 = 0.0052
-3.23E1; // -3.23*10 = -32.3

浮点数类型有如下三种:

类型所占字节数
float4
double8
long double8

浮点数字面量的默认识别类型是double,可以通过F、L后缀将一个浮点数字面量转化为其他类型。
一个浮点类型字面量被识别并转换成IEEE754标准的二进制串。如果将该字面量赋值给相同类型的浮点类型变量,则直接在该变量对应的内存中存放这个二进制串,例如:

float d = 123.45F; // float 4字节

变量中存放的二进制串:
在这里插入图片描述
如果我们按照浮点类型再将其输出:

printf("%f\n",d);

实际输出的结果为123.449997,并非给定的字面量值123.45。
前面提到,并非所有浮点数都能完全转化成二进制串,这里小数部分0.45就不能完全转换(有些即使能完全转换但需要的二进制位很多,但限于计算机对变量的数据宽度有限,有时也不能完全存储),这就涉及精确度的问题。
一个不能完全转换成二进制串的浮点数,只能尽可能地接近原浮点值。这就要人工指定接近的程度即精确度。
如果使用double类型表示123.45:

double d = 123.45;	// 8字节

则输出值为123.45000000000000,显然比float类型精确程度更高。这里123.45000000000000看似是和输入的字面量123.45是相等的,但实际上二者并不真正的相等,只是double有效位数比float多。如果继续增加变量存储浮点值的位数,如:

printf("%.20f\n",d); // 将有效位数设置为20

其输出结果为:
在这里插入图片描述
显然也不是精确值。
C/C++标准中指出:

The important rule to remember is that powers of two and integer multiples thereof can be perfectly represented. everything else is an approximation.

浮点数中只有2的幂及其整数倍可以被精确表示,其他浮点数只是近似值。

三、浮点数的比较问题

一个例子:

	double d = 123.45;
	double m = 123.449999;
	if (d == m) {
		printf("%f == %f\n",d,m);
	}
	else if(d > m) {
		printf("%f > %f\n", d, m);
	}else{
		printf("%f < %f\n", d, m);
	}
	printf("%f - %f = %f\n", d, m,d-m);

此时输出结果为:
在这里插入图片描述
看似符合逻辑
但是将变量换为float类型:

	float d = 123.45;
	float m = 123.449999;

结果却不符合逻辑:
在这里插入图片描述
即使使用double类型也不一定总是得到正确的结果:

	double d = 123.449999999999999998;
	double m = 123.449999999999999999;

在这里插入图片描述
显然仍然是精度问题并且可以看出实际上比较实际存储的二进制串。受到数据宽度的影响,如果是不能精确表示的两个不相等浮点数可能在此数据宽度内存储的二进制串是相同的,就被错误地视为相等。同样地,浮点数字面量如果一个使用float存储、一个使用double存储也会导致存储的二进制串不同,被错误地认为不相等。同样地,大于、小于的比较也可能出现错误,例如

	float d = 123.45;
	double m = 123.44999999999999;

实际的比较结果是
在这里插入图片描述
不仅比较结果不符合逻辑,实际作减法的结果也不符合逻辑。
总之,浮点数的比较受表示精确度的影响,实际是比较存储二进制串。(如果是两个确定的字面量进行比较则不会出现问题,但似乎这种代码没有意义)
实际应用中,我们通常采用这样的比较方式:
1.相等的比较:只要两个浮点数差的绝对值足够小就认为二者相等,如fabs(a -b) < 1E-6认为a和b相等
2.不相等的比较,实际上上面相等的比较也用到不相等的比较。对此并没有什么有效的方法指定精度来比较。通常尽可能使用高精度的类型变量来存储。
本质上还是比较存储在计算机中的二进制串,尽量避免使用小数位数太多的浮点数进行比较、运算来尽量避免这些问题,但无法彻底解决。
在其他语言中通常也会出现这个问题,例如Python中虽然提供了“大整数”之间的运算(像2的1000次幂这样的运算可以直接运算,甚至更大的整数数据也支持),但浮点数之间的比较、运算也不能很好地处理。
比如:
在这里插入图片描述
显然也是不合理的
在很多语言中都有这样一个缺陷:
在这里插入图片描述
即一个整数减去0.00000…01(小数点后适当足够多的0)的结果仍是原来的整数。在C/C++这类在定义变量时要指定变量类型的语言中还比较容易发现这个隐患。但在像Python、javascript这类定义变量时不需要指定变量类型的语言中却容易忽略。
一个简单的场景:

import time
have_item_num = 3
use_item_num = 0.000000000000000000001
while(have_item_num > 0):
    have_item_num -= use_item_num
    print("success:have_item_num = %f" % have_item_num)
    time.sleep(1)

这个过程将一直持续下去,因为3-0.000000000000000000001的结果仍是3.0并不会减少。更危险的是如果判断

3 - 0.000000000000000000001 == 3

结果仍是True
如果have_item_num是某种游戏道具的拥有数量,而use_item_num是用户传来的使用数量,而且只要使用成功就触发相应的效果,则用户可以无限使用该道具。当然可以在客户端加以限制使之只能输入整数,但如果在实际逻辑中不加验证。总可以通过某种手段修改该数值达到上述效果。(实际上某些数据库对整数3和小数部分为0的浮点数3.0并不严格区分)
因此,对数据类型的判断和对浮点数的安全使用是非常必要的,先在只能在程序设计时在逻辑上予以注意。

四、浮点型变量

浮点类型变量有三种:float、double、long double其中float占4个字节、在64位机器上double和long double并没有区别。
最基本的是将一个浮点数字面量(识别为double类型)赋值给double或long double类型的变量,如

 double d = 123.45;

编译器会将字面量按IEEE754标准转化为8字节的二进制串存储到对应的8个字节内存中。
同样地,可以将字面量通过后缀指定为float类型并赋值给float类型的变量。如:

float f = 123.45F;

编译器会将字面量按IEEE754标准转化为4字节的二进制串存储到对应的4个字节内存中。
上面所说的是字面量赋值给相同类型变量,而float和double类型之间进行赋值时类似于不同类型整数之间赋值那样会进行隐式转换。
将float类型赋值给double类型变量:

float f = 123.45F;	// 4字节:0x 66 e6 f6 42
double dt = 123.45; // 8字节:0x cd cc cc cc cc dc 5e 40
double d = 123.45F; // 8字节:0x 00 00 00 c0 cc dc 5e 40

显然,将float类型赋值给double类型的变量时,存储的二进制串既不是将字面量转化为float类型的二进制串扩展4字节,也不是将字面量转化为double类型的值。这里损失了精度,因为这个浮点数本身就是不精确表示的,这里编译器只保证了将float字面量按double解释为二进制串但仅仅保留高4字节(float),所以仅保证高4字节与double一致,而后4字节由编译器决定。
如果将一个double类型字面量赋值给float类型变量:

float f = 123.45F;	// 4字节:0x 66 e6 f6 42
double dt = 123.45; // 8字节:0x cd cc cc cc cc dc 5e 40
float dt = 123.45; //  4字节:0x 66 e6 f6 42

可见将一个double类型字面量赋值给float类型变量时,会将这个字面量按float类型表示成二进制串存储。
不只是字面量赋值给变量,两种类型的变量之间赋值也遵循上面的规则,即:
double赋值给float会将该值按float重新解释再存储,float赋值给double会将该值按double重新解释再存储,但只保证高4字节与double相同,后4字节不一定。
同一浮点数字面量解释成double还是float二者对应的二进制串是不同的,这不是简单地用0或1扩展或者截断。

五、整数和浮点数之间的隐式转换

先考虑相同数据宽度之间的转换
1.float赋值给int:

float f = 123.45F; // 4字节:0x 66 e6 f6 42
int a = 123.45F;  //  4字节:0x 7b 00 00 00
int b = 123; // 	  4字节:0x 7b 00 00 00

结果是将float浮点数的整数部分转化为相应的int类型二进制串存储。
与float本身的二进制串并无关系,相当于直接将123赋值给int a,规则参考整数之间的赋值。
2.int赋值给float:

float f = 123;		// 4字节:0x 00 00 f6 42
int a = 123;	   //  4字节:0x 7b 00 00 00
float b = 123.0;  //   4字节:0x 00 00 f6 42

结果是将整数按照N.0转化为相应的浮点数二进制串存储,与int本身的二进制串无关,相当于直接将123.0赋值给float f,规则参考整数之间的赋值。
再考虑不同宽度之间的转换:
实际上很简单,例如123.45赋值给整数类型变量则只是将小数部分舍弃,变成相应的整数类型再赋值。而将123赋值给浮点类型变量则相当于直接将123.0赋值给变量。
整数类型和浮点数之间可以使用强制类型转换,规则与赋值时的隐式转换规则相同。
这里还要考虑一个问题:一个浮点数转化为二进制串存储可以再解释成相应的浮点数,那么如果用一个整数变量存储同样的二进制串则应该也能被解释成相等的浮点数。(参考前面提到的无差别二进制串、被解释成何值是由程序指定的而不是固定的值)例如:

float f = 123.45;		// 4字节:0x 66 e6 f6 42
int a = 0x42f6e666;	   //  4字节:0x 66 e6 f6 42

两个变量都是4字节,且存储的二进制串都是0x42f6e666,那么int类型a变量中的内容也应该能解释成与f相等的浮点数。
进行如下尝试:

printf("f = %f\n",f);
printf("a = %f\n",a);

结果为:
在这里插入图片描述
显然不同。
再尝试将a强制转换成float类型输出:

printf("f = %f\n",f);
printf("a = %f\n",(float)a);

结果为
在这里插入图片描述
a确实被转换成浮点数,但这个结果也不是我们想要的,因为 f 和 a 中存放的二进制串都是0x42f6e666并且都转化为float类型,应该是同一浮点数。
再按int类型输出a的值:

printf("f = %f\n",f);
printf("(float) a = %f\n",(float)a);
printf("(int)a = %d\n",a);

结果为
在这里插入图片描述
显然即使是强制转化为float类型,也只是将a原来的整数值1123477094转化为浮点数1123477094.0(之前提到,和将int赋值给float类型类似,只是将N.0赋值,而和int值的二进制串无关)。
那么,我们确实想要将a的二进制串解释为对应的浮点值(实际上应该如此,二进制串是无差别的,解释成何值由程序决定)应该怎样做?

float f = 123.45;		// 4字节:0x 66 e6 f6 42
int a = 0x42f6e666;	   //  4字节:0x 66 e6 f6 42
float *p = &a;		  //   获取a变量的内存起始地址
printf("f = %f\n",f);
printf("a = %f\n",*p);	//将a变量内存空间的无差别二进制串解释为float

输出结果为
在这里插入图片描述
这里是正确的结果,验证了无差别二进制串可以任意解释的特点。同时,这里使用了指针,突破了C/C++语法特点(浮点数赋值和强制转换时实际看字面值而不是二进制串),越过编译器的检查将int变量中的二进制串解释为float。这进一步说明了变量的数据类型的特征:
1.无论是定义何种类型的变量,最重要的就是它的数据宽度而不是它的类型
2.变量的数据类型只是给编译器的通知,告知编译器在处理其中数据时应该遵循该类型的某些规则(字面量的识别类型在某种程度上也是这个作用)
3.C/C++可以通过某些技术,如指针,越过变量数据类型的规定而直接操作变量中存放的无差别二进制串(读写和解释)
第三点正是C/C++语言的强大和灵活之处,指针也是C/C++的灵魂。但也正是这种灵活性,如果不能深刻理解而使用,也可能会造成很多错误,有些是明显的也有些是隐藏的错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值