当前位置:首页 » 《随便一记》 » 正文

(干)深度剖析数据在内存中的存储_m0_59793804的博客

18 人参与  2022年04月16日 10:35  分类 : 《随便一记》  评论

点击全文阅读


目录

    前言

1. 数据类型详细介绍

2. 整形在内存中的存储:原码、反码、补码

3. 大小端字节序介绍及判断

4. 浮点型在内存中的存储解析


    前言

        大家好,今天这篇文章来深入剖析一下数据在内存中的存储,包括整型和浮点型,中间会穿插一些相关内容的例题来帮助理解、巩固。


 

1. 数据类型详细介绍

首先,C语言中的基本数据类型包括:(C99标准也加入了bool类型)

注意C语言中并没有字符串类型。
那么数据类型有什么意义呢?首先,数据类型决定了使用这个类型时我们需要开辟的内存空间大小,如上面的大小(字节)分别为1、2、4、4、8、4、8。其次,数据类型也决定了我们访问该块内存空间时是以什么视角访问的。(如指针的使用和数据的打印等等

下面我们把这些基本类型进行再分类

整型家族:

一般编译器下,我们写的char short int long 默认为signed char short int long,也就是带符号的整型

浮点数家族:

构造类型:

指针类型:

以及空类型(void),也算老相识了,在函数返回类型,函数参数,指针类型里经常用到。


2. 整形在内存中的存储:原码、反码、补码

在计算机中整数一般有三种表示方法,即原码、反码、补码,这三种方法均包含符号位和数值位两部分,其中最高位为符号位,用0表示+,1表示-

原码:直接按照二进制将十进制转换即可(包含符号)
反码:原码的符号位不变,其他位按位取反
补码:反码+1
正数的原码反码补码相同,负数的则需要按上述进行运算。

对于整型来说数据的存储其实是用的补码,是因为CPU中只有加法器,使用补码可以将符号位和数值域统一进行处理,加减法运算统一处理。此外,原码、补码可以互相转换,运算过程相同,不需要额外的硬件电路。

这里我们给出一个负数的例子,看一下它在内存中是怎么存储的

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
	int a = -10;
	return 0;
}

内存窗口中数据以16进制显示

首先a是一个int类型,那就开辟4个字节,32个比特位

-10的原码:10000000 00000000 00000000 00001010
-10的反码:111111111 111111111 111111111 111110101
-10的补码:111111111 111111111 111111111 111110110

16进制:         ff                 ff               ff              f6 
数据看来是一样的,验证了数据存储的是补码,但是为什么数据的存储顺序好像是反着的呢,下面来解决这个问题。


 

3. 大小端字节序介绍及判断

首先,什么是大小端字节序?

  大端(存储)模式,是指数据的低位(低字节)保存在内存的高地址中,而数据的高位(高字节),保存在内存的低地
址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地
址中。  
比如   int a=0x11223344;0x44为低字节,0x11为高字节
 那么上面-10的存储是不是就解决了呢,因为VS编译器采用的是小端字节序,因此会出现上图结果
       接下来,为什么会有大小端字节序?
       这是因为在计算机系统中,我们是以字节为单位的,每个地址单
元都对应着一个字节,一个字节为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处理器还可以由硬件来选择是大端模式还是小端模式。
        那如何判断我们的电脑是大端还是小端呢?
#include <stdio.h>
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; }

首先,假定一个变量i是正数1,原码(补码)为00000000 00000000 00000000 00000001 
那么,如果我们只需要访问第一个字节的数据,如果是小端存储,那访问到的就是00000001 ,十进制也就是1,如果是大端存储,那访问到的就是00000000,也就是0,而因为i是一个整型,用整型指针访问时一次会访问4个字节,不管是大端还是小端都会返回1。因此返回的时候强制类型转换把i的地址存到char* 类型中再解地址访问,就可以访问一个字节。


 理解了大小端字节序,我们来看几道题目来巩固理解,会先后给出代码和输出结果。分别判断下面的代码输出什么(需要首先知道计算表达式和存储数据的时候用数据的补码,printf打印的时候用原码)

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);
    return 0; }

首先,上面已经说过,大多数情况下默认char就是signed char  因此a,b的定义是一样的,-1的补码为11111111 11111111 11111111 11111111,存储到char类型中发生截断,因此a,b中存储的数据为11111111,再打印a和b时打印的是%d形式又要发生整型提升(1字节到4字节),补最高符号位到32位,又变成了 11111111 11111111 11111111 11111111,注意这个二进制序列还是补码,打印的时候要还原成原码,补码-1再除符号位按位取反,变成10000000 00000000 00000000 00000001 也就是-1的原码,因此打印-1,那为什么打印c确实255呢 ?因为c在定义的时候类型是无符号char类型,因此-1的补码11111111 11111111 11111111 11111111截断后11111111就会被当成一个正数的补码来存储到c中,打印时发生整型提升,因为无符号,因此整型提升时补0, 00000000 00000000 00000000 11111111,而正数的原码和补码相同,因此这也是c中数据的原码,即十进制255,因此打印后就是255

2.

#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\n",a);
    return 0; }

 -128的补码是11111111 11111111 11111111 10000000,截断后存储到c中的就是10000000
打印时因为a是个有符号char,因此发生整型提升补最高位符号位1,11111111 11111111 11111111 10000000,以%u形式打印,认为内存中存储的是无符号数,那么原码又等于补码,因此就打印11111111 11111111 11111111 10000000对应的十进制数,也就是如图结果,可以用计算器进行验证

3.

#include <stdio.h>
int main()
{
    char a = 128;
    printf("%u\n",a);
    return 0; }

结果和上面一样,这里只是把a变成了+128,128的补码也就是原码为10000000 00000000 00000000 10000000,发生截断,存储在a中的就是10000000,接下来发生和上一题同样的步骤,因此结果也是一样的,因为底层逻辑a中存储的数据是一样的,a数据的类型也都是有符号char

4.

#include <stdio.h>
int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	return 0;
}

 -20的补码:11111111 11111111 11111111 11011000
  10的补码:00000000 00000000 00000000 00001010
相加后补码  11111111 11111111 11111111 11110110
转换为原码  10000000 00000000 00000000 00001010   也就是-10

5.

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

 首先,死循环是肯定的,因为i是无符号整型,肯定会一直大于等于0进入循环,当i=0时,进入循环,打印0,然后i--变成-1,-1的补码是32位全1,以无符号形式打印就会把-1的补码当成一个无符号数的补码,而无符号数的原码和补码相同,因此打印32位全1对应的十进制

 也就是上图的4294967295,此后类推,就会逐步打印上述结果

6.

#include <stdio.h>
int main()
{
	char a[1000];
	int i;
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d", strlen(a));
	return 0;
}

 首先我们知道strlen计算字符串长度遇到\0停止,而数组a是char类型的,也就是遇到一个字节8个比特位全为0的时候就停止计算长度,-1的补码,老生常谈了,32位全1,i是一个整型,对应的数据的二进制补码也是32位的,而数组a中的元素是char类型的,存储时发生截断,留下-1-i对应的低八位的补码,也就是我们要找的数字i就变成了补码低八位全为1,能和-1的补码相减后变成0的i,i还是个正数,补码原码相同,也就是要找i对应的二进制的低八位为全1的时候,那不就是00000000 00000000 00000000 11111111对应的i么,也就是255,因此本题答案就是字符串长度为255。

到这里这个题已经可以做出来了,但是我们再来深入剖析一下更深层的东西,来看一下有符号、无符号的char类型的十进制取值范围,下面的二进制序列都是补码,

 无符号的char取值范围是0-255

有符号的char取值范围是-128~127,其中10000000对应的就是-128,那上面的题第一次出现低八位为全0的时候是不是就是127+128=255,也是一种思路。

7.

#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0; }

 结果应该是死循环,因为上面已经讨论过了,无符号char的取值范围是0-255,循环条件恒成立,因此无限循环

 

嗨呀累的一批就这几道题写了我好久啊

4. 浮点型在内存中的存储解析

浮点数的存储规则:

根据国际标准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 。
那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.5,写成二进制是 -101.1 ,相当于 -1.011×2^3 。那么,s=1,M=1.011,E=3。
IEEE 754 规定:
对于 32 位的浮点数,最高的 1 位是符号位 s ,接着的 8 位是指数 E ,剩下的 23 位为有效数字M。

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

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 为一个无符号整数( 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。
指数 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,补齐0到23
位00000000000000000000000,则其二进制表示形式为:0 01111110 00000000000000000000000   E 全为 0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,
有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于
0的很小的数字。
E 全为 1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);
理解了上面的内容我们来看一个题
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; }

首先n的值是9是肯定没有问题的,当把n的地址存储到pFloat中时,9的原码补码相同,但因为pFloat是float类型,因此存储时:

 E为全0,因此打印的时候就是0.000000(默认保留6位小数)

再来看第三个打印结果,9.0转换为二进制是1001.0,(-1)^0*1.001*2^3,存储的时候S就是0,E就是3+127=30,M就是1.001,因此二进制序列为0 10000010 001 0000 0000 0000 0000 0000

这个二进制序列还原成十进制就是1091567616

结束

 



至此本文所有内容结束,从底层剖析了数据是如何存储的,其中浮点数的存储稍有拓展,好好掌握冲冲冲

 


点击全文阅读


本文链接:http://m.zhangshiyu.com/post/38157.html

补码  字节  符号  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1