引言
很多人写C语言,天天写int a; float b;,但很少思考一个问题:我们写的数字,在计算机内存这一大片字节格子里,到底是怎么存放的? 为什么同样一串二进制,当int读是9,当float读就变成0.000000?为什么char a=-1打印%u会出来255?大小端又是什么鬼?今天咱们抛开晦涩课本,把内存存储一次性讲明白。
一、整数在内存:存的不是原码,是补码
我们都知道,计算机只认识0和1。 对于有符号整数,有三套二进制表示:原码、反码、补码。
- 原码:直接把数字转成二进制,最高位当做符号位,0代表正数,1代表负数。
- 反码:正数反码等于原码;负数符号位不动,其余全部按位取反。
- 补码:正数补码=原码;负数 = 反码 + 1。
💡重点:内存里面,整数一律存放补码!不是原码!
为啥非要搞补码,直接存原码不好吗?
- CPU只有加法器,没有减法器。减法可以转换成补码的加法运算。计算
5‑3等价于5 + (-3)的补码相加。 - 解决原码里面存在
+0和‑0两个编码的问题,补码体系中0只有唯一一份编码,还多挤出来一个负数可用。
举个8位signed char例子:
-1原码:1000 0001-1反码:1111 1110-1补码:1111 1111
所以内存中存的就是11111111。 这就解释了经典笔试题:
char a = -1; unsigned char c = -1; printf("%d", c); //输出255内存字节是0xff,当成无符号解读,就是255。
8位有符号char范围:‑128 ~ 127注意:
‑128没有对应的原码和反码,是补码独有的,二进制10000000。
无符号数溢出:环形绕圈现象
无符号数字到最大值再加1,不会报错,直接绕回0。unsigned char范围0‑255,255+1直接变成0。 之前那道高频面试题:
char a[1000]; int i; for(i = 0; i < 1000; i++) { a[i] = -1 - i; } printf("%d", strlen(a));strlen统计到'\0'(字节0)停止。‑1‑i赋值给char,截断保留低8位。当i=255,‑1‑255 = ‑256,低8位全部为0,也就是\0。前面一共255个有效字符,输出255。
小技巧:8位数据,只要是256的整数倍,截断低8位一定等于0。
二、大小端:同一个数字,两种摆放方式
问题来了:一个int占4字节,4个字节怎么排列放在内存地址上?这里就诞生了大端、小端模式。
举个例子:int a = 0x11223344;把这个int拆开成4个字节: 高字节:0x11,0x22,低字节:0x33,0x44
内存地址是从小到大增长的,就像一排从左向右编号的盒子。
- 小端模式(我们电脑VS、X86/X64都是小端)低字节放到低地址(左边盒子),高字节放到高地址(右边盒子)内存排布(地址由低→高):
44 33 22 11VS内存窗口,从左往右就是地址由低到高,看到的就是这个顺序。 - 大端模式(单片机KEIL C51常用)高字节放到低地址,低字节放到高地址内存排布(地址由低→高):
11 22 33 44,阅读更符合人类阅读习惯。
注意:大小端只是字节的摆放顺序,不会改变数值,只是内存存储布局不同。
面试手写:判断本机是大端还是小端
思路:把int=1,拿第一个内存字节看值。 int i=1,小端下最低地址存的字节是01;大端最低地址存00。
写法1,指针强制转换版:
int check_sys() { int i = 1; return *(char *)&i; }返回1代表小端;返回0代表大端。
写法2,联合体版本(利用联合体成员共享同一起始内存)
int check_sys() { union{ int i; char c; }un; un.i = 1; return un.c; }联合体所有成员共用同一块内存起始位置,
un.c直接取第一个字节,不需要强制转换。
三、浮点数float/double,存储规则完全不一样
整数用补码存,但是浮点数完全不走补码这套体系!遵循国际标准IEEE754。
公式: $$V=(-1)^S × M × 2^E$$
拆解三部分:
- S(符号位):1bit,0正数,1负数
- E(指数位):用来存放2的多少次方
- M(有效数字/尾数)
| 类型 | S符号位 | E指数位 | M有效位 |
|---|---|---|---|
| float(32位单精度) | 1 bit | 8 bit | 23 bit |
| double(64位双精度) | 1 bit | 1 bit | 11 bit |
举例子:5.0,二进制写成101.0科学计数法:$1.01 × 2^2$ S=0,M=1.01,E=2。
两个关键规则
- M的隐藏1M规定范围 $1 ≤ M <2$,一定是
1.xxxx。 标准直接把整数部分的1舍弃不存,内存只保存小数点后面xxxx部分。读取的时候自动把1加回来,凭空多出来1位精度。 - E指数要加偏移量(中间数)E是无符号数,不能存负数。
- float:E真实值 +127存入内存
- double:E真实值 +1023存入内存
比如E真实等于2,float存入内存写:2+127 = 129。
E的三种读取情况
- E不全0,不全1(正常情况)真实指数 = 内存E − 偏移量(127/1023),M补上隐藏的1。
- E全部为0隐藏的1不再加上,M变成
0.xxxx。用来表示接近0的极小数字,±0。 - E全部为1如果M全0,代表正负无穷;M不为0代表NaN(无效数值)。
经典迷惑代码,看完再也不懵
int n = 9; float *pFloat = (float *)&n; printf("n的值为:%d\n", n); printf("*pFloat的值为:%f\n", *pFloat); *pFloat = 9.0; printf("n的值为:%d\n", n); printf("*pFloat的值为:%f\n", *pFloat);输出:
n的值为:9 *pFloat的值为:0.000000 n的值为:1091567616 *pFloat的值为:9.000000n=9,内存二进制:00000000 00000000 00000000 00001001。把这串二进制强行按float规则解读。 S=0,E=全0,进入E全0特殊场景,算出来是极小接近0的数字,打印就是0.000000。- 往这个内存地址写入浮点数9.0,按照IEEE754编码写入一串bit。再用int去读取这串bit,就得到十进制数字1091567616。
⚠️重点提醒:int和float虽然都是4字节,编码体系完全两套!同样二进制,解读方式不同,数值天差地别。同时注意:部分十进制小数(例如1.2)二进制无法精确表达,float/double存储会存在微小精度误差。
本篇核心总结
- 整数内存存补码,CPU靠补码把减法变成加法运算;有符号溢出属于未定义行为,无符号溢出环形回绕。
- 大小端是字节排列顺序:小端低字节放低地址,电脑X86都是小端;两种手写检测方式:指针强转、联合体。
- 浮点数不使用补码,遵循IEEE754:S符号位,E带偏移的指数,M有隐藏最高位1;分E全0、全1、普通三种场景。
- 同样的内存二进制序列,用int读、用float读,得到完全不同结果,因为两套解码规则。
很多C语言坑,根源就是不懂内存存储。面试经常考的溢出题、大小端、int和float强制类型转换迷惑题,底层全部来源于这些知识点。
如果你喜欢本文,欢迎点赞收藏,后面继续更新C指针、结构体内存对齐的底层内容。有看不懂的地方欢迎评论区留言讨论👋886~。