笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)

?? 写在前面 经过上篇博客的学习,你已经知道了数据的运算, 那数据在内存中又是如何存储的呢?
今天bug郭就带你一起学习数据在内存中的储存!

点击目录跳转

  • :scissors: 写在前面
  • :100: 本章重点
  • :book: 数据类型介绍
    • :eye:内置类型
    • 类型的基本归类
  • :tm:整形在内存中的存储
    • :heavy_check_mark: 大小端
    • :old_key:判断大小端
    • :punch: 小试牛刀
      • **练习题目**
      • **:pushpin: 练习讲解**
      • :star: 重点归纳总结
  • :sweat_drops: 浮点型在内存中的存储
    • :thumbsup: 浮点数存储方式介绍
    • :trophy: 总结

本章重点
  1. 数据类型详细介绍
  2. 整形在内存中的存储:原码、反码、补码
  3. 大小端字节序介绍及判断
  4. 浮点型在内存中的存储解析
数据类型介绍 那些我们学过的C语言数据类型,你还记得多少,我们一起来整理一一下吧
内置类型
char//字符型 1byte int//整型4byte short//短整型2byte long//长整型4/8byte long long//更长的整型8byte float //单精度浮点型4byte double//双精度浮点型8byte //C语言中无字符串类型

类型的意义
之前的博客中已经介绍过了
  • 类型可以决定该类型的变量在内存中创建内存空间的大小
  • 类型可以决定指针访问的权限,加减指针的位移
我们可以根据我们变量的大小合理选择类型,创建空间大小。
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

不同的数据类型根据它们的字节大小,需要占用不同空间大小的内存空间
类型的基本归类 整型家族
char signed char unsigned char short signed short [int] unsigned short [int] int signed int unsigned int long signed long [int] unsigned long [int] long long signed long long [int] unsigned long long [int]

注意:字符型也归类为整型家族,每个类型都有有符号类型和无符号类型。
浮点数家族
float double

构造类型
//结构体类型 struct //枚举类型 enum //联合类型 union

指针类型
char* int* float* void*

空类型
void

void空类型
通常使用在函数的参数,返回值,指针。
??整形在内存中的存储 我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们来看看整型是如何存储的。
例如:
int a=1; int b=-3;

我们已经知道整型占用内存空间为4个字节。那么是如何分配储存的!
我们先来了解一下计算机中有符号数的三种表示方法:
原码,反码,补码
  • 计算机中有符号数有三种表示方法,原反补。
  • 这三种表示方法,都是由符号位和数值位组成,符号位1表示负数,0表示正数,数值位各不相同!
    原码
直接将数据通过二进制正负的形式翻译过来的的二进制位
反码
由原码,符号位不变,数值位按位取反。
补码
反码+1得到补码!
正数的原反补相同
数据是以补码的形式在内存中存储
为啥是补码呢?
学过计算机原理的同学肯定了解,因为计算机的CPU中运算器(ALU)只能进行加法!所以负数要转化成加法运算,而补码很好的解决了这个问题!
?? 大小端 笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

根据我们之前博客的学习,避免bug,调试技巧我们已经知道了,调试窗口,可以查看变量的地址和内存,我们&x可以查看到x在计算机中内存的储存。
int x=1; //x为整型有32二级制位 //而每4个二进制位是一个16进制位, //x=1的16进制表示方法:00 00 00 01

而我们看到vsx的内存,低位01却存在最左边。
为啥会存到最左边呢?
我们可以看到x占用4个字节空间,地址从左往右依次递增!低地址存低位字节数据,高地址存高位字节数据。
这就是我们所介绍的小端存储。
而大端存储,不言而喻就是,高地址存低位,低地址存高位!
总结:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
为啥会有大小端
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bitchar之外,还有16bitshort型,32bitlong型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如一个16bitshortx ,在内存中的地址为0x0010x 的值为0x1122 ,那么0x11 为高字节,0x22为低字节。对于大端模式,就将0x11 放在低地址中,即0x0010 中,0x22 放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86 结构是小端模式,而KEIL C51 则为大端模式。很多的ARMDSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
总结:
计算机寄存器宽度大于 一个字节,那么就多个字节类型数据的存储就产生了不一样的大小端存储模式。
判断大小端 我们已经知道有大小端两种存储模式,而我们要如何判断一台机器是小端存储,还是大端储存呢?也就是判断当前机器的字节序?
我们可以设计几个程序,来验证该不同机器的字节序。
设计思路
我们可以想办法将某一地址处存的字节数据拿出即可判断,如果高地址低字节位,说明是小端存储,否者就是大端存储模式。
//代码一 //利用char*指针得到低地址的字节数据 #include int main() { int a=1; int *pa=&a; //利用char*存储a第一个字节的低地址 char*pc=(char*)pa; printf("%d",*pc); //访问这个字节的地址,打印数据 return 0; }

笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

低地址打印了低字节位,说明bug郭的机器是采用小端存储模式!
我们刚刚是说写个程序,判断字节序,所以我们需要封装一下!
//代码1 #include int check_sys() {int i = 1; return (*(char *)&i); } int main() {int ret = check_sys(); if(ret == 1) {printf("小端\n"); } else {printf("大端\n"); } return 0; }

我们之前还了解到了一个C语言自定义类型联合体,我们后期还会详细介绍!
联合体就是一块空间,多个变量联合使用,共同占用一块空间!当我们访问其中一个变量,该空间就存储着该变量!
我们可以利用联合体这一特性来判断字节序
//代码2 int check_sys() { union { int i; char c; }un; un.i=1; return un.c; }

笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

学会了吗,这就是大小端的判断!
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

小试牛刀 到这里我们已经学习了整型在内存中如何存储,我们来写几个练习巩固一下吧!
练习题目
下面一共7道题目
大家可以试着练习一下,我会给大家一一讲解
//练习1 //输出什么? #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; }

//练习2 #include int main() {char a = -128; printf("%u\n",a); return 0; }

//练习3 #include int main() {char a = 128; printf("%u\n",a); return 0; }

//练习4 #include int main() {int i= -20; unsignedintj = 10; printf("%d\n", i+j); //按照补码的形式进行运算,最后格式化成为有符号整数 }

//练习5 #include int main() {unsigned int i; for(i = 9; i >= 0; i--) {printf("%u\n",i); } return 0; }

//练习6 int main() {char a[1000]; int i; for(i=0; i<1000; i++) {a[i] = -1-i; } printf("%d",strlen(a)); return 0; }

//练习7 #include unsigned char i = 0; int main() {for(i = 0; i<=255; i++) {printf("hello world\n"); } return 0; }

练习讲解
//练习1 //输出什么? #include int main() {char a = -1; //-1 : 补码 11111111 11111111 11111111 11111111 //截断放入 a:11111111 signed char b = -1; //-1截断放入b中 b: 11111111 unsigned char c = -1; // 同理 c: 11111111 printf("a=%d,b=%d,c=%d", a, b, c); //a和b是有符号字符型,%d打印整型提升补充符号位后 //补码 11111111 11111111 11111111 11111111 //得到原码:10000000 00000000 00000000 00000001 //所以a和b打印结果是-1 //而c是无符号字符型,所以整型提升,补充0 //00000000 00000000 00000000 11111111 //转换原码 00000000 00000000 00000000 11111111 //所以c的打印结果是 225 return 0; }

运行结果
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

//练习2 #include int main() {char a = -128; //-128 原码:00000000 00000000 00000000 10000000 //补码: 11111111 11111111 11111111 10000000 //截断存入char a 中10000000 printf("%u\n", a); //%u无符号的形式打印 //a是有符号char 整型提升补充符号位 // 11111111 11111111 11111111 10000000 //而%u默认该数据为无符号数据,所以认为a的原码补码相同 //打印结果 4294967168 return 0; }

运行结果
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

//练习3 #include int main() {char a = 128; //128 00000000 00000000 00000000 10000000 //截断存入char a :10000000 printf("%u\n", a); //整型提升char a有符号,补符号位 //11111111 11111111 11111111 10000000 //%u无符号的形式打印,认为该数据原码补码相同 //打印 4294967168 return 0; }

笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片
看到练习3的结果和练习2的结果一样,一个是-128,一个是128
但以%u打印了一样的结果!
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

因为无论是128还是-128截断后存储到a都是相同的二进制位!
//练习4 #include int main() {int i = -20; //-20 原码:10000000 00000000 00000000 00010100 //补码 : 11111111 11111111 11111111 11101100 unsignedintj = 10; //10 :00000000 00000000 00000000 00001010 printf("%d\n", i + j); //i+j补码:11111111 11111111 11111111 11110110 //原码:10000000 00000000 00000000 00001010 // 打印 -10 //按照补码的形式进行运算,最后格式化成为有符号整数 }

