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