一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)

Posted bug 郭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)相关的知识,希望对你有一定的参考价值。

✂️ 写在前面

经过上篇博客的学习,你已经知道了数据的运算那数据在内存中又是如何存储的呢?
今天bug郭就带你一起学习数据在内存中的储存!

💯 本章重点

  1. 数据类型详细介绍
  2. 整形在内存中的存储:原码、反码、补码
  3. 大小端字节序介绍及判断
  4. 浮点型在内存中的存储解析

📖 数据类型介绍

那些我们学过的C语言数据类型,你还记得多少,我们一起来整理一一下吧📖

👁内置类型

char  //字符型 1byte
int  //整型    4byte
short//短整型2byte
long  //长整型4/8byte
long long   //更长的整型8byte
float //单精度浮点型  4byte
double//双精度浮点型8byte
//C语言中无字符串类型

类型的意义
之前的博客中已经介绍过了

  • 类型可以决定该类型的变量在内存中创建内存空间的大小
  • 类型可以决定指针访问的权限,加减指针的位移

我们可以根据我们变量的大小合理选择类型,创建空间大小。

不同的数据类型根据它们的字节大小,需要占用不同空间大小的内存空间

类型的基本归类

整型家族

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

注意:字符型也归类为整型家族,每个类型都有有符号类型和无符号类型。

浮点数家族

float  
double

构造类型

//结构体类型 
struct
//枚举类型
enum
//联合类型
union

指针类型

char*
int*
float*
void*

空类型

void 

void空类型
通常使用在函数的参数,返回值,指针。

™️整形在内存中的存储

我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们来看看整型是如何存储的。
例如:

int a=1;
int b=-3;

我们已经知道整型占用内存空间为4个字节。那么是如何分配储存的!
我们先来了解一下计算机中有符号数的三种表示方法:
原码,反码,补码

  • 计算机中有符号数有三种表示方法,原反补。
  • 这三种表示方法,都是由符号位和数值位组成,符号位1表示负数,0表示正数,数值位各不相同!
    原码

直接将数据通过二进制正负的形式翻译过来的的二进制位

反码

由原码,符号位不变,数值位按位取反。

补码

反码+1得到补码!

正数的原反补相同

数据是以补码的形式在内存中存储
为啥是补码呢?
学过计算机原理的同学肯定了解,因为计算机的CPU中运算器(ALU)只能进行加法!所以负数要转化成加法运算,而补码很好的解决了这个问题!

✔️ 大小端


根据我们之前博客的学习👁,避免bug,调试技巧我们已经知道了,调试窗口,可以查看变量的地址和内存,我们&x可以查看到x在计算机中内存的储存。

int x=1;
//x为整型有32二级制位
//而每4个二进制位是一个16进制位,
//x=1的16进制表示方法:00 00 00 01

而我们看到vsx的内存,低位01却存在最左边。
为啥会存到最左边呢?
我们可以看到x占用4个字节空间,地址从左往右依次递增!低地址存低位字节数据,高地址存高位字节数据。
这就是我们所介绍的小端存储。
而大端存储,不言而喻就是,高地址存低位,低地址存高位!
总结:

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

为啥会有大小端

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bitchar之外,还有16bitshort型,32bitlong型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如一个16bitshortx ,在内存中的地址为0x0010x 的值为0x1122 ,那么0x11 为高字节,0x22为低字节。对于大端模式,就将0x11 放在低地址中,即0x0010 中,0x22 放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的X86 结构是小端模式,而KEIL C51 则为大端模式。很多的ARMDSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

总结:
计算机寄存器宽度大于 一个字节,那么就多个字节类型数据的存储就产生了不一样的大小端存储模式。

🗝判断大小端

我们已经知道有大小端两种存储模式,而我们要如何判断一台机器是小端存储,还是大端储存呢?也就是判断当前机器的字节序?

我们可以设计几个程序,来验证该不同机器的字节序。
设计思路
我们可以想办法将某一地址处存的字节数据拿出即可判断,如果高地址低字节位,说明是小端存储,否者就是大端存储模式。

//代码一
//利用char*指针得到低地址的字节数据
#include<stdio.h>
int main()
{
	int a=1;
	int *pa=&a; 
	//利用char*存储a第一个字节的低地址
	char*pc=(char*)pa;
	printf("%d",*pc);//访问这个字节的地址,打印数据
	return 0;
}


低地址打印了低字节位,说明bug郭的机器是采用小端存储模式!
我们刚刚是说写个程序,判断字节序,所以我们需要封装一下!

//代码1
#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;
}

我们之前还了解到了一个C语言自定义类型联合体,我们后期还会详细介绍!
联合体就是一块空间,多个变量联合使用,共同占用一块空间!当我们访问其中一个变量,该空间就存储着该变量!
我们可以利用联合体这一特性来判断字节序

//代码2
int check_sys()
{	
	union 
	{
	int i;
	char c;
	}un;
	un.i=1;
	return un.c;
}


学会了吗,这就是大小端的判断!

👊 小试牛刀

到这里我们已经学习了整型在内存中如何存储,我们来写几个练习巩固一下吧!

练习题目

下面一共7道题目
大家可以试着练习一下,我会给大家一一讲解

//练习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;
}
//练习2
#include <stdio.h>
int main()
{
    char a = -128;
    printf("%u\\n",a);
    return 0;
}
//练习3
#include <stdio.h>
int main()
{
    char a = 128;
    printf("%u\\n",a);
    return 0;
}
//练习4
#include <stdio.h>
int main()
{
  	int i= -20;
	unsigned  int  j = 10;
	printf("%d\\n", i+j); 
	//按照补码的形式进行运算,最后格式化成为有符号整数
}
//练习5
#include <stdio.h>
int main()
{
	 unsigned int i;
	for(i = 9; i >= 0; i--)
	{
   		 printf("%u\\n",i);
	}
 return 0;
}
//练习6
int main()
{
    char a[1000];
    int i;
    for(i=0; i<1000; i++)
    {
        a[i] = -1-i;
    }
    printf("%d",strlen(a));
    return 0;
}
//练习7
#include <stdio.h>
unsigned char i = 0;
int main()
{
    for(i = 0;i<=255;i++)
    {
        printf("hello world\\n");
    }
    return 0;
}

📌 练习讲解

//练习1
//输出什么?
#include <stdio.h>
int main()
{
    char a = -1; 
    //-1 : 补码 11111111 11111111 11111111 11111111 
    //截断放入 a:  11111111 
    signed char b = -1;
    //-1截断放入b中 b: 11111111 
    unsigned char c = -1;
    // 同理 c: 11111111 
    printf("a=%d,b=%d,c=%d", a, b, c);
    //a和b是有符号字符型,%d打印整型提升补充符号位后
    //补码 11111111 11111111 11111111 11111111 
    //得到原码:10000000 00000000 00000000 00000001 
    //所以a和b打印结果是-1
    //而c是无符号字符型,所以整型提升,补充0
    //00000000 00000000 00000000 11111111 
    //转换原码 00000000 00000000 00000000 11111111 
    //所以c的打印结果是 225
    return 0;
}

运行结果

//练习2
#include <stdio.h>
int main()
{
    char a = -128;
    //-128 原码:00000000 00000000 00000000 10000000
    //补码: 11111111 11111111 11111111 10000000
    //截断存入char a 中  10000000
    printf("%u\\n", a);
    //%u无符号的形式打印
    //a是有符号char 整型提升补充符号位
    // 11111111 11111111 11111111 10000000
    //而%u默认该数据为无符号数据,所以认为a的原码补码相同
    //打印结果 4294967168
    return 0;
}

运行结果

//练习3
#include <stdio.h>
int main()
{
    char a = 128;
    //128 00000000 00000000 00000000 10000000
    //截断存入char a :10000000
    printf("%u\\n", a);
    //整型提升char a有符号,补符号位 
    //11111111 11111111 11111111 10000000
    //%u无符号的形式打印,认为该数据原码补码相同
    //打印 4294967168
    return 0;
}

看到练习3的结果和练习2的结果一样,一个是-128,一个是128
但以%u打印了一样的结果!

因为无论是128还是-128截断后存储到a都是相同的二进制位!

//练习4
#include <stdio.h>
int main()
{
    int i = -20;
    //-20 原码:10000000 00000000 00000000 00010100
    //补码 : 11111111 11111111 11111111 11101100
    unsigned  int  j = 10;
    //10 :  00000000 00000000 00000000 00001010
    printf("%d\\n", i + j);
    //i+j补码:11111111 11111111 11111111 11110110
    //原码:   10000000 00000000 00000000 00001010
    // 打印 -10
    //按照补码的形式进行运算,最后格式化成为有符号整数
}

运行结果

//练习5
#include <stdio.h>
int main()
{
    unsigned int i;  
    //无符号int i 所以始终大于等于0
    for (i = 9; i >= 0; i--)
    {
        printf("%u\\n", i);
        //无法退出循环
    }
    return 0;
}

unsigned int范围:0~2^32
代码会发生死循环!

运行结果

//练习6
#include<stdio.h>
int main()
{
    char a[1000];
    int i;
    for (i = 0; i < 1000; i++)
    {
        a[i] = -1 - i;
        //a[i]是字符型,范围为-128~127
        //超过会进行截断存入a[i]中
        //当-129存入a[128]中截断
        //-129 :原 10000000 00000000 00000000 10000001
        //   补码  11111111 11111111 11111111 01111111
        //截断 后存入a[128]中 01111111
        //此时a[128]符号位为0  故存入的为 127
        //后面数据同理
        //当a[255]=-256 
        // -256 原 10000000 00000000 00000001 00000000
        //补码 :   11111111 11111111 11111111 00000000
        // 故此时a[255]存入的是 0
    }
    printf("%d", strlen(a));
    //strlen 遇到'\\0'停止计数,也就arr[255],所以返回长度为255
    return 0;
}