运行结果
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

//练习5 #include int main() {unsigned int i; //无符号int i 所以始终大于等于0 for (i = 9; i >= 0; i--) {printf("%u\n", i); //无法退出循环 } return 0; }

unsigned int范围:0~2^32
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片
代码会发生死循环!
运行结果
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

//练习6 #include int main() {char a[1000]; int i; for (i = 0; i < 1000; i++) {a[i] = -1 - i; //a[i]是字符型,范围为-128~127 //超过会进行截断存入a[i]中 //当-129存入a[128]中截断 //-129 :原 10000000 00000000 00000000 10000001 //补码11111111 11111111 11111111 01111111 //截断 后存入a[128]中 01111111 //此时a[128]符号位为0故存入的为 127 //后面数据同理 //当a[255]=-256 // -256 原 10000000 00000000 00000001 00000000 //补码 :11111111 11111111 11111111 00000000 // 故此时a[255]存入的是 0 } printf("%d", strlen(a)); //strlen 遇到'\0'停止计数,也就arr[255],所以返回长度为255 return 0; }

笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

char中的范围就是这样的,所以但一个数据小于-128时下一个数据就是127 大于127下一个数据就是-128
运行结果
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

//练习7 #include unsigned char i = 0; //unsigned char范围为0~255 int main() {for (i = 0; i <= 255; i++) {printf("hello world\n"); //当i=255时,i++后,i循环回到i=0 //所以该代码会发生死循环 } return 0; }

笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

运行结果
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

这就是所以练习的答案了,是不是还有点意犹未尽!如果还没学会可以多看几遍!
?? 重点归纳总结
  • 计算机中数据的存储和计算都是以补码的形式进行的!
  • 整型提升还有截断的对象也是针对补码。
  • 无符号整型提升,二级制位补充0,有符号整型提升,二进制位补充符号位。
  • %u(无符号打印)自动认为打印的数据是无符号数据,所以存储的补码也就是原码,%d(有符号打印)认为打印的数据是有符号类型的,要将数据转换成原码打印输出!
浮点型在内存中的存储 我们已经学会了整型在内存中的存储,你肯定会好奇,浮点型数据该怎样存储在内存中呢?
常见的浮点数:
3.14159 1E102.7

浮点数家族包括:
float、double、long double 类型。

浮点数表示的范围:
vsfloat.h有详细介绍浮点数的表示范围,有兴趣的伙伴可以期查阅一下,bug郭截取了一段供大家参考:
// float.h // //Copyright (c) Microsoft Corporation. All rights reserved. // // Implementation-defined values commonly used by sophisticated numerical // (floating point) programs. // #pragma once #ifndef _INC_FLOAT // include guard for 3rd party interop #define _INC_FLOAT#include #pragma warning(push) #pragma warning(disable: _UCRT_DISABLED_WARNINGS) _UCRT_DISABLE_CLANG_WARNINGS_CRT_BEGIN_C_HEADER#ifndef _CRT_MANAGED_FP_DEPRECATE #ifdef _CRT_MANAGED_FP_NO_DEPRECATE #define _CRT_MANAGED_FP_DEPRECATE #else #ifdef _M_CEE #define _CRT_MANAGED_FP_DEPRECATE _CRT_DEPRECATE_TEXT("Direct floating point control is not supported or reliable from within managed code. ") #else #define _CRT_MANAGED_FP_DEPRECATE #endif #endif #endif

大家肯定会疑问,这是个啥,看不懂啊,其实bug郭也看不懂,哈哈哈,不过问题不大!
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

浮点型的其类型说明符有float 单精度说明符,double 双精度说明符。在Turbo C中单精度型占4个字节(32位)内存空间,其数值范围为3.4E-38~3.4E+38,只能提供位有效数字。双精度型占8 个字节(64位)内存空间,其数值范围
1.7E-308~1.7E+308,可提供16位有效数字。
兄弟们,我们写个代码看看,你就了解了浮点型!
#include int main() {int n = 9; float *pFloat = (float *)&n; printf("n的值为:%d\n",n); printf("*pFloat的值为:%f\n",*pFloat); *pFloat = 9.0; printf("num的值为:%d\n",n); printf("*pFloat的值为:%f\n",*pFloat); return 0; }

