数据类型的介绍
在我们之前的学习当中我们已经介绍了基本的内置类型
char 字符数据类型
short 短整型
int 整形
long 长整型
long long 更长的整形
float 单精度浮点数
double 双精度浮点数
这些类型的意义是:
1.使用这个类型开辟内存空间的大小,大小决定了使用范围
2.如何看待内存空间的视角。
类型的基本归类
整形
整形中分为有符号整形和无符号整形,因为我们生后中有些数值需要有正数和负数之分,例如温度我们就可以使用有符号整形,但是有些不需要负数的数值例如身高,我们就可以使用无符号整形。
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
浮点型
float
double
构造类型
数组类型
结构体类型struct
枚举类型enum
联合类型union
指针类型
int pi;
char pc;
float pf;
void pv;
空类型
void表示空类型(无类型)
通常应用于函数的返回类型,函数的参数、指针类型
整形在内存中的存储
我们都知道,创建一个变量需要在内存中开辟空间,那变量究竟是怎么在内存当中存储的呢。我们又要提到原码、反码、补码的概念了。
在计算机中整数有三种表示方式即原码、反码、补码,他们都有符号位和数值位,符号位用0表示正数,用1表示负数。
正整数的原码、反码、补码相同,都是直接将属猪按照正负数的形式翻译成二进制形式就可以得到原码。
负整数的原码、反码、补码
原码
直接将数值按照正负数的形式翻译成二进制形式就可以得到原码
反码
将原码的符号位不变,其他位一次按位取反就可以得到反码
补码
反码+1就是补码
对于一个整形,数据在内存当中存放的其实是他的补码。那么为什么是补码而是不是原码呢,原因是使用补码,可以将符号位和数值域统一处理,同时加法和减法也可以统一处理,我们的cpu只有加法器,此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
例如:
1 - 1
因为cpu中只有加法器,所以我们需要将减法转化为加法,1+ (-1)如果内存中存储的是原码,那么他们相加的结果为
00000000 00000000 00000000 00000001 1的原码
10000000 00000000 00000000 00000001 -1的原码
10000000 00000000 00000000 00000010 相加为-2
我们运算的答案是错误的,但是当我们存储的是补码时:
00000000 00000000 00000000 00000001 1的补码
11111111 11111111 11111111 11111111 -1的补码
00000000 00000000 00000000 00000000 相加0
使用补码运算时,得到正确的答案
int main()
{
int a = 20;
int b = -10;
return 0;
}
如上图编译器为了方便显示的是16进制数,其实对于一个整形,数据在内存当中存放的还是他的补码,我们可以是尝试还原一下。
例如**-10**
原码:10000000 00000000 00000000 00001010
反码:11111111 11111111 11111111 11110101
补码:11111111 11111111 11111111 11110110
将他的补码转换成16进制就变成了ff ff ff f6,跟我们通过调试看到的内容一样
但是我们通过对比发现虽然存储的是补码,但是存储的顺序好像不一样,这是怎么回事呢?
大小端介绍
当我们的数据大于一个字节时,数据的存储就有了顺序问题。例如:一个十六进制数0x11223344,我们说11为数据的高位,44为数据的低位。
大端存储模式:是指数据的低位保存在高地址中,而数据的高位,保存在内存的低地址处。
小端存储模式:是指数据的低位保存在低地址中,而数据的高位,保存在内存的高地址处。
int main()
{
int a = 0x11223344;
return 0;
}
我们通过调试得出,在vs2019中为小端存储模式,因为数据的低位44存储在地址处,高位11存储在高地址处。
一道笔试题
我们通过一道笔试题来巩固一下我们刚才所总结的知识。题目:请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。
小端字节序存储
把数据的低位存储在内存的低地址处,高位字节的内容,存储在内存的高地址处
大端字节序存储
把数据的低字节的内容,存放到内存的高地址处,高字节内容,存放在低地址处
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
if (check_sys() == 1)
{
printf("小端存储\n");
}
else
{
printf("大端存储\n");
}
return 0;
}
浮点数在内存中的存储
首先我们通过一段代码来观察浮点数在内存中的存储
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;
}
以我们的认识,使用%d
打印应该打印9使用%f
打印应该打印9.0,但事实是这样的嘛
我们将代码跑起来发现,并非我们所认识的,那这说明了什么,说明了浮点型与整形在内存当中的存储规则是不一样的,那我们先了解一下浮点数究竟怎么储存的。
浮点数存储规则
根据国际标准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.0122
所以s = 0,m = 1.01, e = 2。
IEEE 754规定:
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
IEEE 754对有效数字M和指数E,还有一些特别规定:
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
至于指数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。
既然我们可以将浮点数存储,我们就可以将他们取出来,但是指数E从内存中取出分为三种情况:
E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将
有效数字M前加上第一位的1。
E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
剖析题目
这些就是浮点数在内存当中的存储规则,了解了这些后让我们回到最开始的那道题,我们进行分析。
首先我们创建了一个整形变量,那么他在内存中存储的是什么呢?是他的补码00000000000000000000000000001001,当我们将他看作一个浮点数打印时,我们可以得到S = 0,M = 000 0000 0000 0000 0000 1001,E = 00000000。所以经过计算(-1)^ 0 × 0.00000000000000000001001×2 ^ (-126) = 1.001×2 ^ (-146), 所以根据规则他是一个非常接近0的数,使用浮点数打印就是0.000000
笔试题的第二部分,我们将一个浮点型使用整形打印,同样的道理,我们先将9.0用浮点数的方式存进内存当中,9.0写成二进制为1001.0,可以写成(-1)^0 * 1.001*2^3,我们可以推出二进制形式:0 10000010 001 0000 0000 0000 0000 0000,所以将他作为一个整形打印时就会是一个非常大的数为1091567616