本次数据在内存中的储存会分为两部分:一是整型在内存中的储存;二是浮点型在内存中的储存。
一、整型在内存中的储存。首先,我们要知道原码、反码、补码,这三个在前面有做过详细的整理,这里就不过多讲述。接下来我们就来了解大小端(大端字节序&小端字节序)。
那么,什么是大小端呢?
大端字节序的存储方式:低位存在高地址,高位存在低地址。
小端字节序的存储方式:低位存在低地址,高位存在高地址。
(其中,大小端的最小单位为字节!)
接下来,会讲述如何用一个程序来判断所用的机器是大端还是小端。
#includeint check_sys() { int a = 1; char* p = (char*)&a; if (*p == 1) { return 1; } else { return 0; } } int main() { int ret = check_sys(); //返回1是小端,返回0是大端 if (ret == 1) { printf("小端n"); } else { printf("大端n"); } return 0; }
这个代码的主要思路是:check_sys函数中通过强制类型转换,提取出一个字节大小中所存的数据是否和内存中首个字节数据相等,若相等,则为小端字节序;若不等,则为大端字节序。
因为我们所取的值1是一个很特殊的值,所以,我们还可以对代码进一步做出简化,具体代码见下:
#includeint check_sys() { int a = 1; return *(char*)&a; } int main() { int ret = check_sys(); //返回1是小端,返回0是大端 if (ret == 1) { printf("小端n"); } else { printf("大端n"); } return 0; }
在了解完原码、反码、补码和大小端的内容后,我们就可以来对有符号和无符号的整型进行研究,具体会通过以下几个题目进行解释:
题目一:
//以下代码会输出什么? #includeint main() { char a = -1; signed char b = -1; unsigned char c = -1; printf("a=%d, b=%d, c=%d", a, b, c); return 0; }
对于a变量来说,这里-1是int类型,转成char类型会发生截断,因为内存中存储的是二进制的补码,所以,发生截断也是对内存中二进制的补码进行截断的,只会保留最低的8个比特位数据,又因为其最后是以%d的形式打印出来的,所以保留的这些数据还会发生整型提升,对于有符号的整型提升,会在二进制的最高位补符号位,最后再通过补码转换为原码,就会发现,最后打印出来a的值为-1。
对于b变量来说,虽然C语言中并没有规定char与signed char是相同的,但是在很多主流的编译器中,都会默认char就是signed char,就比如我经常使用的VS2019等,所以,最后b打印出来的结果也会和a一样,为1。
对于c变量来说,前面与有符号char一样,但是到了整型提升部分,对于无符号的整型提升,会在二进制的最高位补0,最后打印出来c的值就为255。
题目二:
//以下代码会输出什么? #includeint main() { char a = -128; printf("%un", a); return 0; }
首先,-128以补码存入内存中时,在char类型中会发生截断,因为char存的是有符号类型,所以在打印时会发生整型提升,会在二进制最高位补符号位,然后又因为是用%u打印的,所以在内存中会自动识别为无符号数,所以原码就会等于其补码,最后打印出来的数会非常大。
题目三:
//以下代码会输出什么? #includeint main() { char a = 128; printf("%un", a); return 0; }
这道题和题目二类似,只是把-128改为128,然后会发现,128在截断后放入的二进制序列与-128截断后的二进制序列相等,所以经过同样的计算过后,会打印出和题目二一样的一个非常大的数据。
题目四:
//以下代码会输出什么? #includeint main() { int i = -20; unsigned int j = 10; printf("%dn", i + j); return 0; }
这是一道比较简单的题,用来区别前面几道题,直接用两数储存在内存中的二进制数据进行计算,得出的二进制再求原码打印出来,就会看到结果是-10。
题目五:
//以下代码会输出什么? #includeint main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%un", i); } return 0; }
因为i是无符号整型,且打印的时候是以%u的形式打印,所以当i减为0的时候,在下一次循环的开始时,i为-1,其二进制的补码32个比特位会变成全1,在无符号的情况下,原码等于补码,这时候的i会变成一个非常大的值,然后这个非常大的值会继续减到为0,再变为非常大,这样无限循环下去。
题目六:
//以下代码会输出什么? #includeint main() { char a[1000]; int i; for (i = 0; i < 1000; i++) { a[i] = -1 - i; } printf("%d", strlen(a)); return 0; }
这是一道比较重要的题,因为char最多为8个比特位,当char为无符号时,其取值范围是从0到255;而在有符号char中,取值范围是从-128到127,但是,无论是有符号还是无符号,他们的长度(strlen)都是一样的,为255。所以,最后打印出来的结果为255。
推论:不同的整型类型都有一定的取值范围,且对于有无符号也有不同的取值范围。具体可以通过头文件limits.h转到文档进行查看。
题目七:
//以下代码会输出什么? #includeunsigned char i = 0; int main() { for (i = 0; i <= 255; i++) { printf("hello worldn"); } return 0; }
这道题和上一道题类似,因为无符号的char的取值范围是从0到255,所以i的值是不可能大于255的,for循环中的条件是永远都成立,所以该程序打印时会无限循环。
二、浮点型在内存中的储存。这一部分会通过一个例子引出。
例子:
#includeint main() { int n = 9; float* pFloat = (float*)&n; printf("n的值为:%dn", n); //9 printf("*pFloat的值为:%fn", *pFloat); //0.000000 *pFloat = 9.0; printf("n的值为:%dn", n); //1091567616 printf("*pFloat的值为:%fn", *pFloat); //9.000000 return 0; }
在看这段代码之前,我们需要知道浮点数和整数在内存中的存储规则是不一样的,不能用整数的那一套方法直接套用,那么,浮点数的存储规则是什么呢?
对任意二进制浮点数V都可以表示成下面的形式:(-1)^S*M*2^E
1.(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
2.M表示有效数字,大于等于1
3.2^E表示指数位
例如:5.5 转换为二进制为 101.1(也即是-1^0*1.011*2^2)
通过上面描述,可以推断出,二进制浮点数在内存中只需要存入S、M、E三个值就好,那么这三个数在内存中是如何存储的呢?
对于单精度浮点数float(也就是32位浮点数)储存来说,最高一位是符号位S,接着8位是指数E,剩下的23位为有效数字M。
对于双精度浮点数double(也就是64位浮点数)储存来说,最高一位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
M的存入:因为有效数字M值的范围一直都是大于等于1而小于2的,所以储存的时候会将正数部分的1舍去,只将小数部分存到内存中。
E的存入:E为一个无符号数,若是负数,存入内存时E的真实值必须加上一个中间数,对于8位的E,这个中间数为127;对于11位的E,这个中间数为1023。
知道了浮点数是如何存入的,接下来,就要了解其是如何取出的。
从内存中取出的三种情况:
1.E不全为0或不全为1:指数E的计算值减去127(或1023),得到真实值;再将有效数字M前加上第一位1即可。
2.E全为0:指数E等于1-127(或1-1023)即为真实值;有效数字M直接就为0.XXX,这样就可以很容易的看出是一个无限接近于0的数。
3.E全为1:与E全为0相反,是一个接近于无穷的数。
通过上述这一连串的规则,我们就可以很好地做出上面那个例子,上述例子有四个打印,接下来,就把四个打印逐一列举出来。
第一个打印:因为是整型存入、整型取出,所以结果一定是打印出9。
第二个打印:因为是浮点数类型,所以会将原来内存中32个比特位进行还原,其中会发现E为全0。根据上面内存中取出的三种情况中的第二种不难看出,还原出来的浮点数是一个无限接近于0的数。
第三个打印:根据浮点数的转换规则,9.0转换为二进制为1001.0(即为-1^0*1.001*2^3),然后因为是要以整型的形式取出,所以通过浮点数写出这32个比特位,转为原码,以整型的形式读出来,就可以很好地得到1091567616这个结果了。
第四个打印:因为是浮点数存入、浮点数取出,所以结果一定是打印出9.000000。



