数据的存储完整版——C语言进阶

简介: 正片开始👀数据类型 👏

正片开始👀

数据类型 👏

1.基本内置类型:byte,int ,char, float, double


2.构造数据类型:

数组类型;

结构体类型:struct

共用体(联合类型):union

枚举类型:enum


3.指针类型 :int* p,char* p,float* p,void* p


4.空类型 : void(无类型),通常用于函数的返回类型,函数参数与指针类型。


构造类型又叫自定义类型,在各自参数或者元素类型发生变化就会让他彻头彻尾的改变;而基本数据类型的特点就是不可以再分解为其他类型,基本类型就是自我说明,关于他们的作用就不一一赘述了。


内存窗口👏

那首先要在调试栏打开内存窗口,并搞清楚怎么观察内存,这是必要的工具

1.地址栏

image.png

2.内容

image.png

这些密密麻麻的就是内存中的数据,看到这里你可能就会疑惑,不是说内存里存的都是二进制数吗,这些是什么鬼?是的,没有错,但是内存窗口展示内容有限,在有限的范围内,他只能选择以 16 进制的形式展示出来,仅仅是展示而已。

3.文本

image.png

这个更是人不人鬼不鬼的其实是他根据内存的数据简单的以文本的格式输出其可能的内容,无价值简直就是意义不明。


整型的存储👏

不论我们在写代码时创建了个什么东西,他不会居于虚空,存在载体就会占用内存,而空间的大小是根据我们创建的数据的类型而决定的,我们要回到问题最本质的源头,在开辟的内存中到底如何去存储数据?我们不废话直接创建俩个变量看看便知

int main()
{
int a = 5;
int b = -5;
return 0;
}

内存窗口打开我们可以取地址查找 a,b 的数据存储情况:

image.png

这里是不是感觉很奇怪,二者为何差异这么大?要搞清楚我们就要继续深入研究。


原码,反码,补码👏

说整数的二进制有三种表示方法:原码,反码,补码。


整数分为正数和负数,正负数的区别就在于他们二进制32位数的最高位的 0和1代表着符号位,0为正,1为负,其余才是有效位。


正数的原反补三码合一,和他本身是一样的。但是负数就花哨了,负数原码是按照一个数的正,负直接写出来的二进制就是原码。反码在原码基础上,除开符号位进行取反得到。这里强调一下,之前讲过一个操作符:~(按位取反操作符),区别一下他俩,按位取反操作符是针对二进制数每一位全部都取反,包括符号位。补码则是反码的基础上+1得到,比如 -7 这个数的原反补分别为:

10000000 00000000 00000000 00000111 (原)

111111111 111111111 111111111 111111000(反)

111111111 111111111 111111111 111111001(补)


b 的 -5 就是 00000000 00000000 00000000 00000101以补码 11111111 11111111 11111111 11111011 每四个字节为一位化成16 进制就是 0xfffffff3。


补码的意义👏

既然内存中中存储的是二进制的补码,我们现在不谈现象谈本质,为什么偏偏要是补码呢?

我们要明白一件事就是计算机算减法是相对不容易的,因为CPU里面没有减法器,只有加法器,要算 1-1 时只能算作 1+(-1)。计算机用二进制去计算时,我们会发现,当用原码或者反码去计算根本行不通,只有补码才可以实现。


由此看来,补码的地位是绝对的老大哥,在计算机系统中,数值一律用补码来存储,主要原因是:

1.统一了零的编码

2.将符号位和其它位统一处理

3.将减法运算转变为加法运算

4.两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃


由这里看,加法和减法可以统一起来处理,此外补码和原码相互转换时,其运算过程是相同的,不需要额外的硬件电路。


大小端模式

我昨天的博客专门讲了大小端存储模式专题,其实大小端的检验也可以用今天的知识来解决:

