C语言进阶|人人都看得懂的C语言进阶系列之数据存储


数据在内存中的存储

  • 一、数据类型
    • 1.内置类型及意义
    • 2.类型的基本分类
      • 2.1.整型
      • 2.2.浮点型
      • 2.3.构造类型
      • 2.4.指针类型
      • 2.5.空类型
  • 二、整型在内存中的存储
    • 1.原码、反码、补码
    • 2.补码的意义
    • 3.大小端字节序
    • 4.练习题(营养鸡汤)
  • 三、浮点型在内存中的存储
    • 1.浮点数表示规定
    • 2.浮点数存储规定
    • 3.浮点数取出规定

一、数据类型 1.内置类型及意义 前面我们已经学习了基本的内置类型:
C语言数据类型字节数(32位系统环境下) char1个字节 short2个字节 int4个字节 long4个字节(64位系统中是8个字节) long long 8个字节 float4个字节 double8个字节

类型的意义:
1.清楚类型开辟内存空间的大小(int a; 变量a开辟4个字节的空间)
2.由类型明确存的是什么(如int存的是整数,float存的是浮点数)
2.类型的基本分类 2.1.整型
char signed char unsigned char short signed short [int] unsigned short [int] int signed int unsigned int long signed long [int] unsigned long [int]

注意:
  • []的内容可省略不写。
  • signed intint 效果一样。所以不写signed是默认为有符号的。特殊的是,char类型并未规定是有符号还是无符号,但常见的编译器都会默认有符号
  • char 被划分到整型。其内存占一个字节,signed char的范围-128~127 ,unsigned char的范围0 ~ 255(ASCII表)。
2.2.浮点型
float double

2.3.构造类型
数组类型: int[5] char[10] 结构体类型: struct type { v1; v2; }; 枚举类型: enum type { v1, v2, ...}; 联合类型: union type { v1; v2; };

数组也是有类型的,去掉数组名就是类型。不信我们来验证:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

其他构造类型我们到自定义类型部分再来学习(关注我不迷路哦!)
2.4.指针类型
int *pi; char *pc; float* pf; void* pv;

2.5.空类型
viod test(); //函数返回类型 viod test(viod); //函数参数 viod* p; //指针

二、整型在内存中的存储 一个变量的创建是要在内存中开辟空间。空间的大小由变量类型决定。那数据在所开辟内存中是如何存储的?
比如:
int a = 10; int b = -20;

我们知道为a开辟了4个字节的空间,那如何存储?
这就得学习下面原码、反码、补码的概念:
1.原码、反码、补码
  • 注意:整数的存储涉及到原码、反码、补码的概念,这里只讨论整数的存储。
  1. 计算机中的整数有三种表示方法,即原码、反码和补码。
  2. 三种表示方法均有符号位和数值位两部分。
    • 符号位(最高位):0表示正,1表示负
    • 数值位(其他位):
下面是负整数的三种表示方法:
  • 原码:按数据的数值写出二进制序列
  • 反码:原码的符号位不变,其他位取反(0变为1,1变为0,攻受反转哈哈哈)
  • 补码:反码+1
对于负整数, 反码、补码表示方式是人脑无法直观看出其数值的,通常需要转换成原码在计算其数值。
对于正数和无符号整数来说
  • 原码、反码和补码相同。
干说没用,我们拿个例子来验证一下:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

int a = -1; 10000000 00000000 00000000 00000001 - 原码 11111111 11111111 11111111 11111110 - 反码 11111111 11111111 11111111 11111111 - 补码

