1.整数在内存中的存储
一、核心概念速记
| 概念 | 定义 | 特点 |
|---|---|---|
| 原码 | 数值直接翻译成二进制,最高位是符号位(0 正 1 负) | 直观,但加减运算复杂 |
| 反码 | 正数:和原码相同负数:符号位不变,其余位取反 | 是从原码到补码的过渡 |
| 补码 | 正数:和原码相同负数:反码 + 1 | 计算机实际存储的形式,可统一处理加减 |
二、完整例子(以 8 位二进制为例)
1. 正数:+5
- 原码:
0000 0101 - 反码:
0000 0101(和原码相同) - 补码:
0000 0101(和原码相同)
2. 负数:-5
- 原码:
1000 0101(最高位 1 表示负,数值位是 5 的二进制) - 反码:
1111 1010(符号位不变,其余位取反) - 补码:
1111 1011(反码 + 1)
3. 用补码计算 5 - 3(即 5 + (-3))
5的补码:0000 0101-3的补码:1111 1101- 相加:
0000 0101 + 1111 1101 = 1 0000 0010 - 最高位的进位丢弃,结果是
0000 0010,也就是+2,结果正确!
三、为什么计算机要用补码?
- 统一处理符号位和数值位 用补码时,符号位和数值位可以一起参与运算,不用单独处理。
- 加法和减法可以用同一套电路实现CPU 只有加法器,用补码可以把减法变成加法。
- 避免出现 "+0" 和 "-0" 的歧义 原码 / 反码中,
+0和-0有两种表示;补码中只有一种
四、关键结论
- 计算机中所有整数都以补码形式存储,我们写代码时看到的十进制数,底层都是补码。
- 正数的原 / 反 / 补码完全相同,不用额外转换。
- 负数的补码是 "反码 + 1",反码是 "原码除符号位外取反"。
2. 大小端核心概念
一、 什么是大小端?
以 4 字节的 int a = 0x11223344 为例,它的4 个字节从高到低是 :0x11 0x22 0x33 0x44
- 大端模式 :高字节存低地址,低字节存高地址→ 内存中顺序是
11 22 33 44 - 小端模式 :低字节存低地址,高字节存高地址 → 内存中顺序是
44 33 22 11
二、为什么会有大小端?
因为计算机以字节为单位 寻址,而 **short/int/long**是多字节数据,不同硬件架构对多字节数据的存储顺序定义不同:
- x86/x86-64、ARM(多数):小端模式
- Keil C51、部分网络协议:大端模式
- 部分 ARM 架构支持软件 / 硬件切换模式
三、练习题解析(从易到难)
练习 1:判断大小端的两种写法
这是最经典的笔试题,核心思路都是「用 char* 取 int 的第一个字节」
// 写法1:指针强制转换
int check_sys() {
int i = 1;
return *(char *)&i;
}
// 小端:内存是 01 00 00 00 → 取第一个字节得到 1 → 输出小端
// 大端:内存是 00 00 00 01 → 取第一个字节得到 0 → 输出大端
// 写法2:共用体(union)实现
int check_sys() {
union {
int i;
char c;
} un;
un.i = 1;
return un.c;
}
// union 的成员共享同一块内存,char c 会直接取 int i 的第一个字节,和写法1原理一致
练习 2:char/signed char/unsigned char 输出题
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d,b=%d,c=%d",a,b,c);
return 0;
}
运行结果: a = -1, b = -1, c = 255
解析:(输出的原码)(存的是补码)(%d是以十进制数打印一个有符号整数)
-
char a=-1 char 默认是有符号,存
11111111(补码),整型提升符号位扩展 → 输出 -1(原码) -
signed char b=-1 有符号 char,存
11111111,整型提升符号位扩展一直补1 → 输出 -1 -
unsigned char c=-1 无符号 char,存
11111111(补码),整型提升高位补 0(符号位变成0) → 输出 255(原码就和补码相同)
练习 3:char 赋值越界题
代码 1:char a = -128; printf("%u\n",a);
char是有符号类型 ,范围是**-128 ~ 127** ,-128的补码是10000000- 按
%u输出时(%u以十进制形式,打印无符号整数(就没有原反补概念了)) ,会符号扩展为0xFFFFFF80,十进制为4294967168
代码 2:char a = 128; printf("%u\n",a);
char最大正数是127,128超出范围,实际存储的是128 - 256 = -128 (因为取的10000000与-128的补码前八位一样)- 补码同样是
10000000,按%u输出和代码 1 结果一样,为4294967168
练习 4:strlen 与 char 数组题
int main() {
char a[1000];
int i;
for(i = 0; i < 1000; i++) {
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
运行结果:255
解析:
char是有符号类型,赋值范围是**-128 ~ 127** ,循环赋值会溢出:i=0:a[0] = -1i=1:a[1] = -2- ...
i=127:a[127] = -128i=128:a[128] = -129→ 溢出为127i=255:a[255] = -256→ 溢出为0
strlen以'\0'(即0)作为结束标志,第一个0出现在a[255],所以长度是255
练习 5:死循环题
代码 1:unsigned char i = 0; for(i=0; i<=255; i++) printf("hello world\n");
unsigned char的范围是0 ~ 255,当i=255时 ,i++会溢出回到0- 条件
i<=255永远成立,这是一个无限循环
代码 2:unsigned int i; for(i=9; i>=0; i--) printf("%u\n",i);
unsigned int没有负数,当i=0时,i--会溢出变成4294967295(无符号最大值)- 条件
i>=0永远成立,也是一个无限循环
练习 6:指针与大小端题(x86 小端环境)(小端字节序)
#include <stdio.h>
//X86环境 小端字节序
int main()
{
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf("%x, %x", ptr1[-1], *ptr2);
return 0;
}
1. 先算 ptr1[-1] 的值
&a是整个数组的地址 ,类型是int(*)[4](指向 4 个 int 的数组指针)。&a + 1会跳过整个数组的大小 ,也就是4*sizeof(int) = 16字节,指向数组最后一个元素的下一个地址。ptr1 = (int *)(&a + 1);把这个地址转成了**int*类型。**ptr1[-1]等价于*(ptr1 - 1),也就是从ptr1往前移动 1 个int(4 字节),正好指向数组的最后一个元素a[3]。a[3]的值是4,用%x输出就是4。
2. 再算 *ptr2 的值
-
a是数组名,作为地址时等价于**&a[0]** ,类型是int*。 -
(int)a把这个地址强制转换成了整数 ,假设a的地址是0x0000FF00(仅举例),那么(int)a + 1就是0x0000FF01。 -
ptr2 =(int *)((int)a + 1);把这个地址又转回了int*类型,指向0x0000FF01这个地址。 -
我们来看内存布局(x86 是小端序,低地址存低位):
地址: FF00 FF01 FF02 FF03 FF04 FF05 FF06 FF07 ... 数据: 01 00 00 00 02 00 00 00 ... (a[0]=1) (a[1]=2) -
ptr2指向FF01,*ptr2会从**FF01开始读取 4 个字节** :00 00 00 02。(因为是int*) -
因为是小端序,这 4 个字节组成的**
int是0x02000000。**
最终运行结果
4, 2000000
3. 浮点数在内存中的存储(IEEE 754 标准)
一、基础概念
- 浮点数家族:
float、double、long double - 表示范围:定义在头文件
float.h中 - 常见例子:
3.14159、1E10等
二、经典例题
#include <stdio.h>
int main()
{
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);
return 0;
}
运行结果:
n的值为:9
*pFloat的值为:0.000000
n的值为:1091567616
*pFloat的值为:9.000000
这个运行结果说明 整数在内存的存储方式和浮点数是不一样的
三、核心原理:IEEE 754 浮点数表示法
任意二进制浮点数 V 可表示为:
例 1:正整数5 的表示
- 5 的二进制是
101,写成科学计数法:5 = 1.01 × 2² - 套公式:
S=0→ 正数M=1.01(满足1 ≤ M < 2)E=2所以:V = (-1)^0 × 1.01 × 2² = 1 × 1.01 × 4 = 5
例 2:负小数 -3.5 的表示
- 转成二进制科学计数法:
- 3.5 的二进制是**
11.1** - 写成**
1.11 × 2¹**
- 3.5 的二进制是**
- 套公式:
S=1→ 负数M=1.11E=1所以:V = (-1)^1 × 1.11 × 2¹ = -1 × 1.11 × 2 = -3.5
存储结构
| 类型 | 符号位 S | 指数位 E | 尾数位 M |
|---|---|---|---|
| float(32位) | 1 bit | 8 bit | 23 bit |
| double(64位) | 1 bit | 11 bit | 52 bit |
四、存储与读取规则
1. 存的过程
- 符号位 S:直接存正负号
- 尾数位 M :
- 科学计数法中
M总是1.xxxx的形式 - 计算机中只存小数部分
xxxx,前面的1不存(节省 1 位空间) - 读取时再把
1补上
- 科学计数法中
- 指数位 E :
- 存的是偏移值:真实指数 + 中间数
- float 的中间数是
127,double 是1023(进行修正 避免E为负数) - 例:
2^3的指数是3,float 中存为3+127=130
但是有时候会遇到特殊情况 比如有的浮点数无法精确保存
像 1.2 这种十进制小数,转成二进制时是无限循环小数 :1.2₁₀ = 1.001100110011...₂(0011 无限循环)
2. 取的过程(三种情况)
- E 不全为 0 / 不全为 1(常规情况)
- 真实指数 = 存储的 E - 中间数
- 有效数字 M 前面补上省略的 1
- E 全为 0
- 指数真实值 =
1 - 中间数(float 为 - 126) - 有效数字 M 不再补 1,直接按
0.xxxx解析 - 用于表示接近 0 的极小值
- 指数真实值 =
- E 全为 1
- 若 M 全为 0 :表示**
±∞(正负由 S 决定)** - 若 M 不全为 0:表示
NaN(非数字)
- 若 M 全为 0 :表示**
经过上面的知识学习后我们再回过头来解释一下这个代码
五、例题回顾
int n = 9;
float *pFloat = (float *)&n; // 把int的地址强转成float指针
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0; // 用float指针修改同一块内存
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
核心是:n 和 *pFloat 指向的是同一块 4 字节内存,只是解释方式不同
int n:按整数规则解释这 4 个字节float *pFloat:按IEEE 754 浮点数规则解释这 4 个字节
第一步:int n = 9; 时的内存和输出
1. n 的二进制表示
int n = 9 是一个 32 位整数,二进制为:
00000000 00000000 00000000 00001001
按 int 解释,它就是十进制的 9 ,所以第一句输出 n的值为:9。
2. 用 float 解释这串二进制
把上面这串二进制,按 IEEE 754 浮点数规则拆分:
- 符号位
S:最高位0→ 正数 - 指数位
E:接下来 8 位00000000→ E 全 0(非规格化数) - 尾数位
M:剩下 23 位**00000000000000000001001**
套用非规格化数的公式代入:
S=0,所以(-1)^0 = 10.M = 0.00000000000000000001001₂,这个数极小**(非规范数不用加1)**- 乘以 (2^{-126}) (1-127得126)后,结果是一个无限接近 0 的超级小正数
printf("%f") 默认保留 6 位小数 ,这么小的数会被直接显示为 0.000000 ,所以第二句输出 *pFloat的值为:0.000000。
第二步:*pFloat = 9.0; 时的内存和输出
1. 9.0 的 IEEE 754 表示
先把 9.0 转成二进制科学计数法:9.0 1001.0 (-1)^0 * (1.001) * 2^3 按 IEEE 754 规则编码:
- 符号位
S:0(正数) - 指数位
E:真实指数是 3 ,加上偏移量127 → (3 + 127 = 130 ) → 二进制10000010 - 尾数位
M:省略1.001前面的1,存小数部分001,后面补 0 凑够 23 位:00100000000000000000000
最终 32 位二进制:
0 10000010 00100000000000000000000
2. 把这串二进制按 int 解释
把上面这串二进制,当成 32 位有符号整数解析:(符号位为0 原码就是补码直接输出)
- 转成十进制 :
1091567616所以此时n的值变成了1091567616,第三句输出n的值为:1091567616。
3. 用 float 解释
这串二进制本来就是 9.0 的 IEEE 754 编码 ,所以按 float 解释就是 9.0 ,第四句输出 *pFloat的值为:9.000000**。**
总结
我们认识到浮点数的存储与整数是完全不一样的 我们要清楚正确认识浮点数的存储中存和取的过程(非常特殊)