数据在内存中的存储(浮点数与整数的类型转换)

简介: 在c语言操作符那一篇文章中我们讲到整数的二进制表示方法有三种,即原码、反码和补码。 它们都由符号位和数值位组成,数值位中的最高位就是符号位,符号位中0表示”正“,1表示”负“,

1、整型在内存中的存储  

复习旧知

在c语言操作符那一篇文章中我们讲到整数的二进制表示方法有三种,即原码、反码和补码

它们都由符号位数值位组成,数值位中的最高位就是符号位,符号位中0表示”正“,1表示”负“,

10001001010101010101001010101000   //符号位为1、负数

00001001010101010101001010101000   //符号位为0、正数

其中,正整数的原、反、补码都相同,负整数三种码的表示方法各不相同:

已知真值求负数的原码、反码和补码:

原码:符号位变为1,其余各位不变

反码:符号位变为1,其余各位取反

补码:反码加一

tips:原码 = 补码取反加一

预习新知:

数据在内存中是以二进制补码的形式存储的,但是在vs中是以十六进制的形式显示的:

int main()
{
int a = 11;
/00000000  00000000  00000000  00001011   //二进制补码
//0x  00  00  00  0b                      //vs中显示的
return 0;
}

~在内存中查a的地址时要先输入取地址操作符&然后回车,这样才能找到~

查看内存时发现它的存储顺序与我们想象的存储顺序刚好相反,这是因为vs是一个小端机器~

补充:数据在内存中为什么是以二进制补码的形式存储?

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

2、大小端字节序和字节序判断

       我们在上面中的调试中我们说到了vs是一个小端机器,那么什么是小端?有小端是不是还会有大端?它们又有什么用?

什么是大小端字节序?

概念:大小端是指在多字节数据类型(如整数、浮点数)的存储过程中,字节的排列顺序

为什么要有大小端字节序?

因为如果当一个数值占用的内存空间超过一个字节时,它存储在内存中时就会面临字节的存储顺序问题,比如int a = 0x11223344,它在内存中的存储顺序就有十六种.....


 这只是其中的三种,其实这些存储顺序都没有什么问题,但是当我们读取这些内容的时候就会出现要先读取哪一个字节的问题,所以我们引入了大小端字节序的概念来规范字节在内存中的存储顺序。如果你觉得我描述的还是太简单了请看下面这段话:

    在计算机系统中,我们是以字节为单位的,每个地址单元都对应⼀个字节,⼀个字节为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处理器还可以由硬件来选择是⼤端模式还是⼩端模式。


 总之,不同的处理器和操作系统有不同的字节序规定,如果没有统一的规范,数据在不同系统之间传输时可能会出现混乱或错误解析的问题。因此,大小端的存在可以方便不同系统之间的数据交换和解析,确保数据能够正确地传输和处理。在网络通信、文件传输和跨平台开发等领域,正确处理大小端是非常重要的。

必要补充:

还有一点要补充的是:在内存窗口中地址0x0000007104EFFD14存放的其实是44的地址,而33、22和11的地址其实就是在该地址上加一,也就是0x0000007104EFFD15 / 16 / 17



 了解这点有利于在后面判断大小端字节序的时候,知道到底哪里是内存的低地址和高地址,当然如果你以前就知道了的话就更好了😀

补充2.0:一个十六进制表示的数字0x12345678中,12是最高位,78是最低位

大端字节序

       将一个数值的十六进制表示中的低位字节存放到高地址处,将高位字节存放到低地址处

int a = 0x11223344,以大端字节在内存中存储就是11  22  33  44的顺序存储方式

小端字节序

       将一个数值的十六进制表示中的低位字节存放到低地址处,将高位字节序存放到高地址处

int a = 0x11223344,以小端字节在内存中存储就是44  33  22  11的逆序存储方式

练习:判断当前编译环境的字节序(百度面试题)

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

关于*(char*)&a的解释:

判断方式:判断是大小端只需要判断开头的字节是00还是01

具体操作是:先&a得到a的地址,然后强制类型转换(char*),最后*a得到a最低地址处字节的值

根本原因:无论按照大端字节序排列还是小端字节序排列,解引用char*类型的a后获取的都是a在最低地址处存储的字节的值,故若为大端机器由于1的二进制补码是00 00 00 01的形式(简写了)所以说最低地址处放的字节应该就是00,若为小端机器在最低地址处存放的字节就是01


如何进行整型提升?

①: 有符号整数提升,补码符号位为0则整型提升时高位全补零,符号位为1则高位全补1.

② :⽆符号整数提升,⾼位全补0  

无符号 == unsigned

在vs中char即为signed char

不同编译器下char代表的可能是signed char也可能是unsigned char

练习一:

#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 a = -1;
//10000000  00000000  00000000  00000001     原码
//11111111  11111111  11111111  11111110            反码
//11111111  11111111  11111111  11111111            补码
//截断:char类型数据是一个字节八个比特位,而-1是整型是四个字节三十二个比特位,相当于一个一米的洞你往里面塞四米长的东西,故为了刚好塞入就要把多的部分截断
从补码低位开始截八个比特位
//11111111      a截断后
signed char b = -1;
//10000000  00000000  00000000  00000001     原码
//11111111  11111111  11111111  11111110            反码
//11111111  11111111  11111111  11111111            补码
//截断:截断方式同char a
//11111111       b截断后
unsigned char c = -1;
//10000000  00000000  00000000  00000001     原码
//11111111  11111111  11111111  11111110            反码
//11111111  11111111  11111111  11111111            补码
//截断:负数先转为补码后再判断是否需要截断
//11111111       c截断后
printtf("a=%d,b=%d,c=%d",a,b,c);
//%d  -  以十进制的形式打印有符号的整型
经过描述可知:
//11111111      a截断后
//11111111      b截断后
//11111111      c截断后
%d是以十进制形式打印有符号整型,故进行整型提升时的结果为:
//11111111  11111111  11111111  11111111               a整型提升结果(补码)
//10000000  00000000  00000000  00000000        a的反码
//100000000  00000000  00000000  00000001      a的原码
//-1        打印a的结果
//11111111  11111111  11111111  11111111               b整型提升结果(补码)
//10000000  00000000  00000000  00000000        b的反码
//100000000  00000000  00000000  00000001      b的原码、
//-1        打印b的结果
//00000000  00000000  00000000  11111111          c整型提升结果(补码)
//00000000  00000000  00000000   11111111         c的原码
//255      打印c的结果

练习二:

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

解析:

char a = -128;
//10000000  00000000  00000000  10000000     原码
//11111111  11111111  11111111  01111111            反码
//11111111  11111111  11111111  10000000            补码
//10000000     a截断后
prntf("%u\n",a);
%u打印无符号整数,无符号整数也是整数而a是char a所以仍需要整型提升
//11111111  11111111  11111111  10000000      a整型提升结果(其实无符号类型没有补码)
//4294967168  打印a的结果(打印无符号位整数,不需要再求出原码反码直接进行计算将其转换为具体的数即可,这是因为无符号数没原码反码补码的概念)
printf("%d\n", a);
//11111111  11111111  11111111  10000000      a整型提升结果(补码)
//10000000  00000000  00000000  01111111     a的反码
//10000000  00000000  00000000  10000000   a的原码
//-128             打印a的结果

练习三:

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

貌似打印结果应该是一千?实际上:



这是因为,最后打印的是字符数组a的长度,而strlen求字符数组长度,计算的是'\0'之前出现的字符的个数,而'\0'的ASCII码值为0,故找到值为0的数组元素即可

     通过观察监视窗口对字符数组a的遍历过程,我们发现当遍历到第255次时出现了值为0的数组元素且此时它的ASCII码值是'\0',这就说明strlne函数读取到这里时就会停止读取,而这时的长度就是255了





在整个遍历的过程中,我们不难发现对于字符数组而言每个数组元素只占八个比特位,所以当我们对数组元素从零开始每次减一时会发现-1、-2、......-128 127?没错就是127而不是-129,然后127、126、125、.......0、-1、.......这好像是一个循环啊🙄,画一张图试试?

        char类型取值范围为-128~+127

练习四:

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


结果为死循环,这是因为,无符号字符类型的变量作为全局变量和局部变量时的取值范围是一样的都是0~255。由于没有符号位,故它的取值范围不受负号的限制,可以表示更大的值:

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


练习五:

#include <stdio.h>
int main()
{
    int a[4] = {1,2,3,4};
    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a+1);
    printf("%x,%x",ptr1[-1],*ptr2);
    return 0;
}

x86环境下运行结果:

int* ptr1 =(int*)(&a+1)  //&a获取整个数组地址,+1跳过整个数组地址此时它指向的位置在4地地址的下一个地址处,然后强制类型转换为int*类型,如果不进行强制类型转换&a+1的类型为int(*)[4]是数组指针类型,而ptr1为int*类型,等号两端类型不匹配就会报错

因为强制转换为int*类型,所以单次可操作字节大小为4个字节即:

ptr1 - 1 => (&a + 1) - sizeof ( int )
||
V
ptr[-1] = 4


int* ptr2 = (int*)((int)a+1)  //由于a的原类型为int[4]类型,a+1就相当于将a指向的地址+4,而现在我们将a强制转换为int类型,那么此时的+1.就相当于将a指向的地址+1,在内存中读取时的形式是这样的:

此时ptr2指向0x0012ff41:

在打印*ptr2时,*ptr2属于int*类型,所以要向后读取4个字节即00 00 00 02,但是为什么最后打印的是2000000呢?这是因为02是高地址,在小端字节序中高地址位于原来的高位,低地址位于原本的地位。02是四者中最高的地址故读取时应该位于十六进制数的最高位:




依据小端的规则在内存中进行存储,同时也依据小端的规则内存中读取

直接写成01 00 00 00的形式是因为省略了将1、2、3、4的十六进制数转换为小端形式的过程:0x00000001 ——>  01 00 00 00

该程序要在x86环境下运行,否则会报错:



这是因为:

x64环境下,指针大小为8个字节

x86环境下,指针大小为4个字节