二进制1111 = 十进制15 =十六进制f,即是 ff ff ff ff
到此,我们验证成功并发现整数在内存中存放的是补码
2.补码的意义 为什么在计算机系统中,整数数值一律用补码来表示和存储,原因有:
1.加法和减法也可以统一处理(CPU只有加法器,简化运算器的结构、提高运算速度)。
2.使用补码,可以将符号位和数值域位一处理。
3.补码与原码相互转换,其运算逻辑是相同的,不需要额外的硬件电路。
1.CPU只有加法器,计算1-1时可转化为`1+(-1)
若以原码存储并计算 00000000 00000000 00000000 00000001 -1的原码 10000000 00000000 00000000 00000001 - -1的原码 10000000 00000000 00000000 00000010 - -2的原码 小学生都知道1+(-1)=0,-2明显不符合结果若以补码存储并计算 00000000 00000000 00000000 00000001 -1的补码 11111111 11111111 11111111 11111111 - -1的补码 100000000000000000000000000000000000 -0的补码 超过32位发生截断 正确

2.上述以补码存储并计算可以看出 符号位也参与计算,当作数值位统一处理。
3.这点用一张图就能明白:原码《==》补码
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

3.大小端字节序 管你什么大小端,来了都得一锅端,上图:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

咱们可以看到十六进制数字 0x123456 在内存中恰好"反"过来,为什么会这样呢?这里就要说到大小端了
  • 大小端字节序:以字节为单位,两种不同的计算机存储顺序
大端字节序存储:
当一个数据的低位放到高地址处,数据的高位放到低地址处;
小端字节序存储:
当一个数据的低位放到低地址处,数据的高位放到高地址处。
这样的定义看着十分枯燥,老规矩,一张图搞定:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

当我们先定好地址的顺序(如上图,从左往右,地址由低到高),大端模式是按照数字的书写顺序进行存储的,而小端模式是颠倒书写顺序进行存储的。 这样是不是就清晰很多啦!
啊哈哈哈哈哈哈鸡汤来咯!
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

下面是一道出自百度的面试题:
百度2015年系统工程师笔试题(10分):
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
分析:
  • 我们先固定好 地址是由低到高,并定义一个变量a = 1
  • 若机器字节序为大端,变量a在内存中是00 00 00 01
  • 若机器字节序为小端,变量a在内存中是01 00 00 00
    • 不同点是低地址的字节,一个是00,一个是01
  • 如果获取到低地址字节的数据,问题就解决了。流氓做法:将变量a的地址强制类型转换成char* 类型,再解引用,达到访问一个字节的目的
#include int check_sys() { int a = 1; //0x00 00 00 01 return (*(char*)&a); //返回0,大端; 返回1,小端 } int main() { int ret = check_sys(); if(0 == ret) { printf("大端\n"); } else if(1 == ret) { printf("小端\n"); } return 0; }

哎呀,啧啧啧啧,不咸不淡,味道真是好极了!
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

4.练习题(营养鸡汤) 鸡汤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); //a=-1,b=-1,c=255 return 0; }

解毒:
char a = -1; 11111111 11111111 11111111 11111111 - -1的补码 11111111 - 截断存入a 11111111 11111111 11111111 11111111 - 以%d打印 有符号数的整型提升 -1的补码 11111111 11111111 11111111 11111110 - -1的反码 10000000 00000000 00000000 00000001 - -1的原码 signed char b=-1; - 同上 unsigned char c=-1; 11111111 11111111 11111111 11111111 -1的补码 11111111 - 截断存入a 00000000 00000000 00000000 11111111 以%d打印 无符号数的整型提升 255的补码 00000000 00000000 00000000 11111111 - 255的原码

存入时:
a,b,c存入的都是-1的补码,a,b,c都是char类型,只有一个字节的空间,则会发生截断,只存入了11111111
取出时:
1.a,bsigned char有符号数;cunsigned char无符号数
2. 在%d(输出有符号整数)打印之前,因为a,b,c都是char类型,只有1个字节,达不到整型的4个字节,被迫整型提升。
  • 整型提升:
    • 有符号数:
      • 最高位是1,补1(负数)
      • 最高位是0,补0(正数)
    • 无符号数:无论最高位是0或1,都是补0(无符号数是正数)
3. %d和%u有自己的原则:将得到的二进制序列按自己的格式输出
  • %d输出十进制有符号整数:将二进制序列当成有符号数来打印
  • %u输出十进制无符号整数:将二进制序列当成无符号数来打印
整型提升在操作符那章有讲到传送门
变式:
#include int main() { char a= -1; signed char b=-1; unsigned char c=-1; printf("a=%u,b=%u,c=%u",a,b,c); //a=2^32-1,b=2^32-1,c=255 return 0; }

解析:
char a= -1; 11111111 - 截断存入 11111111 11111111 11111111 11111111 - 整型提升并以%u打印 - 2^32-1 signed char b=-1; 11111111 - 截断存入 11111111 11111111 11111111 11111111 - 整型提升并以%u打印 - 2^32-1 unsigned char c=-1; 11111111 - 截断存入 00000000 00000000 00000000 11111111 - 整型提升并以%u打印 - 255

说白了就是,a,b,c 上刀山下火海,但%d%u却只在乎你的结果(整型提升后的二进制序列),并按自己的原则做事,这也太现实了
类型只能决定字节大小和有无符号数,而%d,%u决定了如何使用该数据。
鸡汤2:
#include int main() { char a = -128; printf("%u\n", a); return 0; }

解毒:
char a = -128; 10000000 00000000 00000000 10000000 - 原码 11111111 11111111 11111111 01111111 - 反码 11111111 11111111 11111111 10000000 - 补码 10000000 - 截断存入 11111111 11111111 11111111 10000000 - 整型提升并以%u打印 printf("%u\n", a); //4294967168

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

做法和鸡汤2一样,不做多解析,可以自己尝试写
鸡汤4:
#include int main() { int i = -20; unsigned j = 10; printf("%d\n", i + j); //输出-10 return 0; }

解毒:
int i = -20; 10000000 00000000 00000000 00010010 - -20的原码 11111111 11111111 11111111 11101101 - -20的反码 11111111 11111111 11111111 11101110 - -20的补码 unsigned j = 10; 00000000 00000000 00000000 00001010 -10的原反补 i + j用补码运算,,结果还是补码 11111111 11111111 11111111 11110110 -i + j的补码 11111111 11111111 11111111 11110101 -i + j的反码 10000000 00000000 00000000 00001010 -i + j的原码 //-10

鸡汤5:
int main() { unsigned int i; //i任何值都是 >= 0的 for (i = 9; i >= 0; i--)//循环不会终止 { printf("%u\n", i); } return 0; }

C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

解毒:
当 i = 0 时,i--后,i = -1 以%u打印-1,就是4294967295

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

解毒:
strlen()计算字符串从长度,直到空结束字符(即'\0'),但不包括空结束字符 而'\0'的ASCII值为0,即直到字符数组元素为0停止我们知道signed char 类型是1个字节,8个bit,范围是[-128,127] 从上述循环中的a[i] = -1 - i; 可知数组元素依次为-1, -2, -3,...,-128,..., -128之后是什么呢?是-129吗?并不是

为了更好的理解,继续上图:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

10000000 - -128的补码00000000 -0的补码 补码-1↓补码-1↓ 01111111 -127的补码11111111 - -1的补码 这便从负数回到正数了这便从正数回到负数了如此形成一个signed char 补码轮回图

按照上面做法,我们也可以推出signed short的补码轮回图。
鸡汤7:
#include unsigned char i = 0; int main() { for (i = 0; i <= 255; i++) { printf("hello world\n"); } return 0; }

解毒:
unsigned char 类型变量范围是[0,255],255+1的补码会变成0的补码,也是一个轮回。 因此循环条件恒成立,是个死循环

到此,相信大家都喝个饱了,那就继续下一个内容。
三、浮点型在内存中的存储 浮点数如:3.14159,1E10等,在内存中也是以原反补的二进制序列的形式存储的吗?我们先看一个例子:
#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; }

学习了上面的内容,我们可以做出分析:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

但运行代码后,我了个乖乖!这是发什么甚么事啦
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

经过对比发现:
正确的是:(以整型视角放,以整型视角取)和(以浮点型视角放,以浮点型视角取)
错误的是:(以整型视角放,以浮点型视角取)和(以浮点型视角放,以整理的视角取)
如果放和取的视角不同便会出错,这说明了整型和浮点型的存储机制是不兼容的,是有区别的。
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法
经查阅资料知道:
1.浮点数表示规定 根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制数V可以表示成以下形式
(-1)^S * M * 2^E
  1. 符号位(-1)^S:S=0时,V为正数,S=1时,V为负数。
S=0,(-1)^0^ =1,表示正数
S=1,(-1)^1^ =-1,表示负数
这点类似整数的表示
  1. 有效数位M:范围:[1,2)
类似科学计数法的有效部分
计算机是二进制机器,所以逢二进一
小数点前是2的正次方,小数点后是2的负次方
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

  1. 2^E表示指数位
类似科学计数法的指数部分
十进制进位是×10x
二进制进位是× 2E
概念比较复杂,我们举个例子就明白了:
比如十进制浮点数5.5,转化为二进制便是101.1
将有效部分和符号位带上,就是(-1)0 × 101.1 × 22 ,即S=0,M=1.011,E=2
2.浮点数存储规定 我们为什么要定义一套表示浮点数的逻辑呢?
通过这套逻辑,我们只需往内存放进S,M,E三个量即可,在有限的位数中尽量保存有用的值,利于扩大精度,也方便存储。
那这三个部分,分别占多少个比特位呢
IEEE 754规定:
对于32位浮点数float,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

对于64位的浮点数double,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

最大化利用了内存并提高了精度
为了最大化的利用存储空间,S,M,E在存入时十分灵活:
  • S:只有(-1)0 和(-1)1 两种情况,那存0或1就行
按照相同的逻辑取出就可,其他的位留给E和M用,更能提高精度。
  • M的范围:[1,2),所以M必然等于1.xxxx,可只存小数部分xxxx
舍弃前面的1,只存小数点后面的xxxx,最大化的利用内存。
因为规定在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面xxxx部分,比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省一位有效数字。(以32位浮点数为例,留给M的只有23位,将第1位的1舍去后,等于可以保存24位有效数字)
  • E:E是一个无符号整数
如果E为 8位,它的取值范围:[0,255]
如果E为11位,它的取值范围:[0,2047]
我们知道,科学计数法中的E是可以出现负数的,如: 十进制的0.5要转换成二进制的0.1,再写成科学计数法就是1.0 × 2-1,则这里S = 0;M = 1.0;E = -1
所以规定,存入内存时E的真实值必须再加上一个中间数
对于 8位的E,这个中间数是127
对于11位的E,这个中间数是1023。
(比如:2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001
3.浮点数取出规定 取出和存放是反过来的逻辑,即:
S照常取出,E取出后再减去中间值(127或1023),M取出后再前面"加"个1.
指数E从内存中取出可以分成三种情况:
  • 一般情况
    1.E不全为0或不全为1: E照常减去127或1023就行。
  • 特殊情况(不要求深入了解)
  1. E为全0:
当E为全0(-127)时,说明E的真实值为-127(-127 + 127 =0)
一个数乘以2-127 说明是一个非常小的数,极限等于0。此时,直接取出M,在前面加上0.,得到0.x...xx
  1. E为全1:
当E为全1(255)时,说明E的真实值为128(128 + 127 =255)
一个数乘以2128,说明是一个非常大的数,极限等于∞ 。表示± ∞的情况。
说了这么多,相信你都已经蒙圈了,别问为什么,因为我就是这么过来的
老规矩,举例子,上图:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

看到这里,我们都阔然开朗了!
这时,我们再回头解决最开始的引例:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

分析:
以整型视角放,以浮点型视角取:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

以浮点型视角放,以整理的视角取:
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

内容比较多,一定要好好消化~
【C语言进阶|人人都看得懂的C语言进阶系列之数据存储】最后,各位老铁看了记得点赞关注评论哦,你的支持是我坚持的动力~
C语言进阶|人人都看得懂的C语言进阶系列之数据存储
文章图片

    推荐阅读