我们在之前提到过,整型是如何在内存中存储的,现在我们回忆一下
整型在内存中的存储
整数的二进制的表示方法有三种,原码,反码和补码
三种表示方法均有符号位和数值位这两部分,符号位用0来表示它是正数,用1表示它是负数,数值位中最高位的一位就是符号位,剩下的就是数值位了
正数的原码反码补码都相同。
负整数的三种表示方法则各不相同。
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
//这里我们拿-1来举例
//-1的原码就是
//10000000 00000000 00000000 00000001 原码
反码:将原码的符号位不变,其他的依次按位取反就可以得到反码。
//这里我们拿-1来举例
//-1的反码就是
//11111111 11111111 11111111 11111110 反码
补码:反码+1就可以得到补码。
//这里我们拿-1来举例
//-1的补码就是
//11111111 11111111 11111111 11111111 补码
对于整型来说:数据存放在内存中其实存放的是补码
为什么呢?
在计算机系统中,数值一律用补码表示和存储。原始在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
举例:
整型的取值范围:
在这里我只简单介绍几个
int | 整型4个字节(32个比特位) | 取值范围:-2147483648 ~ 2147483647(这里最高位表示符号位所以只有31个数值位可以用) |
unsigned int | 无符号整型4个字节(32个比特位) | 取值范围: 0~4294967295(这里没有符号位了,都是数值位) |
char | 字符类型1个字节(8个比特位) | 取值范围:-128 ~ 127 |
unsigned char | 无符号字符类型1个字节(8个比特位) | 取值范围:0 ~ 255 |
short | 短整型2个字节(16个比特位) | 取值范围:-32768 ~ 32767 |
unsigned short | 无符号短整型2个字节(16个比特位) | 取值范围:0 ~ 65535 |
整型提升:
c语言中整型算术运算总是至少以缺省整型类型的精度来进行的,为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。
整型提升的规则:
1.有符号的整型提升高位补符号位
2.无符号的整型提升高位补0
举例:
1.正数的整型提升
char a = 1;
//它的补码 00000001
//有符号整数,符号位是0,高位补0
//提升为:00000000 00000000 00000000 00000001
2.负数的整型提升
char a = -1;
//它的补码 11111111
//有符号整数,符号位是1,高位补1
//提升为:11111111 11111111 11111111 11111111
3.无符号的整数提升
unsigned char a = -1;
//它的补码 11111111
//无符号整数,高位补0
//提升为:00000000 00000000 00000000 11111111
整型截断:
整型截断是将所占字节大的元素赋给所占字节小的元素时会出现数值的舍去现象。
简单来说就是将长字节里面的内容截取一部分赋给短字节
int main()
{
int a = 257;
//这里a是正整数原反补相同
//补码:00000000 00000000 00000001 00000001
char c = a;
//这里我们把a赋值给c,但是c是char类型1个字节大小,只能接收8个比特位,所以c把a的后八位截取了(00000001给截取了)
// 0000 0001 高位是0,说明是正数,原反补相同
printf("%d ", c);
//所以最后c的值为1
return 0;
}
算数转换:
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的类型转换为另一个操作数的类型,否则操作就无法进行。
long doubule
double
float
unsigned long int
long int
unsigned int
int
如果某个数的类型在上面的列表中排名靠后,那么首先要转换为另一个操作数的类型后执行运算。
举例:
int main()
{
int a = 1;
float b = 3.0f;
//算数转换,这里需要把a转换成与b类型相同的float类型之后再进行计算了
float c = a + b;
printf("%f",c);
}
在我们了解了上面的知识后,有几个题目分享一下
题目一
#include <stdio.h>
int main()
{
char a = -1;
//-1的原码是 10000000 00000000 00000000 00000001
//-1的反码是 11111111 11111111 11111111 11111110
//-1的补码是 11111111 11111111 11111111 11111111
signed char b = -1;
unsigned char c = -1;
//这里的a,b,c都是char类型类型,只能存8个比特位,所以截取了-1补码的最后8个比特位的内容 11111111
printf("a=%d,b=%d,c=%d", a, b, c);
//这里用%d来打印a,b,c的时候会发生整型提升
//a和b是有符号的,所以高位补符号位 11111111 11111111 11111111 11111111 --> -1
//c是无符号的, 高位补0: 00000000 00000000 00000000 11111111 --> 255
return 0;
}
题目二
#include <stdio.h>
int main()
{
char a = -128;
//这里 -128
//原码 10000000 00000000 00000000 10000000
//反码 11111111 11111111 11111111 01111111
//补码 11111111 11111111 11111111 10000000
//char a里面存的是 10000000
printf("%u\n", a);
//用%u打印,a需要整型提升 他是有符号的,高位补符号位
//11111111 11111111 11111111 10000000 -->4294967168
//%u认为它是个无符号的数,打印出来就是4294967168
return 0;
}
题目三
#include <stdio.h>
int main()
{
char a = 128;
//128
//原码 00000000 00000000 00000000 10000000
//反码 01111111 11111111 11111111 01111111
//补码 01111111 11111111 11111111 10000000
//char a里面存的就是 1000000
printf("%u\n", a);
//用%u打印,a需要整型提升 他是有符号的,高位补符号位
//11111111 11111111 11111111 10000000 -->4294967168
//%u认为它是个无符号的数,打印出来就是4294967168
return 0;
}
题目四
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
// -1 -2 -3 -4... -127 -128 -129...
//但是char类型范围是127~128,存不下-129
// -129的二进制原码: 10000000 00000000 00000000 10000001
// 反码:11111111 11111111 11111111 01111110
// 补码:11111111 11111111 11111111 01111111
// 截断之后: 01111111 --> 127
//所以上面字符数组存的就是 -1 -2 -3 -4... -127 -128 127 126... 4 3 2 1 0 -1 -2...
//长度就为127+128 =255
}
printf("%d", strlen(a));//求字符串的长度,统计\0(ASIIC码值为0)之前的字符个数
return 0;
}
大小端字节序:
我们在了解了整数在内存中存储的时候。
我们会发现,有时候写代码的时候,它有时候是倒着存储的
举个例子
在我们调试的过程中,在内存里面我们会看到它怎么是倒着存储的呢?
这里就有个大小端字节序。
什么是大小端字节序呢?
其实超过一个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小段字节序存储。
大端存储模式:
是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。
这样的就是大端存储
小端存储模式:
是指把数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。
这样的就是小端存储
我们使用的vs2022这个编译器就是小端存储模式。
那我们如何判断自己使用的机器是大端存储还是小端存储呢?
1.直接进行调试看
比如说这样。
2.我们可以通过代码来进行判断
思路:我们知道大端字节序是把大的字节内容保存在前面,小端字节序是把小的字节内容保存在前面,那我们只需要取出它的第一个字节,我们拿1来做例子,如果拿出来的是01,那么它就是大端字节序,如果拿出来的是00。那么它就是小端字节序
int check_sys()
{
int a = 1;
return *(char*)&a;//
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端字节序\n");
}
else
{
printf("大端字节序\n");
}
return 0;
}
关于大小端的一个题目:
//在x86环境下,代码结果
#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;
}
解析:&a,取出的是整个数组的地址,+1得到跳过整个数组的地址,赋值给ptr1,prt1[-1]相当于*(ptr1-1),ptr1-1那么就走到4整个地址上,解引用得到4.
(int )a+1,这里强制类型转换,把首元素地址转换为int类型,那么整型+1,就是+1,
假设a的地址是0x12ff40,+1之后就是0x12ff41了。
我们知道了整型在内存中是如何存储的,那么浮点型在内存中的存储也是这样码?
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n); //这里打印9?
printf("*pFloat的值为:%f\n", *pFloat);//这里打印9.000000?
*pFloat = 9.0;
printf("num的值为:%d\n", n);//这里打印9?
printf("*pFloat的值为:%f\n", *pFloat);//这里打印9.000000?
return 0;
}
这时,通过我们观察的结果,得出了一个结论,浮点型在内存中的存储与整型是不一样的。
浮点数在内存中的存储
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式
V = (-1)^s * M * 2^E
·(-1)^s表示符号位,当S = 0时,V为正数,当S = 1时,V为负数
· M表示有效数字,M是大于等于1,小于2的
· 2^E表示指数位
举例:
十进制的5.5,写成二进制就是101.1,相当于1.011*2^2。
我们按照上面的公式,就可以得出S = 0,M = 1.011,E = 2;
IEEE754规定
对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
简单举个例子:
如果小数点后面的位太多了,就可能导致浮点数在内存中无法精确的保存
例如:
浮点数取得过程
1.对于M的规定
1 ≤ M< 2,即M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。IEEE754规定,在计算机内部保存M的时候,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
2.对于E的规定
E是一个无符号整数,如果E为8位,它的取值范围为 0 ~ 255;如果E为11位,它的取值范围为 0 ~ 2047.但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE754规定,存入内存时的E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127 = 137,即10001001
指数E从内存中取出来还可以分成三种情况:
(1)E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1.
例如:
一个浮点数存储的方式如下
0 01111110 00000000000000000000000
1.我们先将01111110转换为十进制 126
2.再将126 - 127 = -1,指数位为-1
3.有效数字部分为0,加上前面舍去的1,就是1.0
4.符号位是 0,说明它是正数
5.它表示的浮点数是1.0*2^-1 = 0.5
(2)E全为0
这时候指数为 0 - 127(或者0 - 1023),得到的肯定一个非常小的数字,所以特别规定
浮点数的指数E等于 1 - 127(或者1 - 1023),即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数,这样做是为了表示±0,以及接近于0的很小的数字
(3)E全为1
这时指数为255 - 127 = 128或者 2047 - 1023 = 1024,与前面相反,这就是一个非常大的数字,所以也特别规定
这时,如果有效数字M全为0,表示±无穷大(正负号取决于符号位S)
最后我们来回归一下我们学习浮点数在内存中存储的时候的那个题目
#include <stdio.h>
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n); //这里打印9?
printf("*pFloat的值为:%f\n", *pFloat);//这里打印9.000000?
*pFloat = 9.0;
printf("num的值为:%d\n", n);//这里打印9?
printf("*pFloat的值为:%f\n", *pFloat);//这里打印9.000000?
return 0;
}
解析:
1.以整型形式打印整型,结果为9
2.将正数形式的9转换为浮点数形式
9的补码为 00000000 00000000 00000000 00001001
按照浮点数存储:0 00000000 00000000000000000001001
表示为:(-1)^0 * 0.00000000000000000001001*2^(-126) 一个非常小的数
我们知道%f只打印小数点后6位,所以输出结果为0.000000,
3.n被修改为9.0,以整数的形式打印,按照整型的存储方式
浮点数:9.0表示成二进制位1001.0,即1.001*2^3
表示为:0 10000010 00100000000000000000000
直接把0 10000010 00100000000000000000000当成整型打印,结果就为1091567616
4.以浮点数的形式打印浮点数,结果为9.000000