C进阶之深度解刨数据在内存中的存储 冲鸭~

Posted 王嘻嘻-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C进阶之深度解刨数据在内存中的存储 冲鸭~相关的知识,希望对你有一定的参考价值。

  • 数据类型及其取值范围

  • 整形在内存中的存储

  • 大小端字节序介绍

  • 整型提升以及截断

  • 浮点型在内存中的存储解析 


 一、数据类型及其取值范围

在C语言中数据的类型主要可以概括为五大类:整形、浮点数、构造类型、指针类型和空类型。

(1.)整型:char(字符型)和unsigned char(无符号字符型)、short (短整型)和unsigned short(无符号短整型)、int(整型)和unsigned int(无符号整型)、long(长整型)和unsigned long(无符号长整型)以及long long(更长的整型)和 unsigned long long(无符号更长的整型)。

(2.)浮点数:float(单精度浮点数)和double(双精度浮点数)。

(3.)构造类型:数组类型、结构体类型struct、枚举类型enum以及联合类型union。

(4.)指针类型:在C语言中,指针类型就是数据类型,是给编译器看的。

(5.)空类型:void就是无类型,通常用于函数的返回类型、函数参数、指针类型。

取值范围
char有符号字符数据类型[-128,127]
unsigned char无符号字符数据类型[0,255]
short短整型[-32768,32767]
unsigned short无符号短整型[0,65535]
int整型[-2147483648,2147483647]
long长整型

[-2147483648,2147483647]

long long更长整型[-9223372036854775808,9223372036854775807]
float单精度浮点数[-2^128,2^128]
double双精度浮点数

[-2^1024,2^1024]

注意:

对于有符号整形的数据,C语言中用最高位位1,其余位全为0这种状态来表示某一类型中最小的负数。例如char类型,在内存中占8个比特位,规定1000 0000表示该类型取的最小值-128,其余有符号整型原理相同。

二、整形在内存中的存储

原码、反码、补码

计算机中的有符号数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”。 

正数的原、反、补码都相同。       

负数的原、反、补码求法:
原码:直接将十进制数按照正负数的形式转化成二进制数。
反码:原码的符号位保持不变,其他位依次按位取反。
补码:反码+1。

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

三、大小端字节序介绍

1.大小端定义:

大端(存储)模式:数据的低权值位保存在内存的高地址中,而数据的高权值位,保存在内存的低地址中。
小端(存储)模式:数据的低权   值位保存在内存的低地址中,而数据的高权值位,保存在内存的高地址中。  简称 小小小

2.为什么会有大小端模式之分呢?

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

例:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22为低字节。对于大端模式,就将0x11 放在低地址 0x0010 中, 0x22 放在高地址0x0011 中。小端存储模式,就是将就将0x11放在高地址0x0011中,而0x22放在低地址0x0010中。我们常用的X86结构为小端存储模式。

3.判断大小端 (在vs2019环境下输出都是小端)

  • 联合法
#include <stdio.h>
#include <windows.h>

//联合的特性:所有的成员共用一个空间,故又称共用体

int main()
{
    union test
    {
        int x;
        short y;
    };  //联合体中的变量x和y共用一个空间,它们都是从低地址开始存放的。

    union test a;
    a.x=0x00001111;
    if(a.y==0x1111)
    {
        printf("little endian");   //小端
    }
    else
        printf("big endian");     //大端
    system("pause");    
    return 0;
}
  • 指针
#include <stdio.h>
#include <windows.h>

int main()
{
    int a=1;
    unsigned char* ap=(unsigned char*)&a;
    if(*ap)
    {
        printf("little endian");
    }
    else
        printf("big endian");
    system("pause");
    return 0;
}

指针强制类型转换后指向数据的低地址位。因此,如果是小端字节序,强制转换后指针指向地址上的值为1。相反,如果是大端字节序,指针指向地址上则为0。

  • 直接强制类型转换
#include <stdio.h>
#include <windows.h>

int main()
{
    int a=0x00001111;
    short b=(short)a;    //强制类型转换
    if(b==0x1111)
    {
        printf("little endian");
    }
    else
        printf("big endian");
    system("pause");
    return 0;
}

四、整型提升以及截断

整型提升的定义:不同数据类型在混合运算时,时常会进行整型提升或者强制转换的操作,C语言中提升规则如下图

C语言中提升规则

说明

  • 当short或者char类型数据进行混合运算时,统一都会转成int类型,根据左值的数据类型再做转换,所以一般不会有溢出或者截断现象。
  • float/double类型向整型转换时,直接截断取整,去掉小数点后的数据。

整型提升的意义
    表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是 int 的字节长度,同时也是CPU的通用寄存器的长度。
    因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。
    通用CPU(general-purpose CPU)是难以实现两个8比特字节直接相加运算。所以,表达式中各种长度可能小于 int 长度的整型值,都必须先转换为 int unsigned int,然后才能送入CPU去执行运算。

