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

一、数据类型

1.内置类型及意义

前面我们已经学习了基本的内置类型:

C语言数据类型字节数(32位系统环境下)
char      1个字节
short     2个字节   
int       4个字节  
long      4个字节(64位系统中是8个字节)
long long 8个字节
float     4个字节
double    8个字节

类型的意义:
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;
    };

数组也是有类型的,去掉数组名就是类型。不信我们来验证:

在这里插入图片描述
其他构造类型我们到自定义类型部分再来学习(关注我不迷路哦!)

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

对于负整数, 反码、补码表示方式是人脑无法直观看出其数值的,通常需要转换成原码在计算其数值。

对于正数和无符号整数来说

  • 原码、反码和补码相同。

干说没用,我们拿个例子来验证一下:
在这里插入图片描述

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.这点用一张图就能明白:原码《==》补码
在这里插入图片描述

3.大小端字节序

管你什么大小端,来了都得一锅端,上图:
在这里插入图片描述

咱们可以看到十六进制数字 0x123456 在内存中恰好"反"过来,为什么会这样呢?这里就要说到大小端了

  • 大小端字节序:以字节为单位,两种不同的计算机存储顺序

大端字节序存储:
当一个数据的低位放到高地址处,数据的高位放到低地址处;
小端字节序存储:
当一个数据的低位放到低地址处,数据的高位放到高地址处。

这样的定义看着十分枯燥,老规矩,一张图搞定:
在这里插入图片描述
当我们先定好地址的顺序(如上图,从左往右,地址由低到高),大端模式是按照数字的书写顺序进行存储的,而小端模式是颠倒书写顺序进行存储的。 这样是不是就清晰很多啦!

啊哈哈哈哈哈哈鸡汤来咯!
在这里插入图片描述

下面是一道出自百度的面试题:

百度2015年系统工程师笔试题(10分):
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。

分析:

  • 我们先固定好 地址是由低到高,并定义一个变量a = 1
  • 若机器字节序为大端,变量a在内存中是00 00 00 01
  • 若机器字节序为小端,变量a在内存中是01 00 00 00
    • 不同点是低地址的字节,一个是00,一个是01
  • 如果获取到低地址字节的数据,问题就解决了。流氓做法:将变量a的地址强制类型转换成char* 类型,再解引用,达到访问一个字节的目的
#include<stdio.h>
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;
}

哎呀,啧啧啧啧,不咸不淡,味道真是好极了!
在这里插入图片描述

4.练习题(营养鸡汤)

鸡汤1:

#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);//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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
int main()
{
	char a = 128;
	printf("%u\n", a);//4294967168
	return 0;
}

做法和鸡汤2一样,不做多解析,可以自己尝试写

鸡汤4:

#include<stdio.h>
 
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;
}

在这里插入图片描述
解毒:

当 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吗?并不是

为了更好的理解,继续上图:
在这里插入图片描述

10000000 - -128的补码         00000000 -  0的补码
补码-1  ↓                    补码-101111111 -  127的补码         11111111 - -1的补码
这便从负数回到正数了            这便从正数回到负数了

如此形成一个signed char 补码轮回图

按照上面做法,我们也可以推出signed short的补码轮回图。

鸡汤7:

#include <stdio.h>
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<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("num的值为:%d\n", n);
	printf("pFloat的值为:%f\n", *pFloat);
	return 0;
}

学习了上面的内容,我们可以做出分析:
在这里插入图片描述
但运行代码后,我了个乖乖!这是发什么甚么事啦
在这里插入图片描述
经过对比发现:
正确的是:(以整型视角放,以整型视角取)和(以浮点型视角放,以浮点型视角取)
错误的是:(以整型视角放,以浮点型视角取)和(以浮点型视角放,以整理的视角取)

如果放和取的视角不同便会出错,这说明了整型和浮点型的存储机制是不兼容的,是有区别的。

要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法
经查阅资料知道:

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的负次方

  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
在这里插入图片描述

对于64位的浮点数double,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
在这里插入图片描述

最大化利用了内存并提高了精度

为了最大化的利用存储空间,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,说明是一个非常大的数,极限等于∞ 。表示± ∞的情况。

说了这么多,相信你都已经蒙圈了,别问为什么,因为我就是这么过来的😂
老规矩,举例子,上图:
在这里插入图片描述

看到这里,我们都阔然开朗了!
这时,我们再回头解决最开始的引例:
在这里插入图片描述
分析:

以整型视角放,以浮点型视角取:
在这里插入图片描述

以浮点型视角放,以整理的视角取:
在这里插入图片描述

内容比较多,一定要好好消化~

最后,各位老铁看了记得点赞关注评论哦,你的支持是我坚持的动力~
在这里插入图片描述

评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大猩猩!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值