&a表示获取数组a的首元素地址,而位于64位环境中,获取的地址是八字节的,可能是这样的:0x0012ffcd30542320,而现在想要将它强制类型转换为int型也就是四个字节,就会发生截断,得到的结果就是:0x30542320,这时将该地址加一就与我们之前想要操作数组a的全部首元素地址的初衷相悖,加一结果就是0x30542321与原地址无关,这个例子的主要作用就是为了使各位更好的理解小端机器中数据在内存中存储和读取的规则......

3、浮点数在内存中的存储与读取

浮点数的存和取操作主要用于处理浮点数和其他类型数据之间的类型转换问题

IEEE754国际标准规定:任意一个二进制浮点数V都可以表示成下面的形式:

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

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

举例:


V = 5. 0  (十进制)                       V = 5.5  (十进制)


   =101.0  (二进制)                        = 101.1    (二进制)①  


   =1.01 * 2^2                                     =1.011 * 2^2    //与十进制科学计数法类似,这里将10换为2


   =(-1)^0 * 1.01*2^2                          =(-1)^0 * 1.011 * 2^2


即S = 0;M = 1.01;E=2;                即S = 0;M = 1.011;E=2;

①:因为二进制的每一位都有权重,比如11111.11,小数点前面的1就是2^0、2^1、......、2^n

而小数点后面的1每一个代表的就是2^-1、2^-2......、2^-n而相对应的就是0.5、0.25......

IEEE 754标准规定:

对于32位的浮点数,最高位存储符号位S,接着八位存储指数E,剩下的23位存储有效数字M



对于64位的浮点数,最高位存储符号位S,接着十一位存储指数E,剩下的52位存储有效数字M



浮点数存的过程:

IEEE 754标准对有效数字M和指数E,有一些特别的规定:

对有效数字M:

在计算机内部保存M时, 默认这个数的第⼀位总是1 ,因此可以被舍去,只保存后⾯的小数部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样有利于节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字,结果会更精确。

对指数E:

⾸先,E为⼀个⽆符号整数,这意味着,E为32位浮点数时的取值范围为0~255;E为4位浮点数时的取值范围为0~2047。 (E分别占八个和十一个比特位) 但是,科学计数法中的E是可以为负,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个 中间数 ,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。⽐如:2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

我们来分析一段代码来加深理解:

#include <stdio.h>
int main()
{
    float a = 5.5;
    return 0;
}

通过上述的浮点数存的过程我们可以得到这样的解题思路:

float a = 5.5;
//S     E       M
//0 10000001 01100000000000000000000 
//01000000101100000000000000000000
//0100 0000 1011 0000 00000000 00000000
// 4    0     b   0     00        00          //二进制转十六进制表示
//0x40b00000                //最终结果

在内存窗口查看一下:



浮点数取的过程:

E不全为0或不全为1

E= 计算值 + 127 / 1023 、M取小数部分,后面不够的补零

E全为0

E=1-127(或者1-1023)M还原为0.xxxxxx的小数形式

E全为1

如果有效数字M全为0,表⽰±⽆穷⼤(正负取决于符号位s)

 

练习七:浮点数和整数之间的类型转换操作
#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;
}

分块解析:

//以整数的形式存,读取的时候存的时候什么样读的时候就什么样子

int n = 9; 
printf ( "n 的值为: %d\n" ,n);    结果为9


//这里开始时为整数要求打印结果为浮点数,所以无需进行浮点数的存,直接整数转补码然后进行浮点数的取操作

int n = 9 ;

0 00000000 00000000000000000001001    9的补码分解成用于存SME的三块的形式

此时E全为0,则E=1-127,M还原为0.xxxxxx的小数部分

(-1)^0 * 0.00000000000000000001001 * 2^-126  

= 1* (一个近似于0的数)                          

=0.000000......

printf ( "*pFloat 的值为: %f\n" ,*pFloat);
//这里开始时为浮点数要求打印结果为整数,所以先进行浮点数的存操作将浮点数转换为补码的形式然后再打印输出
*pFloat = 9.0 ;
1001.0
(-1)^0 * 1.001*2^3
S = 0;M = 1.001;E = 3
0 1000001 000100000000000000000000
printf ( "num 的值为: %d\n" ,n);  结果为1091567616
//以浮点数的形式存,读取的时候存的时候什么样读的时候就什么样子
*pFloat = 9.0 ;
printf ( "*pFloat 的值为: %f\n" ,*pFloat);  //结果为9.000000


相关文章
|
2月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
586 0
|
2月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
195 1
|
2月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
174 0
|
8月前
|
消息中间件 存储 缓存
kafka 的数据是放在磁盘上还是内存上,为什么速度会快?
Kafka的数据存储机制通过将数据同时写入磁盘和内存,确保高吞吐量与持久性。其日志文件按主题和分区组织,使用预写日志(WAL)保证数据持久性,并借助操作系统的页缓存加速读取。Kafka采用顺序I/O、零拷贝技术和批量处理优化性能,支持分区分段以实现并行处理。示例代码展示了如何使用KafkaProducer发送消息。
|
10月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
312 12
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
797 0
|
11月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
11月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
754 1
|
11月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
424 1