C语言数据在内存中的存储

目录

  • 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 复制代码
低地址  →  高地址

现在要把 1122 放进去。

有两种放法:

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语言深入浅出3strlen函数统计字符串中 '\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;
}

画图演示:



相关推荐
Scott9999HH8 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社9 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海9 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖10 小时前
JDK 26 新特性详解
java·开发语言
马优晨11 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区12 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大13 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai13 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜13 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa14 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio