【C进阶】第十篇——数据在内存中的存储

news2024/11/29 10:52:55

数据类型的介绍

类型的基本归类

整型在内存中的存储

原码,反码,补码

大小端介绍

什么是大小端

为什么有大端和小端?

判断当前机器的字节序

浮点型在内存中的存储

例题引入

浮点数的存

浮点数的取

浮点数的比较


数据类型的介绍

char          //字符数据类型

short         //短整型

int             //整型

long          //长整型

long long  //更长的整型

float          //单精度浮点数

double      //双精度浮点数

以及他们所占存储空间的大小.类型的意义:

1.使用这个类型开辟内存空间的大小(大小决定了使用范围)

假设我们在内存中定义了两个变量,A变量是int类型,B变量是char类型,我们都知道int类型是4字节所以他在内存中占4个字节的空间,float类型是4字节,所以他在内存中占用4字节的空间.

2.如何看待内存空间的视角.

类型就决定了定义变量时他们的大小变量A是四字节,变量B也是四字节,站在int型的角度他的存取都是整数,所以他是根据整数的形式存取的,而站在float类型的角度,他存放的是实数,所以他的存取方式是按实数的方式存取的,不同的数据类型就决定了他们在内存中存取方式是如何的

类型的基本归类

整型类型

char
   unsigned char
   signed char
short
   unsigned short[int]
   signed  short[int]
int 
   unsigned int
   signed int
long
   unsigned long[int]
   signed long[int]

注意: 这个地方为什么char类型也归为整型家族里面呢?因为char类型的数据本质上也是在内存中存储的其ascii码值,而其ascii码就是整数,所以char类型自然也就列为整型家族里面了。

在这里首先我们需要了解一下char类型.

int main()
{
	signed short int a = 0;//有符号短整型
	unsigned short b = 0;//无符号短整型
	char a;//有符号?还是无符号
	return 0;
}

char a是有符号的还是无符号的问题,取决的是编译器

char类型有符号和无符号类型的区别

int main()
{
	unsigned char c1 = 255;
	signed char c2 = 255;
	printf("%d\n",c1);//255
	printf("%d\n",c2); //-1
	return 0;
}

运行结果: 

char类型是占一个字节的8比特位,如果它是无符号类型囊,那么它的符号也是有效位,而取值范围就是可以到达255,再对比有符号的char类型(signed char),首先255对应的二进制序列是11111111.

 最终其实是得到得二进制序列是对应十进制得-1,而它的最高位代表的是符号位1即表示负数.

因为这里写的是signed char所以它的最高位为0的化表示的是一个正数,那么它的原码,反码,补码都是一个样的而第一个二进制序列表示的是0,再看他的最后一个二进制序列,因为signed chae的最高位是1表示的是负数,那么久需要对他的补码-1取反得到原码,那么就会是-1,而-127又是通过下面的计算过程得到的原码对应的二进制序列因为是有符号的所以他的最高位表示的是符号位,而剩下的7位才是他的有效位,而中间的10000000表示的是-128

那如果是无符号的char那么它的二进制序列对应的每一位就都是有效位,所以它的最大范围是0 ~ 255,既然我们知道了char类型的取值范围怎么运算的规则,是不是就可以知道其他数据类型的取值范围了?这里就不再继续了

总结:

1、signed char的取值范围是 0 ~ 127 | -128 ~ -1
2、unsigned char的取值范围是0 ~ 255

浮点类型

float
double

关于浮点型在后面的存储方式再详细介绍,这里简单了解一下:

1.float类型称为单精度,占用4个字节,数值范围位3.4E-38到3.4E+38,有效数字6-8位

2.double类型称为双精度,占用8个字节,数值范围位1.7E-308到1.7E+308,有效数字15-16位 

构造类型

>数组类型
>结构体类型 struct
>枚举类型 enum
>联合类型 union

 指针类型

int* pi;
char* pc;
float* pf;
void* pv;

空类型

void 表示空类型(无类型)

通常应用于函数的返回类型、函数的参数、指针类型

void参数列表声明

void func(void)//表示不需要传递参数
{
	printf("hehe\n");
}

int main()
{
	func(100);
	return 0;
}

 虽然程序不报错,但是显示了一条警告,这也是不太好的,如果指定不需要参数,那么就不传参数,否则就不需要指定void

整型在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的

那接下来我们谈谈数据在所开辟内存中到底是如何存储的

我们为a分配四个字节的空间.那如何存储?

原码,反码,补码

计算机中的整数有三种表示方法,即原码,反码,补码.

三种表示方法均有符号位和数值位两个部分,符号位都是用0表示"正",用1表示"负",而数值1位负整数的三种表示方法各不相同.

原码

直接将二进制按照正负数的形式翻译成二进制就可以

反码

将原码的符号位不变,其他位依次按位取反即可

补码

反码+1就得到补码

注意:

(1)正数的原,反,补都是相同的

举个例子:

再看一个负数

(2)对于整型来说:数据存放内存中其实存放是补码,那么为什么?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统 一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

总结而言,有两点原因:一是为了进行负数的有关运算;二是为了提高运算的效率。

下面来解释一下:

 1.可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理

因为CPU只有加法器,所以对于1-1这样的表达式CPU要处理成1+(-1)来进行计算

而如果直接将两个操作数的原码进行相加,就可能会出错的.

举个例子:

1+(-1),我们用原码相加,得到错误的结果

用补码计算:

 2.补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路

我们通过原码得到补码的方法:

原码的符号位不变,其他位按位取反得到反码,反码加1,得到补码;

然后我们创建两个变量,看一下,内存给我们战术出来的样子:

 我们可以看到对于a和b分别存储的是补码。但是我们发现顺序有点不对劲,好像是相反的。
这是又为什么?

大小端介绍

什么是大小端

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中.

小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中

解释说明:

 到这里就能明白了,编译器使用的的是小端存储模式.

为什么有大端和小端?

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元
都对应着一个字节,一个字节为8 bit。但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。 

判断当前机器的字节序

概念我们上面已经说过了,那怎么设计程序呢?我们来思考一下:

我们可以用整数1来帮助判断,取出1的第一个字节的内容,1的补码是:00000000000000000000000000000001,16进制是:00 00 00 01;

如果第一个字节的值是0(高位在低地址),则为大端;
如果第一个字节的值是1(低位在低地址),则为小端。
(注:我们取出的第一个字节是处在低地址的那一个字节)

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

 

浮点型在内存中的存储

例题引入

思考下面的程序输出结果,并且想一下为什么会出现这种结果.

#include<iostream>
#define _CRT_SECURE_NO_WARNINGS
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;
}

运行结果如下:

根据国际标准IEEE(电气和电子工程协会) 754:

任意一个二进制浮点数V可以表示成下面的形式:

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

(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

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

2^E表示指数位。

举例来说:

十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。

那么,按照上面的格式,可以得出s=0,M=1.01,E=2。

十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,s=1,M=1.01,E=2。

IEEE 754规定:

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

 对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

M占用的比特位越多,数据精度越高。
E占用的比特位越多,数据范围越大。
所以在实际开发中使用double较多。

此外: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) 这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们 知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

总结:
1、如果是float类型,这个中间数是127。比如,2^10的E 是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
2、如果是double类型,这个中间数是1023。比如,2^10的E 是10,所以保存成32位浮点数时,必须保存成10+1023=1033,即010000001001。
 

浮点数的存

下一个话题浮点数以二进制的形式在内存中存储

int main()
{
	float a = 5.5f;
	//(-1)^0 * 1.011 * 2^2
	//S = 0
	//M = 1.011
	//E = 2
	//因为是浮点型中间数是127,而E又是2
	//E = 2 + 127 = 129  
	//对应的二进制序列:01000000 10110000 00000000 00000000
	//对应的十六进制序列:40       B0		00      00
	return 0;
}

对应的二进制序列内存块

 把这32位二进制序列转换成16进制就是40 B0 00 00

 由于使用的是VS2017编译器采用的是小端存储,所以低地址处存放的是二进制序列低位的数据,而高地址处的是二进制序列高位的数据

浮点数的取

 然后,指数E从内存中取出还可以再分成三种情况:

(1)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,补齐0到23位00000000000000000000000,则其二进制表示形式为:

0 01111110 00000000000000000000000

(2)E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

注意:这个地方为什么不是0-127呢?这是规定!规定就是把E全为0的时候看作是1-127即-126。

(3)E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)。

到了这里,前面的题目就显而易见了,因为上面是以浮点数形式进行打印,就是将二进制的数字看成是浮点数,即按照浮点数在内存中存储的形式进行打印。

将9.0存入*pFloat中时,先将9.0按照浮点数的形式存储起来,就是按照上面的格式,按照整数的形式进行打印的时候自然就会出现一个比较大的数字。

回到之前的问题

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

当我们把以上的知识弄明白之后,对于这个问题只要把它的二进制序列求出来,再截取它的6有效位数(因为浮点型的有效位数是6位有效数字) 所以会截取0.000000

 解析这两句代码的意义

*pFloat = 9.0;

 printf(“num的值为:%d\n”,n);
所以它的二进制序列是== 01000001000100000000000000000000==当以%d的形式打印,而他的最高为0所以是正数,而正数的原、反、补相同所以打印的结果是

浮点数的比较

使用这种方式来存储的时候,会带来一个很大的问题,保存的小数往往不是一个精确值,而只是一个近似值。

示例:

#include <stdio.h>
int main()
{
	float a = 11.0;
	float b = a / 3.0;
	if (b * 3.0 == a) {
		printf("相等!\n");
	} else {
		printf("不相等\n");
	}
	system("pause");
	return 0;
}

实际上11.0/3.0*3.0肯定等于11.0。
但是我们看看运行结果: 

 所以,浮点数在内存中存储的时候,很多时候是有误差的

正确的比较方法:

使用做差的方法,然后判断差值是不是在允许误差范围内,如果在的话,就相等。

#include <stdio.h>
#define N 1e-4
int main()
{
	float a = 11.0;
	float b = a / 3.0;
	if (b * 3.0 - a < N && b * 3.0 - a > -N) {
		printf("相等, 此处不是严格相等, 而是允许误差\n");
	} else {
		printf("不相等\n");
	}
	system("pause");
	return 0;
}

运行结果: 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/85190.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

现代制造技术产品设计与柔性制造系统的预测和分析

产品设计要求的市场竞争是面向市场&#xff0c;以用户为中心。精益设计的工业设计方法&#xff0c;以及一系列新的设计概念&#xff0c;如制造、装配过程、检查和测量、环境中的绿色设计等&#xff0c;使设计与柔性制造系统的整个过程紧密结合&#xff0c;包括从产品概念设计到…

Flask全栈开发教程

Flask全栈开发教程 成为使用 Flask、Python、HTML、CSS 和 MongoDB 的全栈 Web 开发人员&#xff01; 课程英文名&#xff1a;Web Developer Bootcamp with Flask and Python 此视频教程共5.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无水印&#xff0c;源码附件全…

[前端]白屏性能优化

[前端]白屏性能优化 业务面会问的东西 从打开一个页面&#xff0c;到页面的画面展示经历了怎样的过程&#xff1f; 简单来说&#xff0c;有以下几个主要步骤。 1、URL解析&#xff1a;判断浏览器输入的是搜索内容还是URL&#xff1b; 2、查找缓存&#xff1a;如果能找到缓存…

笔试强训(四十四)

目录一、选择题二、编程题2.1 驼峰命名法2.1.1 题目2.1.2 题解2.2 单词倒排2.2.2 题解一、选择题 &#xff08;1&#xff09;IPv4版本的因特网总共有多少有效A类地址网络&#xff08;D&#xff09; A.255 B.128 C.256 D.126 A类地址的网络号从0~127共128个&#xff0c;其中有两…

大数据 常用命令

常用shell命令 管道命令 查看/etc目录信息前5行信息 执行命令&#xff1a;ll /etc | head -5 查看/etc/profile文件后5行信息 执行命令&#xff1a;cat /etc/profile | tail -5 grep命令 抓取/etc目录下的python信息 执行命令&#xff1a;ll /etc | grep python 抓…

解决visual studio对不安全函数的警告

解决visual studio 对scanf &#xff0c;strcpy&#xff0c;strcmp等函数的不安全警告报错 可以看到&#xff0c;编译器对scanf进行了报错&#xff0c;原因是说它不安全 编译器自己给了一种解决方案&#xff1a; 使用vs自带的 scanf_s&#xff0c;但是用这个函数&#xff0c;仅…

留学Assignment写作格式简单讲解

对于Assignment写作&#xff0c;不知道大家认为它最基础的东西是什么呢&#xff1f;可能大家的答案都会是格式&#xff0c;毕竟Assignment写作&#xff0c;最需要保证的就是格式无误&#xff0c;特别是文献综述的格式&#xff0c;错了的话后果是非常严重的&#xff0c;下面就给…

Springboot 那年我双手插兜,手写一个excel导出

前言 其实就是利用了csv 和txt 文件转换 。 不多说&#xff0c;开始玩代码。 正文 本篇内容&#xff1a; ① 了解根本生成excel内容的CSV文件玩法 ② 手动拼接文本演示 ③ 项目内实战写法&#xff0c;从数据库到导出 ④ 解决list数据过多&#xff0c;使用分批分页处理生成c…

极智AI | centos7源码编译tensorflow

