面试必问!!!:整数在计算机中是怎么保存的?
面试必问补码、大小端、类型转换一篇打通1. 测试环境说明以下数据基于Visual Studio x8632位环境实测。不同平台下部分类型大小可能不同后文会详细说明。项目配置开发环境Visual Studio目标平台x8632位字节序小端存储2. 本文目标初学C语言时只知道int是 4 字节、char是 1 字节但不清楚为什么这样设计、负数怎么存、类型之间怎么转换。本文目标画完整数家族的全貌搞懂补码、大小端、类型转换这三个面试高频考点。3. 有符号整数家族有符号整数可以表示正数、负数和零。存储结构是1位符号位 N位数值位。类型格式控制符大小(Byte)存储结构范围char%hhd11符号7数值-128 ~ 127short%hd21符号15数值-32768 ~ 32767int%d41符号31数值-2^31 ~ 2^31-1long%ld41符号31数值-2^31 ~ 2^31-1long long%lld81符号63数值-2^63 ~ 2^63-1注意long在 32 位 Windows 上是 4 字节在 64 位 Linux 上是 8 字节。本文环境是 32 位 VS所以longint 4 字节。4. 无符号整数家族无符号整数所有位都表示数值没有符号位。类型格式控制符大小(Byte)范围unsigned char%hhu10 ~ 255unsigned short%hu20 ~ 65535unsigned int%u40 ~ 2^32-1unsigned long%lu40 ~ 2^32-1unsigned long long%llu80 ~ 2^64-1适用场景身高、体重、数组下标、size_t等没有负数的数据。5. 补码负数在内存中的存储方式计算机中所有整数都以补码形式存储。这是面试最高频的考点之一。5.1 正数原码 反码 补码正数三码合一直接存其二进制形式。inta10;// 原码(0) 00000000 00000000 00000000 00001010// 补码(0) 00000000 00000000 00000000 00001010// 内存小端0A 00 00 005.2 负数原码 → 取反 → 1负数需要三步才能存入内存。以 -10 为例// 第一步写原码符号位1数值位绝对值// 原码(1) 00000000 00000000 00000000 00001010// ↑符号位为1表示负数后面是10的二进制// 第二步求反码符号位不变其余位取反// 反码(1) 11111111 11111111 11111111 11110101// ↑符号位保持1后面是原码数值位的按位取反// 第三步得补码反码最低位加1// 补码(1) 11111111 11111111 11111111 11110110// ↑这就是最终存储在内存中的二进制形式// 内存查看小端存储// 地址: 0x00 0x01 0x02 0x03// 数据: 0xF6 0xFF 0xFF 0xFF// 解释: 低地址存低字节所以看到的是 F6 FF FF FF计算过程总结原码符号位1 绝对值的二进制反码符号位不变数值位按位取反补码反码加1得到最终存储形式5.3 从内存读出负数减 1 → 取反如果看到内存中最高位是 1就知道它是负数需要反向操作补码(1) 11111111 11111111 11111111 11110110 减一(1) 11111111 11111111 11111111 11110101 取反(1) 00000000 00000000 00000000 00001010 → 原码 -105.4 为什么用补码统一加减法省去减法电路。 A - B 就是 A (-B)CPU 只需要加法器就能完成所有整数运算。补码运算示例inta10;// 补码00001010intb-6;// 补码11111010intcab;// 00001010 11111010 00000100 4// 10 (-6) 4验证正确6. 大小端字节的存储顺序同一个整数0x12345678在不同 CPU 上的存储顺序不同。这是面试中考察“是否真的写过底层代码”的标志性问题。模式低地址 → 高地址常见平台小端78 56 34 12x86/x64VS 32位也是小端大端12 34 56 78部分嵌入式、网络字节序验证方法用 VS 调试器查看内存窗口或 GDB 的x/4xb a第一个字节是0x78就是小端内存布局对比小端存储x86/x64 地址0x00 0x01 0x02 0x03 数据0x78 0x56 0x34 0x12 大端存储网络字节序 地址0x00 0x01 0x02 0x03 数据0x12 0x34 0x56 0x787. 类型转换的底层规则7.1 大字节 → 小字节截取低字节inta0x12345678;charca;// c 0x78高 3 字节被丢弃// 内存视角// a: 78 56 34 12 (小端存储)// c: 78 (只保留最低字节)7.2 小字节 → 大字节符号扩展// 有符号类型高位全部用符号位填充保证数值不变charc-1;// 内存FFintic;// 内存FF FF FF FF符号位 1 填充高位// 解释c 的符号位是 1扩展时高位全部填充 1// 无符号类型高位全部用 0 填充unsignedcharuc255;// 内存FFunsignedintuiuc;// 内存00 00 00 FF高位填充 07.3 有符号 ↔ 无符号二进制位不变解读方式变unsignedintu0xFFFFFFFF;printf(%d,u);// 输出 -1按有符号解读printf(%u,u);// 输出 4294967295按无符号解读// 同样的二进制按 %d 解读是 -1按 %u 解读是 42949672958. sizeof 验证用代码验证每种类型在自己的平台上实际占多少字节。以下是在 Windows VS x86 环境下的测试代码和结果#includestdio.hintmain(){printf( Windows VS x86 环境类型大小测试 \n\n);// 有符号整数类型printf(有符号整数类型\n);printf(sizeof(char) %zu 字节\n,sizeof(char));printf(sizeof(short) %zu 字节\n,sizeof(short));printf(sizeof(int) %zu 字节\n,sizeof(int));printf(sizeof(long) %zu 字节\n,sizeof(long));printf(sizeof(long long) %zu 字节\n\n,sizeof(longlong));// 无符号整数类型printf(无符号整数类型\n);printf(sizeof(unsigned char) %zu 字节\n,sizeof(unsignedchar));printf(sizeof(unsigned short) %zu 字节\n,sizeof(unsignedshort));printf(sizeof(unsigned int) %zu 字节\n,sizeof(unsignedint));printf(sizeof(unsigned long) %zu 字节\n,sizeof(unsignedlong));printf(sizeof(unsigned long long) %zu 字节\n\n,sizeof(unsignedlonglong));// 指针类型printf(指针类型\n);printf(sizeof(int*) %zu 字节\n,sizeof(int*));printf(sizeof(char*) %zu 字节\n,sizeof(char*));printf(sizeof(void*) %zu 字节\n,sizeof(void*));return0;}运行结果 Windows VS x86 环境类型大小测试 有符号整数类型 sizeof(char) 1 字节 sizeof(short) 2 字节 sizeof(int) 4 字节 sizeof(long) 4 字节 sizeof(long long) 8 字节 无符号整数类型 sizeof(unsigned char) 1 字节 sizeof(unsigned short) 2 字节 sizeof(unsigned int) 4 字节 sizeof(unsigned long) 4 字节 sizeof(unsigned long long) 8 字节 指针类型 sizeof(int*) 4 字节 sizeof(char*) 4 字节 sizeof(void*) 4 字节验证结论在 Windows VS x86 环境下int和long都是 4 字节指针类型大小为 4 字节32位系统与前面表格中的理论值完全一致9. 平台差异提醒不同平台下类型大小可能不同以下是常见差异对比类型32位 Windows本文环境64位 Linux说明char1 字节1 字节字符类型固定1字节short2 字节2 字节短整型固定2字节int4 字节4 字节整型通常4字节long4 字节8 字节主要差异点long long8 字节8 字节长整型固定8字节指针4 字节8 字节主要差异点经验跨平台代码不要假设long和指针的大小用sizeof实测或用固定大小的类型如int32_t、int64_t。10. 踩坑记录坑1格式控制符用错chara255;printf(%hhd\n,a);// -1255 按有符号解读是 -1printf(%hhu\n,a);// 255正确坑2char 的有符号/无符号不确定// char 默认是有符号还是无符号取决于编译器// VS 中默认 char 是有符号的charc1255;// 可能被解释为 -1unsignedcharc2255;// 明确指定无符号保证是 255// 需要明确指定 signed char 或 unsigned charsignedcharsc-128;// 明确有符号unsignedcharuc255;// 明确无符号坑3long 的平台差异// 在 VS 32 位下 long 4 字节// 在 64 位 Linux 下 long 8 字节longvalue100;printf(sizeof(long) %zu\n,sizeof(long));// 不同平台结果不同// 解决方案使用固定大小类型#includestdint.hint32_tfixed32100;// 固定 4 字节int64_tfixed64100;// 固定 8 字节坑4大小端导致的字节序问题// 网络传输时需要注意字节序转换uint32_tnetwork_value0x12345678;uint32_thost_valuentohl(network_value);// 网络字节序转主机字节序// 文件读写时也要注意字节序FILE*fpfopen(data.bin,wb);uint32_tdata0x12345678;fwrite(data,sizeof(data),1,fp);// 写入的是主机字节序11. 学习体会从“背锅侠”到“规则制定者”以前学C语言看到int是4字节、char是1字节总觉得这是“天经地义”的设定背就完事了。面试被问到“为什么负数要存补码”只能支支吾吾说“计算机就是这么规定的”。现在终于明白了这些不是死记硬背的规则而是一套自洽的底层逻辑。补码不是故意为难你而是为了让CPU只用加法器就能搞定所有整数运算包括减法。就像你学会了“借位减法”突然发现原来可以全部用加法搞定——这感觉爽大小端不是CPU设计师的恶趣味而是历史遗留和性能优化的结果。知道这个下次面试官问“怎么判断大小端”你就能淡定地说“看第一个字节是高位还是低位呗”类型转换本质就是“二进制位不变解释方式变”。同一个0xFFFFFFFF%d看是 -1%u看是 4294967295——不是内存变了是你的“眼镜”换了。现在再看这些规则不再是“背锅侠”式的被动接受而是能理解背后的设计逻辑。就像玩游戏终于看懂了规则书从“被规则玩”变成了“玩规则”。12. 下篇预告浮点数的“魔法”与“陷阱”恭喜你整数家族的存储规则已经通关补码让负数有了统一的表示大小端决定了字节排列顺序类型转换的本质是“二进制位不变解释方式变”。但内存的“魔法秀”还没结束——接下来让我们走进浮点数的奇幻世界 下篇看点IEEE 754 标准为什么0.1 0.2 ≠ 0.3不是计算机算错了而是浮点数的“精度游戏”。S-E-M 三段式float 和 double 那套“符号位-指数位-尾数位”到底怎么工作比整数复杂但也更有趣内存视角同一块内存用int和float解读会得到完全不同的结果——这不是bug这是特性实战踩坑为什么float a 0.1; if (a 0.1)可能永远不成立如何正确比较浮点数 预告金句“整数是规规矩矩的上班族浮点数是充满创意的艺术家——但艺术家也有自己的‘怪癖’。”准备好了吗下一篇我们继续拆解内存底层看看那些看似“魔法”的现象背后都是严谨的数学规则。悄悄说学完浮点数你就能理解为什么游戏里的物理引擎有时候会“抽风”也能写出更稳健的科学计算代码了。