ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

int比float表示的数多?从IEEE 754位模式解密数值表示差异

int比float表示的数多?从IEEE 754位模式解密数值表示差异 如果你第一次听到“int比float表示的数多”这个说法八成会愣住float不是能存到3.4e38吗int上限才21亿出头怎么反而是int多我当年刚接触计算机组成原理时也被这个结论绕晕过后来啃完IEEE 754浮点标准、又踩了一堆精度相关的坑才算把这个事彻底想明白。其实这句话说的不是“范围大小”而是“可表示的不同数值的个数”。int虽然范围窄但它在自己的范围里每一个整数都“占住”了一个唯一的位模式float虽然范围宽可它的位模式里有一堆“不务正业”的NaN、无穷大剩下的有限个数还要不均匀地撒在数轴上。这篇文章就把这件事拆开讲透顺便把int和float在日常编程里那些经典误用、报错、转换问题一起捋一遍适合正在学C/C、搞嵌入式PLC、做GIS数据处理或者纯粹被浮点精度坑过的朋友。1. 先看结论int的“数量”为什么赢在起点1.1 int的32位每一位都是有效数字不管是int还是float在一台32位标准机器上内存里都是32个bit也就是2^32种排列组合。关键在于int把这2^32个位模式全部有效利用了。int通常用补码表示整数。32位补码能表示的整数范围是-2^31到2^31-1也就是-2147483648到2147483647。每一个位模式比如0x00000000、0xFFFFFFFF都对应一个确定的整数不存在某个位模式是“非法整数”或“特殊整数”的情况。也就是说int的可表示数量不多不少就是2^32个不同值。这个“全部有效”听上去理所当然但对比一下float你就知道这不是所有类型都能做到的。int就像一个把每一寸土地都种上庄稼的农场没有任何浪费。1.2 float的32位被拆成了三个部分float用的不是补码而是IEEE 754单精度浮点格式。这32个bit被拆成三块1位符号位S决定正负。8位指数位E决定量级采用偏移127存储。23位尾数位M决定有效数字。一个规约形式的float数值计算公式是value (-1)^S × 1.M × 2^(E-127)这里的1.M是尾数部分隐含了一位整数位的1所以尾数其实是24位精度只是最高位固定为1不用存。这个设计很巧妙但也埋下了“浪费位模式”的种子。因为指数位E并不只是0到255的普通整数它被分配了额外的语义E全为1时表示无穷大和NaNE全为0时表示次正规数和±0。这些特殊位模式是int里根本不存在的。1.3 float浪费了多少位模式NaN、无穷、±0我直接给你算一笔账这笔账算完你就能明白“int数量多”的数学底气无穷大指数位全1尾数位全0。符号位有2种可能所以有2个位模式表示±∞。NaNNot a Number指数位全1尾数位不全为0。23位尾数中非0情况有2^23-1种乘以符号位2种一共2 × (2^23-1) 16777214个位模式表示NaN。非有限值总计2 16777214 16777216也就是2^24个位模式。float总共只有2^32个位模式减掉这2^24个非有限值剩下的有限值位模式数量是2^32 - 2^24 4294967296 - 16777216 4278190080而int是2^32 4294967296个位模式。所以从“位模式所表示的不同值”这个角度比int确实比float多了1600多万个“名额”。如果你再较真一点float里还有0和-0两个位模式表示的数值都是0如果按“不同数值”来数float能表示的有限数值还要再减掉1个差距更大。不过大家日常说的“表示的数多”通常按位模式算也就是浮点数集合的基数比int少。1.4 结论数量比的是“不同值有几个”不是谁更大很多人一听到“float比int范围大”马上会以为float能表示的数一定更多。这其实是把“范围”和“数量”混为一谈了。范围是数轴上的跨度数量是刻度的密度。int在-21亿到21亿这4.29亿个“格子”里全部填充float虽然覆盖到±10^38但它只有大约4.278亿个“格子”还要分布在一段巨大无比的区间里必然导致大批整数是空的。所以在严格意义上“int比float表示的数多”这个说法是有数学依据的int的可表示值总数是2^32float的有限可表示值总数是2^32 - 2^24。更精确地说int不仅数量多而且在整数集合范围内每个整数都能精确表示float则不行。2. 范围大、数却少浮点的密度陷阱2.1 float范围大的原因指数位数带来的巨大跨度先不纠结“数量”回到float的强项范围。8位指数位带有符号偏移实际指数范围大约是-126到127还有-126到127的规约范围以及次正规数所以float能表示的最大值约为(1 (1 - 2^-23)) × 2^127 ≈ 3.4e38最小值绝对值最小的规约数约为1.18e-38再加上次正规数还能到1.4e-45。相比之下32位int最大值只有2^31-1 ≈ 2.15e9连float能表示的最大值的零头都不到。这就是为什么在科学计算、物理模拟、天文数据里float、double这种浮点类型不可替代。你不可能用int去存一个6.02e23这种量级的数值但浮点轻轻松松就能存下来。2.2 float不连续相邻两个float的间隔越来越大但“能存下”不等于“存得准”。float在数轴上不是均匀分布的相邻两个float数值之间的间隔取决于当前指数的大小。在1附近float的精度大约是2^-23也就是约1.19e-7两个相邻float间隔很小。但到了2^24这个数量级相邻float的间隔已经变成了2。到了2^30这个数量级两个相邻float的间隔是2^(30-23)128。你可以把float想象成一把“变距刻度尺”靠近0的地方刻度密密麻麻越往两边刻度越疏。而int是一把“等距刻度尺”从-2147483648到2147483647每个整数都是刻度一个不多一个不少。等距尺虽然量程小但在自己的量程内一个刻度都没空着变距尺量程大可大量整数在稀疏区间里根本没有对应刻度。2.3 鸽笼原理为什么必然存在“数不全”的整数鸽笼原理告诉我们如果你有4.2亿个笼子却要把它们分配到从负3.4e38到正3.4e38这个区间里平均每个笼子要覆盖大约10^29的区间宽度。即使指数分布让靠近0的地方密集但也必然存在大片区域里一个float“代表”了很多很多个理论上的整数。最经典的例子就是16777217能不能用float精确表示答案是不能。为什么是16777217这个数因为float的尾数部分有24位有效精度隐含一位1所以它最多能精确区分2^2416777216个连续整数。超过这个数以后能精确表示的整数就只剩偶数了。16777217用float存会被四舍五入成16777216.0或者在某些舍入模式下变成16777218.0。你用printf(%.0f, f)打出来就是16777216。这就是“float虽然范围大但大于2^24的整数并不都能表示”的典型例子。2.4 写个代码验证一下float从16777216开始“数数”理论说再多不如跑一段代码直观。看这个C程序#include cstdio int main() { float f 16777216.0f; int count 0; while (count 5) { std::printf(f %.0f\n, f); f f 1.0f; count; } return 0; }我实测的输出是f 16777216 f 16777216 f 16777216 f 16777216 f 16777216连续加五次1float纹丝不动。因为这里每加1结果都落在两个相邻float的正中间舍入后还是原来的数。你如果改一下循环让f每次加2.0f就能看到从16777216跳到16777218。这个实验告诉我们float的“单位1”在小数区域很正常但在大数区域早就失效了。你用int做同样的循环从16777216加1加1会老老实实一个一个走。这是两者最本质的“性格”差异int是连续整数计数器float是带刻度的科学记数法。3. 从常见误用看int和float的类型差异3.1 double和float的区别到底在哪聊完float和int的区别很多人又会问那double是什么double本质就是“更宽的float”。double占64位其中1位符号位、11位指数位、52位尾数位。它的范围能到约1.8e308精度也更高因为尾数有53位有效精度含隐含位所以能精确表示2^53以内的所有整数。这个精度对于大多数工程计算是足够的。实际选型时要注意如果只是存个数、做简单运算float可能就够了而且省一半内存。如果做科学计算、数值模拟尤其是误差累积很敏感的场景优先double。嵌入式、GPU计算、图像处理这些对存储带宽敏感的地方float更有优势。涉及金融计算时float和double都不行最好用十进制定点类型这是另一个话题。另外有一个很贴近日常的细节C/C里浮点字面量默认是double。你写float f 3.14;编译器通常会给个警告“从double转换到float可能丢失精度”。要避免这个警告就在数字后面加f写成3.14f。这就是热词里“float为什么加f”的标准答案不加f它是个double赋给float是“降级”加了f才明确告诉编译器“我要的就是float”。3.2 GIS、PLC场景里的char、float、int、time/real类型这个词不只出现在C语言。搞GIS和PLC的人天天都在和char、int、float、real打交道。先说GIS属性表。一个要素类的属性字段需要明确指定类型char定长字符串存名称、代码、地类编码。int整数存人口、面积统计值、数量。float/double带小数的测量值如坐标、长度、温度。time/date时间字段存采集时间、历史记录时间。GIS里最容易踩的坑是用float字段存地物ID。我见过有人把要素唯一ID设成float最后查属性表时ID对不上因为大ID被float舍入成了近似值。正确做法是ID用整数类型int或long数值型字段用浮点。另外GIS里做面积计算、长度计算结果默认是double如果你把它赋给float字段也会发生“降级”要注意精度损失。PLC可编程逻辑控制器里的类型也类似。西门子、AB等PLC平台里INT16位有符号整数范围-32768到32767。存计数、工位号、开关量对应数值。REAL32位浮点数范围很大存模拟量、PID参数、温度线性值。DINT/LREAL对应32位整数、64位浮点数。PLC里如果你要把模拟量采样值拿来累加比如累计运行时间或能耗直接用REAL累加是有误差的。比较好的做法是整数部分和浮点部分分开处理或者用DINT存总脉冲只在最终显示时转成REAL。另外PLC的INT和REAL之间做比较时尽量不要写IF INT_VALUE REAL_VALUE THEN因为隐式转换和舍入会让结果不符合预期。3.3 C里int转QString的正确姿势热词里有个“int转qstring”这其实是Qt开发里非常常见的操作。很多新手以为可以像C语言一样用(QString)n强转结果编译报错或者运行乱码。Qt里正确做法是int n 42; // 方法1QString::number QString s1 QString::number(n); // 方法2QString::arg QString s2 QString(%1).arg(n); // 方法3如果还要带格式可以用asprintf QString s3 QString::asprintf(%d, n);QString::number是最直接的还支持进制参数比如QString::number(n, 16)能转成十六进制字符串。arg适合拼模板字符串比如当前值: %1。注意arg的重载在Qt5/Qt6里有很多版本传int时要小心别误调成double重载。对比一下C标准库的做法#include string int n 42; std::string s std::to_string(n);这两种方式都行但如果你想在非Qt标准C里做格式化std::to_string简单粗暴。唯一要注意的是负数std::to_string(-42)会得到std::string类型的-42没有额外空格这点不用担心。3.4 enum、int和函数指针的那些恩恩怨怨热词里还有“c int enum报错”这个也是常见的类型系统问题。在C11之前枚举enum本质上就是一个整数可以隐式转换成int也可以和int互相赋值。enum Color { Red, Green, Blue }; Color c Red; int x c; // 旧C允许C11也允许隐式转int但从C11引入了enum class强枚举情况变了enum class Color { Red, Green, Blue }; Color c Color::Red; int x static_castint(c); // 必须强转如果在工程里混用了两种枚举很容易出现cannot convert Color to int之类的报错。我的建议是新代码尽量用enum class需要整数时显式static_castint这样能避免很多隐式转换带来的奇怪bug。再一个热词是invalid conversion from void (*)() to int [-fpermissive]。这个报错我第一次看到是在一个学生问我为什么main函数的return 0会坏。后来发现真正的原因通常是有人把函数名写成了普通变量#include stdio.h void func() {} int main() { int a func; // 错误函数名退化成函数指针不能赋给int return 0; }编译时GCC会给出像invalid conversion from void (*)() to int这样的错误。在C语言里函数名可以退化成函数指针但函数指针不能随便转换成一个整数。如果你确实想把地址整数化比如做调试分析应该用uintptr_t而不是int#include stdint.h void func() {} int main() { uintptr_t p reinterpret_castuintptr_t(func); return 0; }当然大多数场景下你根本不需要这么干把这个报错当成“类型不匹配”的信号就好。4. 常见报错与排查技巧实录4.1 大一新生第一个C语言程序为什么int和printf配合得这么好热词里那句“大一新生第一个c语言程序”其实是个很标准的入门代码#include stdio.h int main() { printf(hello world! 我是大一新生c语言环境部署成功啦\n); return 0; }这个程序看起来简单但已经涉及int和字符串的重要差异printf的第一个参数是格式化字符串const char*后面跟的参数由格式占位符决定。比如%d对应int%f对应double%s对应char*。新手最容易踩的坑就是把int和字符串直接混在一起。比如int age 18; printf(age %d\n, age); // 正确 printf(age age); // 错误这里是把指针加整数第二个写法在C语言里实际上是指针算术会把字符串起始地址往后移动18个字节年轻人不要模仿。另外return 0;返回的是int给操作系统常用来表示程序正常退出。如果一个函数声明为int main()却写return;编译器会报错。这正好对应热词里那个invalid conversion的坑函数返回类型和实际返回值类型不一致编译器当然要翻脸。4.2 Python的str and int幂运算报错该怎么解Python是动态类型但类型不匹配照样会炸。热词里那个报错a 6 ** 2运行时会抛TypeError: unsupported operand type(s) for ** or pow(): str and int原因很简单字符串类型不支持幂运算。Python不会自动帮你把字符串“6”转成数值6它要求你明确转换。解决方案是a int(6) ** 2 # a 36同理pow(6, 2)也会报一样的错用pow(int(6), 2)才能正确执行。这个报错也提醒我们动态语言里“隐式转换”不是万能的。相比之下JavaScript里6 ** 2会输出36因为JS做了隐式转换。两种语言各有风格但你在Python里最好养成显式转换的习惯否则成千上万个数据点里混入一个字符串程序会当场丑态百出。4.3 求int类型数字长度的几种写法热词“求int类型数字长度”看起来基础但网上搜索量大说明很多人现场写不出来。我整理三种常见写法各有优劣。方法一转字符串求长度#include string int n 12345; int len std::to_string(n).size(); // len 5这个写法最简单但要注意负数。std::to_string(-12345)的结果是-12345长度为6如果你想要的是数字位数包含负号还是不含需要根据业务场景处理。方法二循环除以10int countDigits(int n) { if (n 0) return 1; int count 0; int x abs(n); while (x 0) { x / 10; count; } return count; }这个写法不依赖字符串性能好适合嵌入式、PLC固件这类环境。但要注意abs(INT_MIN)可能会溢出更稳妥的做法是用long long临时变量。方法三用log10#include cmath int n 12345; int len (int)log10(abs(n)) 1;log10(12345)约等于4.09取整加1得到5非常快。但同样要处理n0的特例以及INT_MIN的绝对值问题。浮点函数还可能有极小的误差所以更推荐循环或转字符串。4.4 float和int比较、判断相等时千万别偷懒最后一个常见坑是浮点比较。热词里没直接提“float和real比较”但工程上这问题躲不开。比如float a 0.1f 0.2f; if (a 0.3f) { // 你以为为true实际上大概率是false }为什么因为0.1和0.2在二进制里都是无限循环小数float存的只是近似值两个近似值相加的结果和0.3的近似值并不相等。正确做法是判断两者差值是否小于某个很小的阈值#include cmath float a 0.1f 0.2f; if (std::fabs(a - 0.3f) 1e-6) { // 才认为相等 }PLC里用REAL做接近判断也一样不能写REAL_VAR 1.5而应该用两个REAL做差后看绝对值是否小于一个工程允许误差。至于int和float直接比较int_var float_var时int_var会被转换成float再比较这在变量值较大的时候会出问题。比如int n 16777217; float f 16777216.0f; if (n f) { // 结果为true因为n被转成float后也成了16777216.0 }这种坑隐蔽性极强。所以在混合比较前先想清楚这个值有没有超过2^24有没有可能因为舍入导致相等我一般建议在int和float混用时能统一成long long就统一不能统一就用epsilon比较别图省事。5. 最后聊点个人体会我做C/C这些年最深的体会是类型系统不是故意为难人而是每种类型都有自己的“视图”。int擅长精确计数float擅长表示大跨度的物理量你让int去算天体距离它活不下去你让float去数内存地址它能数到怀疑人生。所以“int比float表示的数多”这句话真正有点醒作用的地方在于它逼着你去想“类型到底存储的是什么”。是确切的整数还是带误差的近似实数是每一个位模式都有效还是一大堆位模式其实在表示NaN和无穷想通了这个你自然就明白为什么浮点不能随便用为什么大整数要用int而不是float为什么16777217会变成16777216.0。最后再分享一个小技巧以后遇到任何关于int、float、double的困惑直接把它当成“这是怎么用bit编码的”问题去推导。int就是二进制补码float就是符号位、指数位、尾数位。用自己的脑子从头算一遍比死记“float精度是6到7位有效数字”要扎实得多。算过几次之后像“float为什么加f”“int转QString为什么不能强转”“GIS里为什么ID不能用float”这类问题就都成了顺理成章的事。
返回列表