在什么情况下会发生?

//例题
#include <stdio.h>
#include <stdlib.h>

int main()
{
    char a = -1;
    unsigned char b = -1;
    printf("a=%d,b=%d\\n",a,b);   //a=-1,b=255
    system("pause");
    return 0;
}

分析:
首先整型提升的规则是:
a.若原类型是有符号的数(singned),则前面补符号位
b.若原类型是无符号数(unsigned),则前面补0

①对a来进行分析
因为-1为int类型,大小为4个字节,且为负数,故其在内存中是按照补码形式存储的,其原码为1000 0000 0000 0000 0000 0000 0000 0001,其反码为1111 1111 1111 1111 1111 1111 1111 1110,其补码为1111 1111 1111 1111 1111 1111 1111 1111,但是a是char类型的,大小为1个字节,故这里会发生截断,将4个字节截断成为1个字节,其截断后结果为 11111111。但是最后要求以%d的形式输出,所以此处要进行整型提升,将截断后的1个字节提升为4个字节的整型。a为有符号类型的数,整型提升就是在截断后的结果前面补3个字节长度的符号位(即1)。整型提升后的结果为1111 1111 1111 1111 1111 1111 1111 1111。根据最高位(即符号位)是1可得出该结果为负数,则将其补码形式转换成原码为1000 0000 0000 0000 0000 0000 0000 0001的二进制数,其对应的十进制数为 -1。所以最终以%d形式输出的a为 -1。
②对b来进行分析
因为-1为int类型,大小为4个字节,且为负数,故其在内存中是按照补码形式存储的。从对a的分析可知,其补码为1111 1111 1111 1111 1111 1111 1111 1111,但是a是unsigned char类型的,大小为1个字节,故这里会发生截断,将4个字节截断成为1个字节,其截断后结果为 11111111。但是最后要求以%d的形式输出,所以此处要进行整型提升,将截断后的1个字节提升为4个字节的整型。a为无符号类型的数,整型提升就是在截断后的结果前面补3个字节长度的0。整型提升后的结果为0000 0000 0000 0000 0000 0000 1111 1111 ,转成十进制数 255。

【~更多练习题请看前一篇文章 ^_^】

五、浮点型在内存中的存储解析 

1.浮点数存储规则

根据国际标准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。

2.浮点数的存储 

(存的是与S M E相关的值)

IEEE 754规定:

单精度浮点型:

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

单精度浮点数存储模型

双精度浮点型:

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

双精度浮点数存储类型

1、IEEE 754对S的规定: 负数放1,正数放0

2、IEEE 754对M的规定:1≤M<2

也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。(就是要我们可以多存一位,精度就更高了,所以当我们真实存储的时候,只存储了小数点之后的位数)

比如保存1.01的时候,只保存 01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。

3、IEEE 754对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。

例如:

float f = 5.5f;            // 科学计数法为(-1)^0 * 1.011 * 2^2

                                // S = 0   M=1.011   E=2 + 127

存储 0 10000001 01100000000000000000000

16进制:(以16进制在内存中存储)   40 B0 00 00

3.浮点数的取出

指数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

我们存进去的时候是加了127存进去的,那么存进去之后发现为0了,那么真实的E至少为-127

,那么那个数字就会很小,为± 1.xxxx * 2^(-127)

这时,浮点数的指数E等于1-127(或者1-1023),即为真实值.

有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

3、E全为1

如果全为1,那么是128加上127,那么这个数字就会是正负无穷大了

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位S),再将有效数字M前加上第一位的1。

例题:

//例题
#include <stdio.h>
#include <windows.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); 
    system("pause");
	return 0; 
}

 运行结果为:

分析:

1、首先n为整数,是以二进制放入内存,

9 的原、反、补码相同 00000000000000000000000000001001  打印出来是9。

2、接着float *pFloat = (float *)&n。在pFloat角度来看,认为数是浮点数,认为二进制序列也是浮点数,则符号位S为0,E为00000000 ,M为00000000000000000001001,取的时候发现E为全0,这个时候还原E为1-127=-126,M=0.00000000000000000001001,S=0,这个数无线接近于0,所以结果是0.000000

3、接着*pFloat = 9.0,写成二进制 1001.0,写成科学计数法:(-1)^0 * 1.001 * 2^3.那么E=3,M=1.001,S=0,那么E+127存入,即为(二进制为):0 10000010 00100000000000000000000

以%d的形式打印,符号位为0,则为整数,原、反、补码相同,所以打印出来为1091567616

4、最后,打印浮点数,为 9.000000。


thank you  ^_^ 

以上是关于C进阶之深度解刨数据在内存中的存储 冲鸭~的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶笔记用图文+代码让你深度理解数据在内存中的存储

从初识到进阶,硬核解说C语言< 进阶篇 1 > 深度剖析数据在内存中的存储

深度解刨C语言内存管理(详)

C语言进阶——数据的存储

C语言之深度剖析数据在内存中的存储

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