char中的范围就是这样的,所以但一个数据小于-128时下一个数据就是127 大于127下一个数据就是-128
运行结果

//练习7
#include <stdio.h>
unsigned char i = 0;
//unsigned char  范围为0~255
int main()
{
    for (i = 0; i <= 255; i++)
    {
        printf("hello world\\n");
        //当i=255时,i++后,i循环回到i=0
        //所以该代码会发生死循环
    }
    return 0;
}

运行结果

这就是所以练习的答案了,是不是还有点意犹未尽!如果还没学会可以多看几遍!

⭐️ 重点归纳总结

  • 计算机中数据的存储和计算都是以补码的形式进行的!
  • 整型提升还有截断的对象也是针对补码。
  • 无符号整型提升,二级制位补充0,有符号整型提升,二进制位补充符号位。
  • %u(无符号打印)自动认为打印的数据是无符号数据,所以存储的补码也就是原码,%d(有符号打印)认为打印的数据是有符号类型的,要将数据转换成原码打印输出!

💦 浮点型在内存中的存储

我们已经学会了整型在内存中的存储,你肯定会好奇,浮点型数据该怎样存储在内存中呢?

常见的浮点数:

3.14159 1E10  2.7 

浮点数家族包括:

float、double、long double 类型。

浮点数表示的范围:
vsfloat.h有详细介绍浮点数的表示范围,有兴趣的伙伴可以期查阅一下,bug郭截取了一段供大家参考:

// float.h
//
//      Copyright (c) Microsoft Corporation. All rights reserved.
//
// Implementation-defined values commonly used by sophisticated numerical
// (floating point) programs.
//
#pragma once
#ifndef _INC_FLOAT // include guard for 3rd party interop
#define _INC_FLOAT

#include <corecrt.h>

#pragma warning(push)
#pragma warning(disable: _UCRT_DISABLED_WARNINGS)
_UCRT_DISABLE_CLANG_WARNINGS

_CRT_BEGIN_C_HEADER



#ifndef _CRT_MANAGED_FP_DEPRECATE
    #ifdef _CRT_MANAGED_FP_NO_DEPRECATE
        #define _CRT_MANAGED_FP_DEPRECATE
    #else
        #ifdef _M_CEE
            #define _CRT_MANAGED_FP_DEPRECATE _CRT_DEPRECATE_TEXT("Direct floating point control is not supported or reliable from within managed code. ")
        #else
            #define _CRT_MANAGED_FP_DEPRECATE
        #endif
    #endif
#endif

大家肯定会疑问,这是个啥,看不懂啊,其实bug郭也看不懂,哈哈哈,不过问题不大!

浮点型的其类型说明符有float 单精度说明符,double 双精度说明符。在Turbo C中单精度型占4个字节(32位)内存空间,其数值范围为3.4E-38~3.4E+38,只能提供位有效数字。双精度型占8 个字节(64位)内存空间,其数值范围
1.7E-308~1.7E+308,可提供16位有效数字。

兄弟们,我们写个代码看看,你就了解了浮点型!

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

输出结果会是怎么样的?会是像我们整形数据那样分析吗?
结果肯定是否定的!

运行结果:

可以看到打印结果完全出乎我们意料,num*pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。所以我们可以知道,浮点型数据和整型数据在计算机中有着不一样的存储方式!

👍 浮点数存储方式介绍

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

  • (-1)^S * M * 2^E
  • (-1)^s表示符号位,当s=0V为正数;当s=1V为负数。
  • M表示有效数字,大于等于1,小于2
  • 2^E表示指数位。

举例来说:

十进制的5.0,写成二进制是101.0 ,相当于1.01×2^2 。 那么,按照上面V的格式,可以得出s=0M=1.01E=2
十进制的-5.0,写成二进制是-101.0 ,相当于-1.01×2^2 。那么,s=1M=1.01E=2

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

对于double 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,情况就比较复杂。
首先,E为一个无符号整数(unsigned int) 这意味着,如果E8位,它的取值范围为0~255;如果E11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10E10,所以保存成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,补齐023
    00000000000000000000000,则其二进制表示形式为:
    0 01111110 00000000000000000000000
  • E全为0
    浮点数的指数E等于1-127(或者1-1023)即为真实值, 有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
  • E全为1
    这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);

好了,关于浮点数的表示规则,就说到这里。

学到这我们了解了浮点数据的存储方式,就可以把刚刚的运行

以上是关于一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)的主要内容,如果未能解决你的问题,请参考以下文章

# yyds干货盘点 # 一文带你解读​JavaScript中的变量作用域和内存问题

一文带你搞懂 MySQL 中的分区!

一文带你彻底搞懂Docker中的cgroup

一文带你彻底搞懂Docker中的cgroup

一文带你彻底搞懂Docker中的cgroup

简直不要太硬了!一文带你彻底理解文件系统