目录
- 1.整数在内存中的存储
- 2.大小端字节序与字节序判断
-
- [2.1 什么是大小端](#2.1 什么是大小端)
- [2.2 为什么有大小端?](#2.2 为什么有大小端?)
- [2.3 练习](#2.3 练习)
-
- [2.3.1 练习1](#2.3.1 练习1)
- [2.3.2 练习2](#2.3.2 练习2)
- [2.2.3 练习3](#2.2.3 练习3)
- [2.2.4 练习4](#2.2.4 练习4)
- [2.2.5 练习5](#2.2.5 练习5)
- [2.2.6 代码6](#2.2.6 代码6)
- [3. 浮点数在内存中的存储](#3. 浮点数在内存中的存储)
-
- [3.1 练习](#3.1 练习)
- [3.2 浮点数的存储](#3.2 浮点数的存储)
-
- [3.2.1 浮点数存的过程](#3.2.1 浮点数存的过程)
- [3.2.2 浮点数取的过程](#3.2.2 浮点数取的过程)
- [3.3 题目解析](#3.3 题目解析)

1.整数在内存中的存储
在C语言操作符详解中,我们就讲过下面的知识点,下面我把之前的知识点简单回顾一下:
计算机中,整数以 二进制 形式存储。整数的二进制表示方式主要有三种:
| 表示方式 | 说明 |
|---|---|
| 原码 | 直接按照正负和数值表示 |
| 反码 | 在原码基础上按规则转换 |
| 补码 | 整数在内存中的实际存储形式 |
无符号数 只能表示非负数 ,所有二进制位都用于表示数值。
c
unsigned int a = 10;
有符号数 可以表示正数、负数和 0 。最高位 是 符号位:
| 符号位 | 含义 |
|---|---|
| 0 | 正数 |
| 1 | 负数 |
其余位表示数值大小 。原码就是直接根据数值写出的二进制形式。以 8 位二进制为例:
text
+5 的原码:00000101
-5 的原码:10000101
其中,负数最高位为 1,表示负号。反码的规则如下:
| 数值类型 | 反码规则 |
|---|---|
| 正数 | 原码、反码相同 |
| 负数 | 符号位不变,其余位按位取反 |
例如:
text
-5 的原码:10000101
-5 的反码:11111010
补码的规则如下:
| 数值类型 | 补码规则 |
|---|---|
| 正数 | 原码、反码、补码相同 |
| 负数 | 反码加 1 |
例如:
text
-5 的原码:10000101
-5 的反码:11111010
-5 的补码:11111011
正数的三种表示形式相同。
text
+5 的原码:00000101
+5 的反码:00000101
+5 的补码:00000101
负数的三种表示形式不同
text
-5 的原码:10000101
-5 的反码:11111010
-5 的补码:11111011
整数在内存中实际存储的是 补码。也就是说:
text
正数存储的是补码
负数存储的也是补码
由于正数的原码、反码、补码相同,所以正数看起来像是直接存储原码,而负数在内存中存储的是补码,不是原码。
减法可以转换为加法:
text
a - b = a + (-b)
这样 CPU 只需要使用加法器 即可完成加法和减法 。在补码中 ,符号位和数值位 可以统一参与计算。这样可以简化计算机硬件设计。负数补码转原码的规则:
text
符号位不变,其余位取反,再加 1
例如:
text
-5 的补码:11111011
取反后:10000100
加 1 后:10000101
得到:
text
-5 的原码:10000101
2.大小端字节序与字节序判断
了解整数在内存中的存储后,我们来看一个代码:
c
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
int a = 0x11223344;
return 0;
}
调试看一下细节:

我们发现了a中这个0x11223344是按照字节为单位,倒着存储的,接下来我们就要通过学习下面的知识来解释原因。
2.1 什么是大小端
当数据超过一个字节时,在内存中的存储就会涉及到字节(存储)顺序问题,按照不同的存储顺序,我们分为大端字节存储和小端字节存储,下面我们来讲解一下:
大端存储模式:
是指数据的高位字节放在低地址处,低位字节放在高地址处。
画图演示:

小端存储模式:
数据的低位字节存放在低地址处,高位字节存放在高地址处。
画图演示:

2.2 为什么有大小端?
计算机内存是按 字节 来编号的。也就是说:
text
一个内存地址 = 一个字节
但是很多数据不止 1 个字节。
例如:
c
short x = 0x1122;
short 通常占 2 个字节,也就是:
text
0x11 0x22
这时问题就来了:
这两个字节放进内存时,谁放低地址?谁放高地址?
于是就产生了 大端模式 和 小端模式 。
大端模式 简单记忆:高位在前,低位在后
小端模式 简单记忆:低位在前,高位在后
因为不同 CPU 的设计习惯不同。有些处理器认为:高位字节先存更直观 ,这就是大端模式 。有些处理器认为:低位字节先存更方便计算 ,这就是小端模式 。
例如:
| 架构 | 常见存储方式 |
|---|---|
| x86 | 小端 |
| ARM | 多数为小端,也可支持大端 |
| Keil C51 | 大端 |
可以把数据 0x1122 看成一个两位数:
text
11 22
内存像一排格子:
text
低地址 → 高地址
现在要把 11 和 22 放进去。
有两种放法:
text
大端:11 22
小端:22 11
所以,大小端本质上就是:
多字节数据在内存中按什么顺序存放的问题。
2.3 练习
2.3.1 练习1
请简述⼤端字节序和⼩端字节序的概念,设计⼀个⼩程序来判断当前机器的字节序。(10分)- 百度笔试题
c
#include <stdio.h>
int check_sys()
{
int a = 1;
if (*((char*)(&a)) == 1)
return 1;
else
return 0;
}
int main()
{
int a = 0x00000001;
if (check_sys() == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
画图演示:


2.3.2 练习2
c
#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;
}

这里涉及到了整型提升的知识点,在C语言操作符详解中讲到过,点击链接就能跳转。
画图演示:

2.2.3 练习3
c
//代码1
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
//代码2
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
画图演示:

2.2.4 练习4
c
#include <stdio.h>
#include <string.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
这道题要用到之前strlen库函数的相关知识点和数组名 的相关理解,对应知识点链接:字符函数与字符串函数,C语言深入浅出3:strlen函数统计字符串中 '\0' 之前的字符个数,在这道题上就是数字0之前的个数。
画图推演:

为什么char(signed char)类型的取值范围是-128 ~ 127,unsigned char的取值范围是0 ~ 255呢?
这里我来画一个轮盘 来演示原因:

2.2.5 练习5
c
//代码1
#include <stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
//代码2
#include <stdio.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
想必通过上面两个题的讲解,这两个题就运用到了char(signed char)类型的取值范围是-128 ~ 127,unsigned char的取值范围是0 ~ 255的原理,我再来画图推演:

2.2.6 代码6
c
#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;
}
画图推演:

3. 浮点数在内存中的存储
我们已知范围内常见的浮点数3.1415926、2E5等,浮点型家族包括:float、double、long double类型,浮点数表示的范围:float.h 中定义。
3.1 练习
c
#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;
}

也许许多人的答案都以为是9,而结果却不全是9,为了弄懂原因,下面我们来学浮点数在内存中是如何存储的。
3.2 浮点数的存储
计算机中,浮点数通常按照 IEEE 754 标准 存储。根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个**⼆进制浮点数V**可以表⽰成下⾯的形式:
V = ( − 1 ) S × M × 2 E V = (-1)^S \times M \times 2^E V=(−1)S×M×2E
其中:
-
( − 1 ) S (-1)^S (−1)S 表示符号位
- 当 S = 0 S = 0 S=0 时, V V V 为正数
- 当 S = 1 S = 1 S=1 时, V V V 为负数
-
M M M 表示有效数字
- M M M 大于等于 1 1 1
- M M M 小于 2 2 2
-
2 E 2^E 2E 表示指数位
举例1:
十进制的 6.5 6.5 6.5,写成二进制是:
6.5 10 = 110.1 2 6.5_{10}=110.1_2 6.510=110.12
将其规格化:
110.1 2 = 1.101 2 × 2 2 110.1_2 = 1.101_2 \times 2^2 110.12=1.1012×22
所以,按照上面的 V V V 的格式,可以得到:
S = 0 , M = 1.101 , E = 2 S=0,\quad M=1.101,\quad E=2 S=0,M=1.101,E=2
举例2:
十进制的 − 0.75 -0.75 −0.75,写成二进制是:
− 0.75 10 = − 0.11 2 -0.75_{10}=-0.11_2 −0.7510=−0.112
将其规格化:
− 0.11 2 = − 1.1 2 × 2 − 1 -0.11_2 = -1.1_2 \times 2^{-1} −0.112=−1.12×2−1
所以,按照上面的 V V V 的格式,可以得到:
S = 1 , M = 1.1 , E = − 1 S=1,\quad M=1.1,\quad E=-1 S=1,M=1.1,E=−1
IEEE 754规定:
对于32位的浮点数(float),最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数(double),最⾼的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M

3.2.1 浮点数存的过程
IEEE 754 对有效数字 M M M 和指数 E E E 有一些特殊规定。
前面说过,浮点数可以表示为: V = ( − 1 ) S × M × 2 E V=(-1)^S\times M\times 2^E V=(−1)S×M×2E
其中, S S S 表示符号位, M M M 表示有效数字, E E E 表示指数。
对于规格化浮点数,有: 1 ≤ M < 2 1\leq M<2 1≤M<2
也就是说, M M M 一定可以写成: 1. x x x x x x 1.xxxxxx 1.xxxxxx
其中, x x x x x x xxxxxx xxxxxx 表示小数部分。
IEEE 754 规定,在计算机内部保存 M M M 时,默认这个数的第一位是 1 1 1,因此可以被省略,只保存后面的 x x x x x x xxxxxx xxxxxx 部分。
例如: 1.01 1.01 1.01
在内存中只保存: 01 01 01
因为最高位的 1 1 1 默认存在,不需要真正存进去。这样做的好处是可以节省 1 1 1 位空间。
以 32 位浮点数为例,尾数部分原本只有 23 23 23 位,但是因为最高位的 1 1 1 被省略,所以实际相当于可以表示 24 24 24 位有效数字。
至于指数 E E E,情况稍微复杂一些。
首先, E E E 在内存中是按照无符号整数保存的。
对于 float 类型,指数位有 8 8 8 位,取值范围是: 0 ∼ 255 0\sim255 0∼255
对于 double 类型,指数位有 11 11 11 位,取值范围是: 0 ∼ 2047 0\sim2047 0∼2047
但是,科学计数法中的指数可能是正数,也可能是负数。因此,IEEE 754 规定,存入内存时,真实指数需要加上一个偏移量。
对于 float 类型,偏移量是: 127 127 127
所以: E 存储 = E 真实 + 127 E_{\text{存储}}=E_{\text{真实}}+127 E存储=E真实+127
对于 double 类型,偏移量是: 1023 1023 1023
所以: E 存储 = E 真实 + 1023 E_{\text{存储}}=E_{\text{真实}}+1023 E存储=E真实+1023
例如,十进制数 5.0 5.0 5.0 转换成二进制为: 5.0 10 = 101.0 2 5.0_{10}=101.0_2 5.010=101.02
将其写成规格化形式: 101.0 2 = 1.01 2 × 2 2 101.0_2=1.01_2\times 2^2 101.02=1.012×22
因此可以得到: S = 0 , M = 1.01 , E 真实 = 2 S=0,\quad M=1.01,\quad E_{\text{真实}}=2 S=0,M=1.01,E真实=2
如果使用 float 类型保存,指数需要加上偏移量 127 127 127: E 存储 = 2 + 127 = 129 E_{\text{存储}}=2+127=129 E存储=2+127=129
将 129 129 129 转换成二进制: 129 10 = 10000001 2 129_{10}=10000001_2 12910=100000012
尾数部分只保存小数点后面 的 01 01 01,并在后面补 0 0 0 到 23 23 23 位:
M = 01000000000000000000000 M=01000000000000000000000 M=01000000000000000000000
所以, 5.0 5.0 5.0 在 float 中的存储结果为: 0 10000001 01000000000000000000000 0\ 10000001\ 01000000000000000000000 0 10000001 01000000000000000000000
其中:
S = 0 S=0 S=0
E = 10000001 E=10000001 E=10000001
M = 01000000000000000000000 M=01000000000000000000000 M=01000000000000000000000
如果保存的是 − 5.0 -5.0 −5.0,由于数值部分相同,只是符号变为负数,所以: S = 1 S=1 S=1
指数位和尾数位不变:
E = 10000001 E=10000001 E=10000001
M = 01000000000000000000000 M=01000000000000000000000 M=01000000000000000000000
因此, − 5.0 -5.0 −5.0 在 float 中的存储结果为: 1 10000001 01000000000000000000000 1\ 10000001\ 01000000000000000000000 1 10000001 01000000000000000000000
总结来说,浮点数的存储过程可以概括为:
十进制小数 → \rightarrow → 二进制小数 → \rightarrow → 规格化形式 → \rightarrow → 拆分出 S S S、 E E E、 M M M → \rightarrow → 按照 IEEE 754 存入内存。
其中最关键的三点是:
S S S:表示正负, 0 0 0 表示正数, 1 1 1 表示负数。
M M M:只保存小数部分,最高位的 1 1 1 默认存在。
E E E:存储的不是原始指数,而是加上偏移量后的结果。
即:
float: V = ( − 1 ) S × 1. M × 2 E − 127 V=(-1)^S\times 1.M\times 2^{E-127} V=(−1)S×1.M×2E−127
double: V = ( − 1 ) S × 1. M × 2 E − 1023 V=(-1)^S\times 1.M\times 2^{E-1023} V=(−1)S×1.M×2E−1023
注意:有的浮点数是无法精确存储的,看如下代码:
c
int main()
{
int a = 1.2;
return 0;
}
画图分析:

3.2.2 浮点数取的过程
浮点数从内存中取出时,并不是直接把二进制当作整数读取,而是先按照 IEEE 754 的格式拆分出: S S S、 E E E、 M M M
其中:
S S S 表示符号位, E E E 表示指数位, M M M 表示尾数位。
取出浮点数时,主要分为三种情况。
一、 E E E 不全为 0 0 0,也不全为 1 1 1
这是最常见的情况,表示规格化浮点数。
此时,指数位 E E E 先转成十进制,然后减去偏移量,得到真实指数。
对于 float: E 真实 = E 存储 − 127 E_{\text{真实}}=E_{\text{存储}}-127 E真实=E存储−127
对于 double: E 真实 = E 存储 − 1023 E_{\text{真实}}=E_{\text{存储}}-1023 E真实=E存储−1023
尾数部分需要在前面补上默认的 1 1 1,也就是: M = 1. x x x x x x M=1.xxxxxx M=1.xxxxxx
所以真实值为:
float: V = ( − 1 ) S × 1. M × 2 E − 127 V=(-1)^S\times 1.M\times 2^{E-127} V=(−1)S×1.M×2E−127
double: V = ( − 1 ) S × 1. M × 2 E − 1023 V=(-1)^S\times 1.M\times 2^{E-1023} V=(−1)S×1.M×2E−1023
例如: 0 01111110 00000000000000000000000 0\ 01111110\ 00000000000000000000000 0 01111110 00000000000000000000000
其中:
S = 0 S=0 S=0
E = 01111110 2 = 126 E=01111110_2=126 E=011111102=126
M = 00000000000000000000000 M=00000000000000000000000 M=00000000000000000000000
因为这是 float,所以: E 真实 = 126 − 127 = − 1 E_{\text{真实}}=126-127=-1 E真实=126−127=−1,尾数前面补默认的 1 1 1: M = 1.0 M=1.0 M=1.0
因此: V = ( − 1 ) 0 × 1.0 × 2 − 1 V=(-1)^0\times 1.0\times 2^{-1} V=(−1)0×1.0×2−1,即: V = 0.5 V=0.5 V=0.5
二、 E E E 全为 0 0 0
当指数位全为 0 0 0 时,表示非规格化数或者 0 0 0。
此时尾数前面不再补 1 1 1,而是补 0 0 0: M = 0. x x x x x x M=0.xxxxxx M=0.xxxxxx,指数不再使用: E 存储 − 127 E_{\text{存储}}-127 E存储−127
而是固定为:
float: E 真实 = 1 − 127 = − 126 E_{\text{真实}}=1-127=-126 E真实=1−127=−126
double: E 真实 = 1 − 1023 = − 1022 E_{\text{真实}}=1-1023=-1022 E真实=1−1023=−1022
所以 float 的计算公式为: V = ( − 1 ) S × 0. M × 2 − 126 V=(-1)^S\times 0.M\times 2^{-126} V=(−1)S×0.M×2−126
如果 E E E 全为 0 0 0,并且 M M M 也全为 0 0 0,则表示: + 0 +0 +0 或 − 0 -0 −0,符号由 S S S 决定。
例如: 0 00000000 00000000000000000000000 0\ 00000000\ 00000000000000000000000 0 00000000 00000000000000000000000,表示: + 0 +0 +0
而: 1 00000000 00000000000000000000000 1\ 00000000\ 00000000000000000000000 1 00000000 00000000000000000000000,表示: − 0 -0 −0
三、 E E E 全为 1 1 1
当指数位全为 1 1 1 时,表示特殊值。
对于 float 来说,指数位全为 1 1 1 即: E = 11111111 E=11111111 E=11111111
对于 double 来说,指数位全为 1 1 1 即: E = 11111111111 E=11111111111 E=11111111111
这时需要看尾数 M M M。如果: M = 0 M=0 M=0,则表示无穷大。
当 S = 0 S=0 S=0 时: + ∞ +\infty +∞
当 S = 1 S=1 S=1 时: − ∞ -\infty −∞
例如: 0 11111111 00000000000000000000000 0\ 11111111\ 00000000000000000000000 0 11111111 00000000000000000000000,表示: + ∞ +\infty +∞
而: 1 11111111 00000000000000000000000 1\ 11111111\ 00000000000000000000000 1 11111111 00000000000000000000000,表示: − ∞ -\infty −∞
如果: M ≠ 0 M\neq 0 M=0,则表示: N a N NaN NaN,也就是"不是一个数字"。
3.3 题目解析
我们再来看来 3.1 3.1 3.1中的练习,就知道该怎么写了。
c
#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;
}
画图演示:


完