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;
}

画图演示:



相关推荐
basketball6166 小时前
C++面试考点 头文件与实现文件形式
开发语言·c++
SilentSamsara6 小时前
类型注解进阶:Union、Optional、Any 与 Callable
开发语言·python·青少年编程
历程里程碑6 小时前
56 . 高效ET非阻塞IO服务器设计指南
java·运维·服务器·开发语言·数据结构·c++·排序算法
恣艺6 小时前
Python 游戏开发与文件处理:PyGame + Turtle + openpyxl + python-docx + PyPDF2
开发语言·python·pygame
高林雨露6 小时前
kotlin 相关code
开发语言·kotlin
我还记得那天6 小时前
函数的递归调用
c语言·开发语言·visualstudio
zhangfeng11336 小时前
ThinkPHP5 事件系统的标准最佳实践 事件系统的完整设计逻辑tags.php tags.php(事件地图)
android·开发语言·php
xyq20246 小时前
HTML 标签简写及全称
开发语言
tongluowan0076 小时前
数据结构 Bitmap(位图)示例 - 用户签到系统
开发语言·数据结构·bitmap·用户签到系统