为什么说int比float表示的数多?从二进制存储看懂数据类型的真相
前两天在技术交流群里看到一条消息“为什么说int比float表示的数多”底下瞬间吵成一团。有人说float范围能到3.4E38int撑死21亿怎么可能更少也有人说int和float都是32位位模式数量一样应该一样多。我当时没直接回因为这问题表面是比大小实际是在考你对二进制表示的理解。今天我把这件事彻底掰开顺便把和int/float相关的几个高频问题——float字面量加f、int转QString、enum报错、PLC里选INT还是REAL——都串起来聊一遍。看完你就会明白“float范围大”和“float能表示的数更多”完全是两码事。1. 先拆题int比float到底在比什么1.1 int的底层存储模型每一位都代表一个整数先看int。以最常见的32位int为例内存里就是32个bit按补码存储。最高位是符号位剩下31位表示数值。因为用了补码负数也能很自然地参与加减法不需要单独处理减法器。这32个bit一共能组合出2^32种状态也就是4,294,967,296种。每一种状态都对应一个整数从-2^31到2^31-1没有一个浪费。所以单论“能表示的不同数值的个数”int有4,294,967,296个这是一个非常干净的数字。你随便翻一个32位整数它都实实在在代表一个整数不存在“两个位模式指向同一个数”的情况。这就是整数类型最朴素也最可靠的地方位模式和数值是一一对应的。1.2 float的存储模型符号、指数、尾数凑出来的精度再看float。同样是32位但它把这32位拆成了三部分1位符号位决定正负。8位指数位用偏移127的方式表示指数范围。23位尾数位保存有效数字的二进制小数部分。规格化float的计算公式是(-1)^s * 1.M * 2^(E-127)其中M是尾数位拼出来的小数部分E是8位指数位的值。因为规格化数尾数部分隐含一个最高位的1所以实际上有效数字有24位精度只是存储时只存23位。这带来了一个结果float的“取值范围”确实很大能表示到大约3.4×10^38远超int的21亿。但“可取值范围大”不等于“能表示的数多”。float的32个bit被拆出去8位给指数剩下23位给尾数它能表达的“有效数字”就被卡死在24位二进制精度以内。换句话说float不是一台连续的数轴而是一排在数轴上越远越稀疏的点。1.3 位模式数量相同结论为什么完全不同网上很多人说“int和float都是32位所以表示的数一样多”这个说法只看了一半。位模式总数确实都是2^32但float这2^32个位模式并不是每个都代表一个“正常的实数”更不是每个都代表一个“不同的数”。我们至少要扣掉几类指数位全1、尾数全0这两个位模式表示正负无穷大。指数位全1、尾数非0这些位模式表示NaNNot a Number数量还不小。指数位全0、尾数全0正0和负0。0.0和-0.0在数值上其实是同一个数但占用了两个位模式。指数位全0、尾数非0非规格化数虽然用来补0附近的空洞但依然不是每个位模式都对应一个“普通意义上的精确整数”。把这些特殊位模式去掉后float能表示的“不同的有限实数”数量已经少于2^32了。如果再限定在“精确表示的整数”上差距就更明显int把2^32个整数全部一个不落地表示出来而float在超过2^24之后整数就开始一个、一个地丢。所以说“int比float表示的数多”指的是int作为整数计数器覆盖密度远高于floatfloat只是“范围大”但点数稀疏。2. 深入float内部为什么范围大反而数不多2.1 指数分布导致的精度变化float最反直觉的地方在于它并不是均匀分布的数轴而是越靠近0越密越往两端越疏。原因就是那个指数位。以正整数区间为例可以大致算一下float能表示的相邻两个数的间隔在[1, 2)区间指数E127尾数最小步长是1/2^23间隔约0.000000119。在[2^23, 2^24)区间间隔变成1。在[2^24, 2^25)区间间隔变成2。在[2^31, 2^32)区间间隔变成2^8256。也就是说当数值超过167772162^24之后float已经不能表示连续的整数了。到了int最大值21亿附近float的两个相邻可表示数之间隔了256。换句话说在21亿附近你随机取一个整数这个整数大概率不是float能精确表示的值它会被舍入成附近某个数。这就是“float范围大但数不多”的本质范围是靠指数撑起来的密度是靠尾数决定的指数涨一格尾数覆盖的范围就得成倍扩展间隔也随之成倍拉大。2.2 float到底丢掉了哪些整数我们来做个小实验。int能表示0到16777215里的每一个整数float在0到16777216这个范围内间距最小很多数都没问题。可一旦越过16777216比如16777217这个数在计算机里存成float后解析回来会变成16777216或者16777218而不是16777217。为什么因为2^24在二进制下是1后面24个0需要24位尾数精确表示但float的尾数实际只存23位所以从2^24开始多出来的低位就必须丢掉。于是奇数开始大量丢失接着是间隔2、间隔4、间隔8……越往上丢得越厉害。如果拿“精确表示的整数个数”来比int有4,294,967,296个float能精确表示的整数数量远少得多。很多初学者会用float存订单号、学号、身份证号结果发现数据莫名其妙变了就是栽在这个坑上。记住一个经验值32位float能保证精确表示的连续整数上限是16777216超过这个数就要谨慎。2.3 特殊值和冗余位模式也在“占名额”float的2^32个位模式不是每一个都在数轴上对应一个唯一的正常实数。我们细算一下规格化数指数E可以取1到254共254个尾数M有2^23种符号位有2种。这样规格化数共有254 × 2^23 × 2 4,261,412,864个位模式。非规格化数指数E0尾数M不等于0有2^23-1种乘以符号位2共16,777,214个位模式。正负02个位模式。正负无穷2个位模式。NaN指数全1且尾数非0共有2×(2^23-1)16,777,214个位模式。NaN位模式不是实数0的两个位模式又指向同一个数值。把这些剔掉之后float能表示的“不同有限实数”数量大约是4,278,190,078小于int的4,294,967,296。所以严格说32位int能表示的不同数值个数确实比32位float多。这个结论平时没人提是因为大家习惯用“值域范围”去判断类型容量。但在计算机里数据类型能表示的数从来不是连续区间而是一组有限的离散点。位宽相同不代表点数相同点再密也不代表范围一定大。3. 那些和int/float有关的常见坑3.1 C里float字面量为什么要加f有个热搜词叫“c加加编程float为什么加f”这问题我见过很多次。在C里直接写一个小数字面量比如3.14默认的类型是double不是float。当你写下float f 3.14;编译器会先把double类型的3.14转换成float如果数字的有效位太多转换过程中就会产生一次舍入精度损失已经发生了。虽然这个例子里3.14精度损失不太明显但换成3.14159265358979你就可能看到警告并且结果变成3.14159274之类的近似值。正确的写法是float f 3.14f;加上f后缀明确告诉编译器这个字面量就是float不会先按double处理再降级。社区里很多人都遇到过“同样的代码float结果和double结果不一样”的问题原因往往就在这里。更深一层float在寄存器或内存里只有24位有效二进制位当你用float参与连续运算时每次舍入误差都会累积。所以除非你明确知道数据范围小、精度要求不高、内存又紧张否则优先用double。double的有效位数大约53位二进制换算成十进制大概是15到17位有效数字绝大多数业务场景都够用。3.2 int转QString、求int数字长度热词里还有“int转qstring”和“求int类型数字长度”。这两个操作看着基础但不少人写过错误代码。在Qt中int转QString的标准做法是int num 42; QString s QString::number(num);不要用QString直接加int比如s value: num这在Qt里不会得到你想要的效果。老老实实用number函数简单可靠还不容易踩坑。至于求int类型数字长度最常见的是求十进制位数。如果只是正整数一个循环搞定int n 12345; int len 0; while (n 0) { len; n / 10; } // 别忘了处理 n 0 的情况也有同学想走捷径用std::to_string(n).length()这也没问题只是注意负数时前面的负号也会被算进去。还有人想用数学函数int len floor(log10(n)) 1;理论上没错但n很大时一旦int转成double/float再算log10就可能因为表示精度误差导致结果差1。比如某个大数float表示得不够精准log10下来取整就翻车了。所以我个人建议能用整数循环就别用浮点函数这正好也是“int表示整数比float可靠”的活例子。3.3 int与enum、函数指针之间的类型报错再看几个C新手常被编译错误劝退的场面。一个是“c int enum报错”另一个是“invalid conversion from void (*)() to int [-fpermissive]”。先说enum。C风格的枚举类型虽然本质上是整数但C为了类型安全不允许随意用int给枚举赋值也不建议在没强转的情况下拿枚举值去和int做比较。比如enum Color { RED, GREEN, BLUE }; int x RED; // 某些编译器可能允许但并不是所有C风格都推荐 Color c 1; // 直接报错不能把int隐式转成Color更安全的做法是使用enum classenum class Color { RED, GREEN, BLUE }; Color c Color::RED; // 必须显式指定 int x static_castint(c);enum class不能隐式转int也不能用int隐式构造这其实是好事。它能避免你拿一个枚举去跟任意的int比较逻辑变得清晰。再说函数指针。错误信息“invalid conversion from void (*)() to int”特别典型通常是有人写了int value someFunction; // 想把函数地址塞进int在C语言里函数指针转整数在部分平台确实可以凑合但C里直接这样写编译器就报fpermissive。根本原因是函数指针的大小不一定和int一致在64位平台下函数指针可能是64位int只有32位强行塞进去必然丢信息。就算你会在32位平台强转这种代码也是典型的“能用但早晚出事”。int是整数存储别什么都往里塞。3.4 PLC和GIS里怎么选int、real、float热词里还出现了plc、int real和gis里面char float int time。这其实属于工业控制和地理信息系统里的数据选型问题。在PLC编程中INT通常是16位有符号整数范围-32768到32767DINT是32位整数REAL是32位浮点。做计数器、工位计数、配方编号这种需要精确整数的场合用INT或DINT最稳。如果采集的是模拟量比如压力、温度传感器的原始值可能用REAL更方便。但REAL同样存在浮点精度问题两个数值相差很小的时候用判断就可能出错。所以PLC里做模拟量比较时一定要加死区或容许误差。GIS里更明显。经纬度如果用int存需要先放大很多倍再取整不然精度不够直接用float/double存则要小心浮点舍入导致边界偏移。char适合存那些固定短代码比如国家代码、状态码int适合存人口数量、面积这种整数time通常用时间戳或者专用时间类型。说白了GIS里的每一步选型也回到同一个问题你需要的是“精确的离散整数”还是“允许误差的量测值”。4. 用代码验证float在大整数区间上的空洞4.1 先算一下可表示数量的理论差距我们知道32位int可表示的不同整数数量是2^324,294,967,296。float正常实数数量上一节算过约4,278,190,078。看上去只差一千多万其实这个差距完全来自NaN、无穷和±0这些特殊位模式。但如果不比“全部可表示实数”而只比“精确可表示的整数”float就差得更多了。float的24位有效二进制位决定了任意一个精确整数超过2^24之后就不可能保证连续性。即便考虑后续更大的整数float也只是离散地选中其中一部分而不是全部。所以论“不同数值个数”int float。论“精确整数个数”int远大于float。论“取值范围”float远大于int。三条结论放在一起这道题才算做完整了。4.2 写个程序看看16777217去了哪可以用一小段C代码直观感受float丢整数的问题#include cstdio int main() { int start 16777216; // 2^24 for (int i start; i start 8; i) { float f static_castfloat(i); int back static_castint(f); if (back i) { std::printf(%d can be exactly represented as float\n, i); } else { std::printf(%d becomes %d\n, i, back); } } return 0; }在常见的IEEE 754环境下输出大概是16777216 can be exactly represented as float 16777217 becomes 16777216 16777218 can be exactly represented as float 16777219 becomes 16777218 16777220 can be exactly represented as float 16777221 becomes 16777220 16777222 can be exactly represented as float 16777223 becomes 16777224看到没有从2^24开始float只能表示偶数再往上间隔变成2、4、8……很快一个int区间里能被float精确表示的整数就寥寥无几了。这就是我说“float表示整数能力和int差着数量级”的最好证据。4.3 用union逆向观察位模式如果你还想更直观地感受int和float对同一段内存的解读差异可以用union做一个小实验#include cstdio int main() { union { int i; float f; } u; u.i 123456789; std::printf(bit pattern: 0x%08x\n, u.i); std::printf(as int: %d\n, u.i); std::printf(as float: %f\n, u.f); u.f 16777216.0f; std::printf(as int: %d\n, u.i); std::printf(as float: %f\n, u.f); return 0; }你会发现整数的123456789在float视角下变成了一个带小数的近似值而float的16777216.0在int视角下位模式又是另一个完全不同的整数。这从底层说明了一件事int和float的每一位含义不同它们的“容纳能力”不能用范围简单衡量。5. 实操经验和选型建议5.1 我在项目里的几个原则接触过大量跟int/float相关的bug后我给自己定了几个原则你们可以直接抄作业计数、下标、ID、状态码、端口号、开关量一律用int或者long不用float。如果只是需要小数但精度要求一般优先用double不要用float。除非你有明确的性能或内存压力比如GPU渲染或者大批量数组存储。必须用float时字面量后面记得加f别让编译器在double和float之间做隐式转换。判断浮点是否相等不要直接写if (a b)而是写if (fabs(a - b) 1e-6)否则会因为舍入误差得到意料之外的结果。int和float混算时int会先被转成float再运算结果可能丢失精度。如果对精度有要求先把float转成double或者统一用double。比如在写一个计数器时你很清楚这个数不会超过21亿那就老老实实用int不要为了“以防万一”用float。float的“防万一”其实只是扩大了范围却引入了精度不确定性在精确计数场景里反而是灾难。5.2 回到标题以后再遇到这个问题的回答模板如果下次有人再问你“为什么说int比float表示的数多”你可以这样回答“这句话一般是在讨论32位类型时可表示的不同数值数量。int的2^32个位模式全部对应唯一整数一个不浪费float虽然也有2^32个位模式但里面要分给正负无穷、NaN、正负0这些特殊值真正能表示的不同有限实数已经少于2^32。再加上float的大数区域间隔很大很多整数根本精确表示不了。所以在精确表示整数的能力上int确实比float强得多。”我个人在实际工作中常年用int处理精确计数用double处理需要小数的计算几乎不在业务逻辑里碰float。除非是图形学、音频处理或者PLC模拟量采集这类有明确内存和硬件需求的场景否则float带来的那点内存节省远比不上它丢精度让你查bug的时间成本。尤其是看到初学者把学号、金额、传感器计数用float存储时我第一反应就是劝他们赶紧换成int或者long。这个坑踩过一次就够你记一辈子了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →