💛 前情提要💛
经过前面一系列的学习,想必大家对于C语言有一定了解了吧 😚
本篇文章主要带大家认识C语言中的更深层的知识——数据在内存中是如何的存储
接下来我们即将进入一个全新的空间,对代码有一个全新的视角~
以下的内容一定会让你对C语言有一个颠覆性的认识哦!!!
以下内容干货满满,跟上步伐吧~
作者介绍:
🎓作者:热爱编程不起眼的小人物🐐
🔎作者的Gitee:代码仓库
📌系列文章推荐:
- 实现Strcpy函数 - 通过函数发现 “程序之美” | 不断优化、优化、再优化~
- 【第一章】 C语言之牛客网刷题笔记【点进来保证让知识充实你一整天】
- 【第二章】 C语言之牛客网刷题笔记 【点进来保证让知识充实你一整天】
📒我和大家一样都是初次踏入这个美妙的“元”宇宙🌏 希望在输出知识的同时,也能与大家共同进步、无限进步🌟
导航小助手
- 深度剖析数据在内存中的存储
- 本章重点
- 一.数据类型的详细认识
- 二.整形在内存中的存储
- Ⅰ.原码、反码、补码
- 三.大小端字节序
- Ⅰ.大小端的认识与理解
- Ⅱ.判断“字节序”的小程序
- 四.原码、反码、补码实践应用
- Ⅰ.(有/无符号)`字符类型`的取出
- Ⅱ.无符号类型打印
- Ⅲ.char与unsigned char的规律
五、浮点型在内存中的存储
- 总结
深度剖析数据在内存中的存储 本章重点
数据类型
的详细认识- 深入认识整形在内存中的存储:
原码
、反码
、补码
大小端字节序
的理解及判断浮点型
在内存中的存储解析
一.数据类型的详细认识
经过C语言的学习,想必我们已经见过不少的数据类型
我们可以将它们分为几大类:
- 整形家族:
char
unsigned char - 无符号字符类型
signed char - 有符号字符类型
short
unsigned short [int] - 无符号短整型类型
signed short [int] - 有符号短整型类型
int
unsigned int - 无符号整型类型
signed int - 有符号整型类型
long
unsigned long [int] - 无符号长整形类型
signed long [int] - 有符号长整型类型
- 浮点数家族:
float - 单精度浮点型类型
double - 双精度浮点型类型
- 构造类型:
数组类型:
struct - 结构体类型
enum - 枚举类型
union - 联合类型
- 指针类型
int* p;
char* p;
float* p;
void* p;
- 空类型
void* 通常表示“空类型”,即“无类型”
【通常用于函数的返回类型、函数的参数、指针类型等等】
【前情提要:void*类型到后续学习中我们可以看到它的强大之处】
有的同学可能看见“char-字符类型”
在整型家族
中,可能有疑问:“为什么char类型属于整型呢”?那是因为:在C语言这古老的语言中,并没有像Python这类新兴语言整合了 “字符串类型”
,所以C语言对于字符类型的本质上:是在用字符所对应的ASCII进行计算or存储
,所以对于字符类型底层还是在用数字去对应ASCII表去对应转换使用
有了上述对类型的认知,接下来我们将深入探索“整型”在内存是如何存储的 |
之前我们已经了解到一个变量的创建是要在内存中开辟空间的,而空间的大小是根据不同的类型而决定的
而现在我们将看到所存储的数据在开辟的内存中是如何存储的
比如:
int a = 20;
我们都知道存储整型需要开辟4个字节的空间
那在空间中是如何存储的呢?
int b = -10;
Ⅰ.原码、反码、补码
对于整型的存储方式,是数据在内存中以二进制
的方式存储的;
对于整型来说:整型的二进制
有三种
表示形式:原码
、反码
、补码
而我们所看到的表现形式【即
数字
本身】是由二进制原码
转换为十进制而来的
而真正存进内存里的,是数据的
二进制补码
其中它们都是32位二进制的表示方式存进内存
- 对于
正整数
来说:原码、反码、补码相同【不需要相互转换】 - 对于
负整数
来说:原码、反码、补码都要进行相应的计算转换 【下列展示的方法】 - 原码、反码、补码这三种表示方法均有符号位和数值位两部分
-
原码
:根据数值直接写出的32位二进制序列 -
反码
:原位的符号位(即第一位)不变,其它位按位取反(即0变1,1变0) -
补码
:反码最后一位+1,即可得:
1.原码、反码、补码的符号位为32位二进制中的第一位:正数的符号位为:0
、负数的符号位为:1
2.负整数的符号位不需要参与计算的【即转换为十进制的时候不需要考虑符号位上的1】
为什么说数据存进内存中其实放的是:补码
在计算机系统中,数值一律用补码来表示和存储。
原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理 (CPU只有加法器) 此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
有了上述的了解后,我们再看回实例:
int a = 20;
原码:00000000000000000000000000010100
反码:00000000000000000000000000010100
补码:00000000000000000000000000010100
补码【十六进制】:00 00 00 14
int b = -10;
原码:10000000000000000000000000001010
反码:11111111111111111111111111110101
补码:11111111111111111111111111110110
补码【十六进制】:ff ff ff f6
不难发现,对比我们书写的数据在内存中的补码的存储顺序与真正在内存中存储的顺序好像有点不一样,为什么是反着存储的呢?
以下,就关联到我们要学习的大小端字节序
什么是大小端:
1.大端(存储)模式:
是指数据的低位保存在内存的高地址处,而数据的高位,保存在内存的低地址中。
2.小端(存储)模式:
是指数据的低位保存在内存的低地址处,而数据的高位,保存在内存的高地址中。
但是我们还是会不由自主地发出两个疑问:
- 为什么会有
“大端和小端”
- 什么是
字节序
现在我们就来一一解答:
- 之所以会有大小端模式之分,是因为:
在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为
8bit
,但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器)另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。
因此就导致了大端存储模式和小端存储模式。
2.字节序:
字节在内存中存储的顺序,Eg:高位字节存储在内存中的低地址处 or 低位字节存储在内存中的高字节处……
下面我们来真实的看一下实际案例吧~
所以不难看出,此编译器的存储方式为:小端模式存储
特别注意: 大小端(模式)存储只与编译器有关,是编译器去决定使用大端存储方式or小端存储方式
那当我们遇到不同编译器的时候,我们又如何去分辨这个编译器用的是哪种模式的存储呢?
此时我们便可以书写一个“判断大小端的程序”
当然,这个小程序不仅是我们篇目的需求,更是在曾经百度2015年系统工程师笔试题
中出现过
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
(10分)
这也就是为什么我们需要多积累
、增强内功
- 我们先部署好环境
int main()
{
int flag = system_check(); //判断机器小端or大端的函数
if (flag == 1)
{
printf("大端(模式)存储\n");
}
else
{
printf("小端(模式)存储\n");
}
return 0;
}
- 开始书写
“判断机器大小端存储的小程序”
int system_check()
{
int test = 1;
//用1好方便测试
//1的字节为:00 00 00 01
//要判断在内存中的字节序的话:
//我们只需要“拿出第一个字节的内容,判断它是1 or 0即可”
char* p = (char*)&test;
//因为test里存的是整型的地址
//所以需要“强制类型转换”才能存进p这个字符指针里
//因为要拿出第一个字节的内容,所以要用到char*
//【char*这个指针类型决定了指针一次性能访问几个字节】
return *p;
}
- 整体合起来就是:
int system_check()
{
int test = 1;
char* p = (char*)&test;
return *p;
}
int main()
{
int flag = system_check(); // 判断机器小端or大端的函数
if (flag == 1)
{
printf("小端(模式)存储\n");
}
else
{
printf("大端(模式)存储\n");
}
return 0;
}
既然有以上这些的了解,那让我们看点类型来巩固一下吧~
四.原码、反码、补码实践应用 Ⅰ.(有/无符号)
字符类型
的取出
#include
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;
}
以上代码会输出什么呢?
上面覆盖了三方面的内容:
整型提升
(有/无符号)字符类型
取出负整数
在内存中的转换
以上内容我们一个一个来分析:
【在此之前我们需要了解一下char类型】
- 对于char类型,只能存储
2
个字节的内容【即 只能拿8
bit的内容(而且是从后往前拿补码的8个bit)】 - 对于
数据
,存储进去内存的格式全是以32位二进制序列
来存储的,只有拿的时候才会收到不同的要求【eg:无符号和有符号的拿取方式、类型的空间大小不同导致拿取的字节个数不同……】
有了上述的前提
下,我们便可以更加深入的一步一步分析:
char a = -1;
原码:10000000000000000000000000000001
反码:11111111111111111111111111111110
补码:11111111111111111111111111111111
所以真正存进a里的为:11111111(补码后8位【2个字节】)
signed char b = -1;
原码:10000000000000000000000000000001
反码:11111111111111111111111111111110
补码:11111111111111111111111111111111
所以真正存进b里的为:11111111(补码后8位【2个字节】)
unsigned char c=-1;
原码:10000000000000000000000000000001
反码:11111111111111111111111111111110
补码:11111111111111111111111111111111
所以真正存进c里的为:11111111(补码后8位【2个字节】)
所以我们可以看见:存进去的方式其实是一样
的,只是拿出来理解的方式不一样
【即使为8bit
– 第一位永远是符号位(对于负数来说符号位不参与运算)】
然后我们开始进行下一步:打印
printf("a=%d,b=%d,c=%d",a,b,c);
由此我们可知:三个字符类型的都要以%d
的形式打印
即要以整型
的方式打印出来【(32位二进制序列)需要"整型提升"
】
-
特别注意:
整型提升
只对整型家族
有效,其中只有short
类型和char
类型需要整型提升 -
整型提升
是按符号位
进行提升
有了以上补充,我们再来看看:
%d 打印 char a;
a的补码:11111111
【因为是以“整型”去打印的,且打印出来的为“原码”翻译的】
(提升为32位)补码:11111111111111111111111111111111
反码:10000000000000000000000000000
原码:10000000000000000000000000001
所以打印出来的:-1
同理,signed b是一样的道理
打印出来位:-1
注意!以下有所不同:【总体 *** 作一样,但其中有一步的理解不一样】
无符号类型
默认变量存储的数据(8个bit)为无符号数
【整个数以“正数看待”】
正因为有了上述的描述:
- 无符号类型默认
没有符号位
,即将转换后的补码
看作是没有符号位
的,所以整型提升的时候是以0
来进行提升的【它的作用范围
:仅在提升的时候起作用,打印怎么样的与它无关】
%d 打印 unsigned char c
补码:11111111
整型提升后:00000000000000000000000011111111
【因为符号位为0,则为正数:原、反、补码相同】
原码:00000000000000000000000011111111
打印出来为:255
是不是很有意思呢~
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
本题与上题的不同在于:
- 上题是以
无符号类型
的视角去存放,有符号整型
的视角打印 - 本题是以
有符号类型
的视角去存放,无符号类型
的视角打印
【无符号类型
视角:认为补码
(无论是否为符号位为1 or 0)都是正数的视角
打印出来的,仅仅在看32位补码的时候起作用,前面的提升
都与它无关,它只管 打印
】
a = -128
原码:10000000000000000000000010000000
反码:11111111111111110111111101111111
补码:11111111111111111111111110000000
真正存储的补码:10000000
%u打印 -- 无符号类型的视角打印
补码:10000000
【转换的时候只需要考虑自己是什么类型,先不用思考是什么类型打印的】
(提升后)补码:11111111111111111111111110000000
以“无符号”视角拿出来 -- 所以认为这个补码是"正数"
原码=反码=补码
打印出来为:4294967168
经过上述的了解,我们就可以非常的熟悉%u
与unsigned
的规则啦~
如果还不太熟练的话,建议逐字分析,一定会有收获哒~
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
分析以上题目,我们得先分析两个点:
-
char
类型的大小数值规律 -
strlen
是如何计算的 -
对于
strlen
来说,它的目的就是要找到
(即0
strlen
)就停止计算长度【注意:0
不会把char
算进长度中】 -
对于
-128~127
类型的取值范围:“-128”
其中10000000
是特殊的,因为在32位二进制序列的“-128”的补码后八位为char类型
,所以char类型就会截断这后八位下来
所以“-128”还是可以存进char
的空间里,只是存进了部分而已,所以内存直接对于10000000
里的-128
直接转换过来为char类型
-
TIPS:我们可以将
循环
的取值范围作为一个规律记下来,因为它的取值范围可以算作一个unsigned char
-
对于
0~255
类型的取值范围:256
,一共unsigned char
个数【本质上char
和unsigned
的取值区间长度是一样,对于256
的视角,将原本负数变成正数了,即:127+128 =0
】
让我们看回题目:
- 它会一直打印:-1、-2、-3……-127、-128、127、126、……、3、2、1、
255
【当它找到0的时候就会截止,所以一共计算了整型
次】
以上内容就是对于浮点型
在内存中的存储啦~
接下来让我们再来看看浮点型
在内存中存储又有什么不一样吧~
五、浮点型在内存中的存储
特别注意:
-
整型
和存储方式
在内存中的解读方式
和S
【存进去和拿出来】都有区别
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^
M
*E
* 2^s
- ( -1)^
符号位
表示M
,当s=0,V为正数;当s=1,V为负数。
-有效数字
表示E
,大于等于1,小于2。
- 2^
指数位
表示浮点型
。
通俗易懂来说:就是
二进制形式的科学计数法存储的
在内存中是以S
,将浮点数分成三个部分:M
、E
、浮点数
- 对于
s
来说,只需要存好 这三个数据,那浮点数就会很好的还原出来了
在内存中,这三个数的分布如下:
- 对于32位的浮点数,最高的1位是符号位
E
,接着的8位是指数M
,剩下的23位为有效数字S
- 对于64位的浮点数,最高的1位是符号位
E
,接着的11位是指数M
,剩下的52位为有效数字S
使用规则:
-
第一位(
符号位
)控制的是E
:0代表正数,1代表负数 -
对于
指数
(控制的是中间数
,即2的几次方):E被规定为一个无符号整数,所以即使是2^-1、 2^-2……不能直接存-1、-2……进去,即使是指数是正数也得加上一个内存中的E的位数的第一位为1的时候,刚好就是中间数【对于8位的E,中间数为:127、对于11的E,中间数为:1023】再转换成二进制序列存进内存
TIPS:
实际指数
8 bit:10000000 = 127
11 bit:10000000000 = 1023所以我们只看
10000000
是多少,转换为二进制序列后直接在10000000000
或者M
加上去即可
- 对于
小数点的数字
(表示的是二进制形式的科学计数法
):因为已经书写成二进制形式的科学计数法(1<=M<2),所以一般省略了小数点前面的数字【这样可以多出一个bit的空间去保留更多有效数字】
- 存进内存的时候:直接将浮点数转换成
M
中的小数点后的数字从左到右填进0
的空间即可【位数不够则填1.
补充满空间即可】(等读取的时候,再直接加上浮点数9.0
即可)
有了以上的了解后,让我们来实践实践吧:
请问9.0,如何用二进制表示?还原成十进制又是多少?
1001.0 -> ( ->-1)^01.0012^3= -> s0,= M1.001,=E3+127=130第一位的符号位s=0
那么
- 有效数字M等于001后面再加20个0,凑满23位
- 指数E等于3+127=130,即10000010
S
写成二进制形式,就是:E
+M
+0,在内存就是:
10000010 001 00000000000000000000 浮点型
最后要注意:
- 正是因为
整型
和存取方式
的%d
有所不同,所以这也是为什么%f/%lf
和整型
来分别读取浮点数
和“数据在内存中是如何的存储”
总结
综上,我们已经深入了解了C语言中的 🍭 的知识啦~~
恭喜你的内功又双叒叕得到了提高!!!
感谢你们的阅读😆
后续还会继续更新💓,欢迎持续关注📌哟~
💫如果有错误❌,欢迎指正呀💫
✨如果觉得收获满满,可以点点赞👍支持一下哟~✨
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)