我们知道,计算机的本质工作就是计算。而实际的计算中就避免不了接触和处理大量的数据,那么数据有什么类型呢?数据在内存中的存储又是什么样的呢?计算机是如何对数据进行计算和处理的呢? 下面就让我们来一探究竟,揭开数据在计算机里存储的神秘面纱吧。
1.数据的类型:
数据的类型是十分重要的,因为数据的类型直接决定了计算机存储它们的方式。对于C语言来说,数据的类型大致分为如下几种:
1.整型家族:
这时候你可能就会疑问,为什么char字符型也被归算到整形家族呢? 事实上,字符类型确实也是整形的一种。一个字符会对应一个Ascii码,在计算机的内存中,字符变量被解析成对应的Ascii码进行存储,所以说字符类型(char)也是一种整型。
对于C语言来说,整型家族的成员的符号遵循这一个原则:
浮点数家族:
自定义数据类型:
对于复杂的数据类型,我们暂且不做深究,接下来我们深入研究整型和浮点型家族的内容。
整型家族:
前面我们列举了整型家族的成员,有的同学可能就会好奇,为什么char类型也被分到了整型家族中去?因为字符型常量在计算机种存储的就是char的Ascii码值!所以我们也认为字符型是整型。
这时候有的同学突发奇想,用一个无符号类型的整数a存储-1并打印i的值。
#includeint main() { unsigned int a=-1; printf("%un",a);//打印无符号数用%u return 0; }
可能你会想,打印出来的可能是1。但真的是这样吗?运行结果如下:
天啊,怎么会打出这么大的数字?接下来我们就要剖析-1到底在计算机里是怎么存储的。
1.原码,反码,补码
这里以有符号数-1为例,我们知道-1是int类型,占4个字节,1个字节有8个比特位。所以一个整型数字是由32个01二进制序列组成的。
原码的构成:32个比特位,第一位是符号位,0代表是正数,1代表是负数。规定完符号为后,把这个数的绝对值转化成2进制,用0补齐就是这个数的原码,就拿-1来说。
10000000000000000000000000000001//-1的原码
第一个1就是它的符号位。
那么内存里真的是存储原码的吗?并不是!为什么呢?如果是,不妨考虑下面这个计算。
计算:1-1 我们都知道1-1=0,但其实对于cpu来说,它只有加法器,所以1-1会被cpu解析成
1+(-1),如果是存储原码,我们不妨使用原码进行计算:
00000000 00000000 00000000 00000001--->1的原码
10000000 00000000 00000000 00000001--->-1的原码00
相加得:
10000000 00000000 00000000 00000010--->相加的结果
你会发现,无论符号位是否参与运算,最后的结果显然都不是正确的,说明计算机存的并不是对应数字的原码。事实上,计算机存储的是补码。那么原码是怎么变成补码的呢?
事实上,原码先变成反码后再变成补码,具体规则如下:
对于正整数来说,原码 反码 补码均相同。
而对于负数来说,原码 反码 补码遵循下面这个规则:
反码:原码符号位不变,其余位按位取反。
补码:反码+1。
以-1为例:
我们接着用补码来计算1-1的结果:
用补码计算得到了正确的结果,也就可以说明计算机存储的是补码。
同理我们也可以解释为什么以无符号整数形式打印-1会得到那么大的结果了,我们打印使用的是原码,而对于无符号整型来说,内存中的补码全都被解析成有效位,我们用计算器输入32个1组成的二进制序列转换成10进制整数的结果和运行结果进行对比:
结果一模一样,证实了计算机存储的是补码!
2.截断和整型提升:
我们知道一个整型的大小是4个字节,一个char类型的元素大小是1个字节,如果我要把整型放到字符型会发生什么呢?
以200为例:
如图,因为char 只能存八个比特位,所以只有最后的8个比特位的内容被保留了下来。
2.整型提升:
前面我们知道了将一个整型放到一个字符型只会截取最低的8个比特位,那么下面这段代码会发生什么呢?
#includeint main() { char a=128; printf("%un",a); return 0; }
打印结果如下:
我们来简单分析一下运行结果,首先我们知道128是一个整数,我们写出它的二进制序列:
00000000000000000000000010000000----->128的原码
放到char类型发生截断: 100000000
而我们用%u将a看做无符号整型,但是a不够4个字节,所以要发生整型提升:
整型提升原则:如果原来的数是有符号位,则高位不符号位,如果原数是无符号数,高位补0
在vs环境下,char类型默认是有符号类型,那么最高位就是符号位。
这里的128发生截断后的二进制位是:100000000
根据整型提升的原则,用符号位的数字填充高位得到:
11111111111111111111111110000000--->整型提升之后的a的补码
使用计算器计算结果和运行结果对比:
结果一摸一样,说明如果我们在打印的时候用%d %u来解析字符型的变量,这个字符型的变量会发生整型提升!
接下来看一段神奇的代码:
#include#include int main() { char a[1000]; int i; for(i=0;i<1000;++i) { a[i]=-1-i; } printf("%dn",strlen(a)); return 0; }
运行结果如下:
为什么不会是1000呢?我们来简要分析一下:
我们知道char类型是8个比特位,那么8个比特位可能的2进制序列如下:
00000000--->0
00000001--->1
00000010--->2
.......
011111111--->127
......
10000000--->-128(人为规定,为了和0000000/区分,127+1--->-128)
10000001--->-1(符号位是1)
10000010--->-2
......
111111111--->-127
00000000--->0(+1,最高位被丢弃,结果还是0)//陷入了循环!
我们知道,strlen碰到''才能结束,那么我们就来看看什么时候碰到‘’:
首先程序从-1开始递减到-128--->执行了127次
-12*-1--->127(127+1==-128)
接着从127--->0执行了128次
所以最终长度=127+128=255
我们可以用这样一个图来表示这个过程:
其实对于任何一种数据类型,C语言都规定了每种类型的最大最小值,可以用头文件
整型家族的最大最小值:
浮点型家族最大最小值:
2.大小端字节序:
前面我们知道了数据在计算机内部存储的是补码,那么我们怎么真实观察内存中的数据存储呢?
使用vs的内存窗口就可以观察:下面观察10在内存中的存储:
因为用2进制表示太长,所以内存中使用16进制来存储:4个2进制位就是一个16进制位
10的2进制序列:0000 0000 0000 0000 0000 0000 0000 1010
16进制序列: 0x00 00 00 0a
内存存储: 0a000000
计算机存储的确实是补码,但好像并不是直接存它的补码,而是用一定的方式存进去,这就是
我接下来要介绍的大小端字节序存储:
对于多余1个字节的类型数据,计算机的存储模式分别有两种:大端,小端存储
小端存储:把低字节的内容放在低地址的空间,把高字节的内容放在高地址的存储方式。
大端存储:把高字节的内容放在低地址的空间,把低字节的内容放在高地址的存储方式:
接下来我们在以-10为例来看-10在内存究竟是大端还是小端存储方式
10000000 00000000 00000000 00001010--->-10的原码
11111111 11111111 11111111 11110101--->-10的反码
11111111 11111111 11111111 11110110--->-10的补码
0xff ff ff f6 ---->补码的16进制表示法
如果是大端:存储的结果应为: 0xfffffff6
如果是小端:存储结果应为:0xf6ffffff
打开窗口调试结果如下:
和小端存储模式预期结果相同,说明这台计算机的存储模式是小端模式:
那么怎么通过程序来验证呢?
假设我们放入一个1,如果是小端存储,那么小端字节的第一个内容就是1,假设是大端存储则是0
所以我们只需要访问第一个字节的内容,而我们知道指针的类型决定了它解引用能够访问的字节内容,我们只要一个字节,所以我们需要一个字符指针。
代码如下:
int check_sys()//小端模式返回1,大端返回0 { int i = 1; char* pi = (char*)&i;//使用字符型指针访问1,解引用只能访问1个字节 if (1 == *pi) { return 1; } return 0; } int main() { if (check_sys() == 1) { printf("小端n"); } else { printf("大端n"); } return 0; }
运行结果如下:
注意!,大小端字节序对char类型的变量没有任何意义!因为char类型只有1个字节,就没有什么字节序可研究!
三 浮点数在内存中的存储:
有如下的一段代码:
#includeint main() { int n=9; float* pFloat=(float*)&n; printf("n的值是%dn",n); //预期结果 9 printf("*pFloat的值是%fn",*pFloat);//预期结果9.0 *pFloat=9.0; printf("nums的值为:%dn",num);//预期结果9 printf("*pFloat的值是%fn",*pFloat);//预期结果9.0; return 0; }
运行结果和我们预期一样吗吗? 运行结果如下:
你会发现 我们只对了一半,但为什么会出现0和1091567616呢?这就和浮点数在内存中的存储方式有很大关系了。
事实上,国际标准协会IEEE(电子和电气工程协会)754对浮点数做出了如下的规定:
对于任何一个二进制浮点数v,都可以表示成下面这个形式:
v=(-1)^s*M*2^E
s:代表的是符号位,0代表正数,1代表负数
M:代表的是有效数字,范围是1.0<=M<2
2^E:代表指数位。
光听定义可能有点抽象,下面我们用十进制的0.5为例来深入理解定义:
0.5是正数---->s=0
0.5=1.0/2->1.0*2^(-1);
所以10进制的0.5表示成2进制的浮点数形式就是 (-1)^0*1.0*2^(-1)。
那么浮点数同样在计算几种也被用二进制存储,下面我们来讲一下32位平台和64位平台浮点数存储的方式。
32位平台:
64位平台 :
注意,因为浮点数的有效数字基本都是介于1-2之间的,则在存储的时候,有效数字中小数点前的1不会被存进内存中,这样数据就能够多存储一位数字。
这里的指数位有点特殊,并不是直接将指数位对应的真实数据存储进入内存,而是加上一个数字在存入内存,对于32位平台,这个数字是127,而对于64位平台则是1023。以32位平台为例:
假设一个数的指数是3
3+127=130,则在内存中存储的就是以130进行存储:
10000010--->对应的就是指数3
这就有可能出现两种极端的二进制序列:
00000000
11111111
假设说一个浮点数对应在内存中的二进制序列是全0,那么真实的指数就是-127
这是一个无限接近于0的数字,所以对于这样的浮点数,不在以1作为开头而是直接把它
表示成以0的浮点数,这样做是为了表示它很小。
同样,如果八位二进制全1,那么真实对应的指数是128,是一个非常大的数字,
这里我们就不在深究这个很大的数字。
接下来我们就应用定义和规律来分析前面代码的结果
第一步:写出9的2进制序列->1001
第二步:转换成2进制浮点数--->1.001*2^3
第三步:结合定义,得出s=0,E=3,M=1.001
最后写出内存中存储的真实情况:
符号位:0(正数)
指数位:3+127=130--->10000010
有效数字:001(第一位不存)
写出序列
01000001000100000000000000000000--->浮点数9.0在内存中的存储。
同样我们写出整数9在内存中的存储
00000000000000000000000000001001--->整数9在内存中的存储。
接着我们跟着分析代码:
printf("n的值是%dn",n);
//用%d告诉计算机我解析的是一个整数,所以结果正确。
printf("*pFloat的值是%fn",*pFloat);
//%f告诉计算机我解析的是一个浮点数
00000000000000000000000000001001-->浮点数的方式解析
解析结果:指数位全0,真实指数是-127,以0开头-->0.1001*2^(-127)
但是因为%f默认打印是小数点后6位,而有效位在很后面,所以打印了0.000000
*pFloat=9.0;
printf("nums的值为:%dn",num);通过指针改变了num成浮点数9.0,num的内容如下:
01000001000100000000000000000000--->浮点数9.0在内存中的存储。
%d说明我解析的是一个整数
01000001000100000000000000000000--->反码,由于是正数也是原码。
计算器运算结果如下:
运行结果对比:
结果相同,证实了浮点数在内存中确实是和我们分析的相同,也就是说即使二进制序列相同,以不同的方式解析数据,最后得到的结果也是不相同的。
四.在循环中慎用无符号类型!
代码如下:
#include#include //使用Sleep函数 int main() { unsigned int i=9; while(i>=0) { printf("%dn",i); --i; Sleep(1000);//睡眠函数,有利于我们观察结果 } return 0; }
因为对于无符号数来说补码的全部二进制位都会被认为是有效位,也就是不可能会出现小于0的情况,这时候程序就陷入了死循环中!!
5.布尔类型:
在C99标准中引入了新的类型----bool,bool类型的变量只有两种值,true or false,表示条件的成立与否,在C++和java中使用较多,对于C语言,要想使用布尔类型,需要包含头文件
我们知道,C语言用0表示假,用非0表示真,那么布尔类型的true和false的本质是什么的?
我们可以转到
可以看到,true和false的本质也是1和0。所以在C语言中,不必特意去使用布尔类型。
最后插入一张有意思的图片:
任何一个小白都会经历这样的过程称为大牛!希望大家在挺过绝望之谷,共同走向成功!!!
总结:
我们要了解整型家族的成员,浮点型家族。了解整型提升的原理,整型在内存中的存储,浮点型在内存中的存储,制作不易,有错误还望指正。希望大家共勉和努力,共同进步!!!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)