面试必问!!!:整数在计算机中是怎么保存的?
面试必问:补码、大小端、类型转换,一篇打通
1. 测试环境说明
以下数据基于Visual Studio x86(32位)环境实测。不同平台下部分类型大小可能不同,后文会详细说明。
| 项目 | 配置 |
|---|---|
| 开发环境 | Visual Studio |
| 目标平台 | x86(32位) |
| 字节序 | 小端存储 |
2. 本文目标
初学C语言时只知道int是 4 字节、char是 1 字节,但不清楚为什么这样设计、负数怎么存、类型之间怎么转换。
本文目标:画完整数家族的全貌,搞懂补码、大小端、类型转换这三个面试高频考点。
3. 有符号整数家族
有符号整数可以表示正数、负数和零。存储结构是1位符号位 + N位数值位。
| 类型 | 格式控制符 | 大小(Byte) | 存储结构 | 范围 |
|---|---|---|---|---|
| char | %hhd | 1 | 1符号+7数值 | -128 ~ 127 |
| short | %hd | 2 | 1符号+15数值 | -32768 ~ 32767 |
| int | %d | 4 | 1符号+31数值 | -2^31 ~ 2^31-1 |
| long | %ld | 4 | 1符号+31数值 | -2^31 ~ 2^31-1 |
| long long | %lld | 8 | 1符号+63数值 | -2^63 ~ 2^63-1 |
注意:long在 32 位 Windows 上是 4 字节,在 64 位 Linux 上是 8 字节。本文环境是 32 位 VS,所以long=int= 4 字节。
4. 无符号整数家族
无符号整数所有位都表示数值,没有符号位。
| 类型 | 格式控制符 | 大小(Byte) | 范围 |
|---|---|---|---|
| unsigned char | %hhu | 1 | 0 ~ 255 |
| unsigned short | %hu | 2 | 0 ~ 65535 |
| unsigned int | %u | 4 | 0 ~ 2^32-1 |
| unsigned long | %lu | 4 | 0 ~ 2^32-1 |
| unsigned long long | %llu | 8 | 0 ~ 2^64-1 |
适用场景:身高、体重、数组下标、size_t等没有负数的数据。
5. 补码:负数在内存中的存储方式
计算机中所有整数都以补码形式存储。这是面试最高频的考点之一。
5.1 正数:原码 = 反码 = 补码
正数三码合一,直接存其二进制形式。
inta=10;// 原码:(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 只需要加法器就能完成所有整数运算。
补码运算示例:
inta=10;// 补码:00001010intb=-6;// 补码:11111010intc=a+b;// 00001010 + 11111010 = 00000100 = 4// 10 + (-6) = 4,验证正确6. 大小端:字节的存储顺序
同一个整数0x12345678在不同 CPU 上的存储顺序不同。这是面试中考察“是否真的写过底层代码”的标志性问题。
| 模式 | 低地址 → 高地址 | 常见平台 |
|---|---|---|
| 小端 | 78 56 34 12 | x86/x64(VS 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 大字节 → 小字节:截取低字节
inta=0x12345678;charc=a;// c = 0x78,高 3 字节被丢弃// 内存视角:// a: 78 56 34 12 (小端存储)// c: 78 (只保留最低字节)7.2 小字节 → 大字节:符号扩展
// 有符号类型:高位全部用符号位填充,保证数值不变charc=-1;// 内存:FFinti=c;// 内存:FF FF FF FF(符号位 1 填充高位)// 解释:c 的符号位是 1,扩展时高位全部填充 1// 无符号类型:高位全部用 0 填充unsignedcharuc=255;// 内存:FFunsignedintui=uc;// 内存:00 00 00 FF(高位填充 0)7.3 有符号 ↔ 无符号:二进制位不变,解读方式变
unsignedintu=0xFFFFFFFF;printf("%d",u);// 输出 -1(按有符号解读)printf("%u",u);// 输出 4294967295(按无符号解读)// 同样的二进制,按 %d 解读是 -1,按 %u 解读是 42949672958. sizeof 验证
用代码验证每种类型在自己的平台上实际占多少字节。
以下是在 Windows VS x86 环境下的测试代码和结果:
#include<stdio.h>intmain(){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 | 说明 |
|---|---|---|---|
| char | 1 字节 | 1 字节 | 字符类型,固定1字节 |
| short | 2 字节 | 2 字节 | 短整型,固定2字节 |
| int | 4 字节 | 4 字节 | 整型,通常4字节 |
| long | 4 字节 | 8 字节 | 主要差异点 |
| long long | 8 字节 | 8 字节 | 长整型,固定8字节 |
| 指针 | 4 字节 | 8 字节 | 主要差异点 |
经验:跨平台代码不要假设long和指针的大小,用sizeof实测或用固定大小的类型(如int32_t、int64_t)。
10. 踩坑记录
坑1:格式控制符用错
chara=255;printf("%hhd\n",a);// -1(255 按有符号解读是 -1)printf("%hhu\n",a);// 255(正确)坑2:char 的有符号/无符号不确定
// char 默认是有符号还是无符号取决于编译器// VS 中默认 char 是有符号的charc1=255;// 可能被解释为 -1unsignedcharc2=255;// 明确指定无符号,保证是 255// 需要明确指定 signed char 或 unsigned charsignedcharsc=-128;// 明确有符号unsignedcharuc=255;// 明确无符号坑3:long 的平台差异
// 在 VS 32 位下 long = 4 字节// 在 64 位 Linux 下 long = 8 字节longvalue=100;printf("sizeof(long) = %zu\n",sizeof(long));// 不同平台结果不同// 解决方案:使用固定大小类型#include<stdint.h>int32_tfixed32=100;// 固定 4 字节int64_tfixed64=100;// 固定 8 字节坑4:大小端导致的字节序问题
// 网络传输时需要注意字节序转换uint32_tnetwork_value=0x12345678;uint32_thost_value=ntohl(network_value);// 网络字节序转主机字节序// 文件读写时也要注意字节序FILE*fp=fopen("data.bin","wb");uint32_tdata=0x12345678;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)可能永远不成立?如何正确比较浮点数?
🚀 预告金句:
“整数是规规矩矩的上班族,浮点数是充满创意的艺术家——但艺术家也有自己的‘怪癖’。”
准备好了吗?下一篇,我们继续拆解内存底层,看看那些看似“魔法”的现象背后,都是严谨的数学规则。
(悄悄说:学完浮点数,你就能理解为什么游戏里的物理引擎有时候会“抽风”,也能写出更稳健的科学计算代码了。)