欢迎关注我的公众号 [极智视界]&#xff0c;获取我的更多笔记分享 大家好&#xff0c;我是极智视界&#xff0c;本文介绍一下 centos7 源码编译 tensorflow 的方法。 之前这篇《极智开发 | centos7源码编译bazel》已经为这篇 tensorflow 的源码编译铺平了道路&#xff0c;所以…

[附源码]Nodejs计算机毕业设计基于web的小说浏览系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

【Docker】Docker安装MySQL,并解决中文乱码和配置数据备份同步到宿主机

专栏精选文章 《Docker是什么&#xff1f;Docker从介绍到Linux安装图文详细教程》《30条Docker常用命令图文举例总结》《Docker如何构建自己的镜像&#xff1f;从镜像构建到推送远程镜像仓库图文教程》《Docker多个容器和宿主机之间如何进行数据同步和数据共享&#xff1f;容器…

银河麒麟操作系统V10SP1创建网页快捷方式至桌面的方法

修改浏览器配置文件添加快捷方式 1.在桌面点击鼠标右键&#xff0c;选择‘’打开终端‘’&#xff0c;终端界面显示‘桌面’ 2.在终端界面输入命令行 sudo vim qaxbrowser-safe.desktop (奇安信浏览 器的快捷方式) 进去后按‘/’然后输入‘Exec’&#xff0c;最后按回车键。…

Android监听UEvent之UEventObserver分析

&#xff08;1&#xff09;背景概述 众所周知&#xff0c;在安卓系统中有状态栏&#xff0c;在插入外设的时候&#xff0c;会在顶部状态栏显示小图标。 比如&#xff0c;camera设备&#xff0c;耳机设备&#xff0c;U盘&#xff0c;以及电池等等。这些都需要在状态栏动态显示。…

wy的leetcode刷题记录_Day58

wy的leetcode刷题记录_Day58 声明 本文章的所有题目信息都来源于leetcode 如有侵权请联系我删掉! 时间&#xff1a;2022-12-2 前言 力扣每日一题简单模拟左右抵消和二叉平衡搜索树 1769. 移动所有球到每个盒子所需的最小操作数和108. 将有序数组转换为二叉搜索树 目录wy的l…

无线路由器首次配置、修改WiFi名称和密码—— Cisco实验/家里实验

一、Csico实验 192.168.0.1、192.168.1.0和192.168.1.1是路由器常用的默认IP 1. 在PC打开浏览器&#xff08;PC用网线直连无线路由器&#xff09;&#xff0c; 输入无线路由器在局域网内的静态IP&#xff1a;192.168.0.1 2. 输入管理者的账号和密码&#xff0c;默认都是admin…

搜索与图论- Dijkstra 算法

文章目录一、Dijkstra 算法1. 简介2. 基本思想3. 朴素 Dijkstra 算法&#xff08;重点&#xff09;3.1 朴素 Dijkstra 算法实现步骤3.2 朴素 Dijkstra 算法伪代码4. 朴素 Dijkstra 算法具体实现详见例题 Dijkstra 求最短路 I 。5. 堆优化朴素 Dijkstra 算法6. 堆优化 Dijkstra …

cookie、sessionStorage和localStorage的区别(二)

cookie、sessionStorage和localStorage的区别&#xff08;一&#xff09;详细精炼知识调用前言引入核心干货webstorage本地存储cookiesessionStoragelocalStorage知识调用 文章中可能用到的知识点前端学习&#xff1a;浏览器缓存方式有哪些&#xff08; cookie localstorage s…

rocketmq源码学习-broker启动

前言 这篇笔记记录broker启动的源码学习 broker主要完成一下几件事情&#xff1a; 1.接收producer的发送请求&#xff0c;并对消息进行持久化、同步其他节点 2.接收consumer读取消息星球 3.定时向nameSrv注册心跳信息&#xff0c;保持连接 在启动的时候&#xff0c;也是分了…

Ant Design 6.0.0 实践集合

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 使用的6.0.0 beta版本 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结前言 Ant Design 简称为 Antd antd 为 Web 应用提供了丰富的基础 U…

操作指南|通过JumpServer实现Kubernetes运维安全审计

本文重点介绍如何通过JumpServer实现Kubernetes的运维安全审计。此前&#xff0c;我们专门介绍过在Kubernetes集群上快速部署JumpServer的方法步骤&#xff0c;可参见《操作指南&#xff5c;在Kubernetes集群上快速部署JumpServer开源堡垒机》一文。 一、Kubernetes运维审计现…