正片开始👀
数据类型 👏
1.基本内置类型:byte,int ,char, float, double
2.构造数据类型:
数组类型;
结构体类型:struct
共用体(联合类型):union
枚举类型:enum
3.指针类型 :int* p,char* p,float* p,void* p
4.空类型 : void(无类型),通常用于函数的返回类型,函数参数与指针类型。
构造类型又叫自定义类型,在各自参数或者元素类型发生变化就会让他彻头彻尾的改变;而基本数据类型的特点就是不可以再分解为其他类型,基本类型就是自我说明,关于他们的作用就不一一赘述了。
内存窗口👏
那首先要在调试栏打开内存窗口,并搞清楚怎么观察内存,这是必要的工具
1.地址栏
2.内容
这些密密麻麻的就是内存中的数据,看到这里你可能就会疑惑,不是说内存里存的都是二进制数吗,这些是什么鬼?是的,没有错,但是内存窗口展示内容有限,在有限的范围内,他只能选择以 16 进制的形式展示出来,仅仅是展示而已。
3.文本
这个更是人不人鬼不鬼的其实是他根据内存的数据简单的以文本的格式输出其可能的内容,无价值简直就是意义不明。
整型的存储👏
不论我们在写代码时创建了个什么东西,他不会居于虚空,存在载体就会占用内存,而空间的大小是根据我们创建的数据的类型而决定的,我们要回到问题最本质的源头,在开辟的内存中到底如何去存储数据?我们不废话直接创建俩个变量看看便知
int main() { int a = 5; int b = -5; return 0; }
内存窗口打开我们可以取地址查找 a,b 的数据存储情况:
这里是不是感觉很奇怪,二者为何差异这么大?要搞清楚我们就要继续深入研究。
原码,反码,补码👏
说整数的二进制有三种表示方法:原码,反码,补码。
整数分为正数和负数,正负数的区别就在于他们二进制32位数的最高位的 0和1代表着符号位,0为正,1为负,其余才是有效位。
正数的原反补三码合一,和他本身是一样的。但是负数就花哨了,负数原码是按照一个数的正,负直接写出来的二进制就是原码。反码在原码基础上,除开符号位进行取反得到。这里强调一下,之前讲过一个操作符:~(按位取反操作符),区别一下他俩,按位取反操作符是针对二进制数每一位全部都取反,包括符号位。补码则是反码的基础上+1得到,比如 -7 这个数的原反补分别为:
10000000 00000000 00000000 00000111 (原)
111111111 111111111 111111111 111111000(反)
111111111 111111111 111111111 111111001(补)
b 的 -5 就是 00000000 00000000 00000000 00000101以补码 11111111 11111111 11111111 11111011 每四个字节为一位化成16 进制就是 0xfffffff3。
补码的意义👏
既然内存中中存储的是二进制的补码,我们现在不谈现象谈本质,为什么偏偏要是补码呢?
我们要明白一件事就是计算机算减法是相对不容易的,因为CPU里面没有减法器,只有加法器,要算 1-1 时只能算作 1+(-1)。计算机用二进制去计算时,我们会发现,当用原码或者反码去计算根本行不通,只有补码才可以实现。
由此看来,补码的地位是绝对的老大哥,在计算机系统中,数值一律用补码来存储,主要原因是:
1.统一了零的编码
2.将符号位和其它位统一处理
3.将减法运算转变为加法运算
4.两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃
由这里看,加法和减法可以统一起来处理,此外补码和原码相互转换时,其运算过程是相同的,不需要额外的硬件电路。
大小端模式
我昨天的博客专门讲了大小端存储模式专题,其实大小端的检验也可以用今天的知识来解决:
# include<stdio.h> int check_s() { int i = 1; return (*(char*)&i); } int main() { int ret = 0; ret = check_s(); if (ret == 1) { printf("小端\n"); } else { printf("大端\n"); } return 0;
其结果:
不同数据类型存储👏
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; }
上面这个代码乍一看就是三个-1,仔细看就会发现char类型后面阴差阳错跟个 -1,-1是整数啊,这时是不是感觉有一丝凌乱?-1的原码为1000(30个0)1,取反+1得到补码111……111(32个1),而 char 只能放 8 个比特位,就意味着会发生截断,,a = 11111111,而我们在vs环境里面 signed char和char是一样的,我们不妨看看运行结果如何:
为什么c会是 255呢?首先要明白为什么b 会是 -1,我们 signed char b在截断后,如果要 以%d形式进行打印,就会发生整型提升,有符号数的最高位会被认为是符号位,而整型提升时会以整型的原符号位进行提升,因为是负数就会全部补成1,再按照补码转原码倒回去会发现结果就是 char a= -1。
同理,c 是无符号数,但也会发生整型提升,无符号数提升通通补 0 ,按照%d 打印时,内存就会认为这是一个有符号数,最高位又会被默认为符号位,最后转换成原码,就会得到 255。
接着上面的思路再来看看这个代码:
# include<stdio.h> # include<windows.h> int main() { unsigned int a ; for(a=9;i>=0;i--) { printf("%u ",i); Sleep(1000); return 0; } }
这个代码也是一个眼见不为实的代码,表面上是打印9个数,实则实在无限的死循环,你发现了吗?聪明的你如果看到 判断条件 i>=0 这个条件,就可能发现端倪,十有八九会死循环,因为不管什么情况都会恒成立。我们来看看运行效果:
当我0进入后很自然的变成了-1,而-1会放到无符号整型,补码是32位全1,而作为无符号整型,最高位不再是符号位,所有位都是有效位,转化为原码是一个巨大的数字,由于满足循环判断条件于是开始欢乐死循环。我们可以根据这些实例总结出相应的经验。
浮点数存储机制👏
浮点数相对于整型,他的存储机制会更复杂。
浮点数包括float和double,甚至long double,浮点数的表示范围在 float.h中定义。
int main() { int n = 9; float* pFloat = (float*)&n; printf("n=%d",n); printf("*pFloat=%f\n",*pFloat); *pFloat = 9.0; printf("n=%d\n",n); printf("*pFloat=%f\n",*pFloat); return 0; }
对上面这个代码,我一开始的觉得打印的是:9,9, 9.0, 9.0,后来发现是我格局小了。
如上结果我们就能明显看出浮点数和整型存储是不一样的。
国际标准IEEE(电气和电子工程协会)754标准,任意一个二进制浮点数V都可以表示成下面的形式:
V=(-1)^S * M * 2^E
1.(-1)^s表示符号位,s为0表示正,s为1表示负
2.M表示有效数字,大于等于1,小于2
3.2^E表示指数位
对于32位浮点数,最高符号位s,接着 8位是指数E,剩下23位为有效数字M;而64位浮点数,11位是指数 E,剩下的 52 位有效数字M。
这里的M和E还有特殊的规定
IEEE 754规定,在计算机内部保存M时,默认这个数的第1位总是1,因此可以被舍去,只保存后面的小数部分。例如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
我们会把指数位看作是无符号整数,取值在0到255,如果是11位的E,取值在0到2047,但是众所周知,科学记数法里面E是可以出现负数的,所以还有规定就是E在存入时会先修饰一波,给真实值加上一个中间值,对于 8位与11位的E,这个中间数是127和1023,比如 2 的十次方,E就是10,保存成32位浮点数时,必须变成10+127=137,即10001001。E在全0和全1的情况下代表着接近于0的无穷小和无穷大。
我们回到最开始的问题,9的原反补三码合一00000000 00000000 00000000 00001001,E为全0时其真实值是-126,除去S与E,小数部分M为0000……01001,而%f只能打印后6位,这就是为什么打印0.000000。接着pFloat为9.0浮点数进入,对应的二进制为1001.0,科学记数法1.0012^3,S=0,E=3,M=1.001,存入后变成 0 10000010 0010000000000000000000,(S E M),为了方便我们换成16进制计算出是 0x41100000,我以%f取出就是他自己这没什么解释的,困惑的可能是%d的结果,以%d打印就默认为有符号数,变成补码转换出来就是 1091567616。