# include<stdio.h>
int check_s()
{
  int i = 1;
  return (*(char*)&i);
}
int main()
{
  int ret = 0;
  ret = check_s();
  if (ret == 1)
  {
    printf("小端\n");
  }
  else
  {
    printf("大端\n");
  }
  return 0;

其结果:

image.png

不同数据类型存储👏

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

上面这个代码乍一看就是三个-1,仔细看就会发现char类型后面阴差阳错跟个 -1,-1是整数啊,这时是不是感觉有一丝凌乱?-1的原码为1000(30个0)1,取反+1得到补码111……111(32个1),而 char 只能放 8 个比特位,就意味着会发生截断,,a = 11111111,而我们在vs环境里面 signed char和char是一样的,我们不妨看看运行结果如何:

image.png

为什么c会是 255呢?首先要明白为什么b 会是 -1,我们 signed char b在截断后,如果要 以%d形式进行打印,就会发生整型提升,有符号数的最高位会被认为是符号位,而整型提升时会以整型的原符号位进行提升,因为是负数就会全部补成1,再按照补码转原码倒回去会发现结果就是 char a= -1。


同理,c 是无符号数,但也会发生整型提升,无符号数提升通通补 0 ,按照%d 打印时,内存就会认为这是一个有符号数,最高位又会被默认为符号位,最后转换成原码,就会得到 255。


接着上面的思路再来看看这个代码:

# include<stdio.h>
# include<windows.h>
int main()
{
unsigned int a ;
for(a=9;i>=0;i--)
{
printf("%u ",i);
Sleep(1000);
return 0;
}
}

这个代码也是一个眼见不为实的代码,表面上是打印9个数,实则实在无限的死循环,你发现了吗?聪明的你如果看到 判断条件 i>=0 这个条件,就可能发现端倪,十有八九会死循环,因为不管什么情况都会恒成立。我们来看看运行效果:

image.png

当我0进入后很自然的变成了-1,而-1会放到无符号整型,补码是32位全1,而作为无符号整型,最高位不再是符号位,所有位都是有效位,转化为原码是一个巨大的数字,由于满足循环判断条件于是开始欢乐死循环。我们可以根据这些实例总结出相应的经验。


浮点数存储机制👏

浮点数相对于整型,他的存储机制会更复杂。

浮点数包括float和double,甚至long double,浮点数的表示范围在 float.h中定义。

int main()
{
int  n = 9;
float* pFloat = (float*)&n;
printf("n=%d",n);
printf("*pFloat=%f\n",*pFloat);
*pFloat = 9.0;
printf("n=%d\n",n);
printf("*pFloat=%f\n",*pFloat);
return 0;
}

对上面这个代码,我一开始的觉得打印的是:9,9, 9.0, 9.0,后来发现是我格局小了。

image.png

如上结果我们就能明显看出浮点数和整型存储是不一样的。


国际标准IEEE(电气和电子工程协会)754标准,任意一个二进制浮点数V都可以表示成下面的形式:


V=(-1)^S * M * 2^E

1.(-1)^s表示符号位,s为0表示正,s为1表示负

2.M表示有效数字,大于等于1,小于2

3.2^E表示指数位


对于32位浮点数,最高符号位s,接着 8位是指数E,剩下23位为有效数字M;而64位浮点数,11位是指数 E,剩下的 52 位有效数字M。

这里的M和E还有特殊的规定


IEEE 754规定,在计算机内部保存M时,默认这个数的第1位总是1,因此可以被舍去,只保存后面的小数部分。例如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。


我们会把指数位看作是无符号整数,取值在0到255,如果是11位的E,取值在0到2047,但是众所周知,科学记数法里面E是可以出现负数的,所以还有规定就是E在存入时会先修饰一波,给真实值加上一个中间值,对于 8位与11位的E,这个中间数是127和1023,比如 2 的十次方,E就是10,保存成32位浮点数时,必须变成10+127=137,即10001001。E在全0和全1的情况下代表着接近于0的无穷小和无穷大。


我们回到最开始的问题,9的原反补三码合一00000000 00000000 00000000 00001001,E为全0时其真实值是-126,除去S与E,小数部分M为0000……01001,而%f只能打印后6位,这就是为什么打印0.000000。接着pFloat为9.0浮点数进入,对应的二进制为1001.0,科学记数法1.0012^3,S=0,E=3,M=1.001,存入后变成 0 10000010 0010000000000000000000,(S E M),为了方便我们换成16进制计算出是 0x41100000,我以%f取出就是他自己这没什么解释的,困惑的可能是%d的结果,以%d打印就默认为有符号数,变成补码转换出来就是 1091567616。

相关文章
|
2月前
|
存储 编译器 C语言
C语言存储类详解
在 C 语言中,存储类定义了变量的生命周期、作用域和可见性。主要包括:`auto`(默认存储类,块级作用域),`register`(建议存储在寄存器中,作用域同 `auto`,不可取地址),`static`(生命周期贯穿整个程序,局部静态变量在函数间保持值,全局静态变量限于本文件),`extern`(声明变量在其他文件中定义,允许跨文件访问)。此外,`typedef` 用于定义新数据类型名称,提升代码可读性。 示例代码展示了不同存储类变量的使用方式,通过两次调用 `function()` 函数,观察静态变量 `b` 的变化。合理选择存储类可以优化程序性能和内存使用。
156 82
|
1月前
|
存储 C语言 C++
深入C语言,发现多样的数据之枚举和联合体
深入C语言,发现多样的数据之枚举和联合体
深入C语言,发现多样的数据之枚举和联合体
|
1月前
|
存储 C语言
深入C语言内存:数据在内存中的存储
深入C语言内存:数据在内存中的存储
|
1月前
|
C语言
回溯入门题,数据所有排列方式(c语言)
回溯入门题,数据所有排列方式(c语言)
|
2月前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
384 8
|
1月前
|
存储 C语言
C语言中的浮点数存储:深入探讨
C语言中的浮点数存储:深入探讨
|
2月前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
2月前
|
存储 算法 C语言
C语言手撕数据结构代码_顺序表_静态存储_动态存储
本文介绍了基于静态和动态存储的顺序表操作实现,涵盖创建、删除、插入、合并、求交集与差集、逆置及循环移动等常见操作。通过详细的C语言代码示例,展示了如何高效地处理顺序表数据结构的各种问题。
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
22 6