输出结果会是怎么样的?会是像我们整形数据那样分析吗?
结果肯定是否定的!
运行结果:
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

可以看到打印结果完全出乎我们意料,num*pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。所以我们可以知道,浮点型数据和整型数据在计算机中有着不一样的存储方式!
浮点数存储方式介绍
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
  • (-1)^S * M * 2^E
  • (-1)^s表示符号位,当s=0V为正数;当s=1V为负数。
  • M表示有效数字,大于等于1,小于2
  • 2^E表示指数位。
举例来说:
十进制的5.0,写成二进制是101.0 ,相当于1.01×2^2 。 那么,按照上面V的格式,可以得出s=0M=1.01E=2
十进制的-5.0,写成二进制是-101.0 ,相当于-1.01×2^2 。那么,s=1M=1.01E=2
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

对于double 64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

IEEE 754对有效数字M和指数E,还有一些特别规定。
数据的存入
前面说过,1≤M<2 ,也就是说,M可以写成1.xxxxxx 的形
式,其中xxxxxx表示小数部分。
IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。
比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。
32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int) 这意味着,如果E8位,它的取值范围为0~255;如果E11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10E10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001
然后,指数E从内存中取出还可以再分成三种情况:
  • E不全为0或不全为1
    这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。 比如: 0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^(-1),其阶码为-1+127=126,表示01111110,而尾数1.0去掉整数部分为0,补齐023
    00000000000000000000000,则其二进制表示形式为:
    0 01111110 00000000000000000000000
  • E全为0
    浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
  • E全为1
    这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
好了,关于浮点数的表示规则,就说到这里。
学到这我们了解了浮点数据的存储方式,就可以把刚刚的运行结果解释清楚了!
解析
#include int main() {int n = 9; //n 900000000 00000000 00000000 00001001 float* pFloat = (float*)&n; //*pFloat :00000000 00000000 00000000 00001001 //利用存储公式 (-1)^s * M *2^E //所以 s 为 0为正数 //E 为 00000000全为0 E无需减去127,M无需加上1. //M 000000 00000000 00001001 //所以*pFloat 是一个无限接近0的数 printf("n的值为:%d\n", n); //n的打印结果肯定是9 printf("*pFloat的值为:%f\n", *pFloat); //所以打印结果为0.000000 *pFloat = 9.0; //9.0存入 float中 //9.0: 00001001.0 向左移动3位 00001.001*2^3 //s为0 E为33+127=130 // s 0 // E 10000010 // M 001后面添20个0补齐 00100000000000000000000 //所以存入 *pFloat 数据为 // 0 10000010 00100000000000000000000 printf("num的值为:%d\n", n); //*pFloat解引用操作,将n的值也改变了 //补码 01000001 00010000 00000000 00000000 //转换成10进制 1091567616 printf("*pFloat的值为:%f\n", *pFloat); return 0; }

笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
文章图片

让我们回到一开始的问题:
为什么0x00000009 还原成浮点数,就成了0.000000
首先,将0x00000009 拆分,得到第一位符号位s=0,后面8位的指数E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 0000 1001
9 ->0000 0000 0000 0000 0000 0000 0000 1001
由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成: V=(-1)^0×0.00000000000000000001001×2^(-126)=1.001×2^(-146) 显然,V是一个很小的接近于0的正数,所以用十进制小
数表示就是0.000000
再看例题的第二部分。
请问浮点数9.0,如何用二进制表示?还原成十进制又是多少?
首先,浮点数9.0等于二进制的1001.0,即1.001×2^3
那么,第一位的符号位s=0,有效数字M等于001后面再加200,凑满23位,指数E等于3+127=130,即10000010。 所以,写成二进制形式,应该是s+E+M,即
01000001 00010000 00000000 00000000
【笔记|一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)】这个32位的二进制数,还原成十进制,正是1091567616
总结
  • 浮点数存入时,由于指数有可能是负数,所以统一单精度指数加上127,双精度加上1023存入E
  • M有二进制位不足时采用右边补位补0补齐M
  • E全为01时为特殊情况!
兄弟们看到这里那就收藏一下吧!

    推荐阅读