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语言中提升规则如下图
说明
- 当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进阶之深度解刨数据在内存中的存储 冲鸭~的主要内容,如果未能解决你的问题,请参考以下文章