C语言:数据在内存中的存储
目录
1. 整数在内存中的存储
2. 大小端字节序和字节序判断
2.1 什么是大小端
2.2 为什么会有大小端
2.3 练习
2.3.1 练习1
2.3.2 练习2
2.3.2 练习3
2.3.4 练习4
3. 浮点数在内存中的存储
3.1 浮点数的存储
3.1.1 浮点数存储的过程
3.1.2 浮点数取出的过程
3.2 题目解析
1. 整数在内存中的存储
我们在之前的操作符的学习中就学习过:
整数的二进制表示方法有三种,分别是:原码,反码和补码。
在表示有符号的整数时,三种表示方法都有符号位和数值位两部分。最高位为符号位,符号位上0表示“正”,1表示“负”。其余位均为数值位。
正整数的原、反、补码都相同。 负整数的三种表示方法各不相同。
原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
对于整型数据来说,数据在内存中存放的其实是补码。
这是因为:原因在于,使用补码,可以将符号位和数值域统⼀处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
2. 大小端字节序和字节序判断
2.1 什么是大小端
当大小超过一个字节的数据在内存中存储的时候,就会有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储,具体的概念为:
大端(存储)模式:是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。
小端(存储)模式:是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。
例如:一个16bit的short类型数据x,在内存中的地址为0x0010,x的值为0x1122。那么,0x11为高字节,0x22为低字节。大端模式下,0x11位于低地址中,即0x0010;0x22位于高地址中,即0x0011中。小端模式则刚好相反。
2.2 为什么会有大小端
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8个bit 位,但是在C语言中除了8bit的 char 类型之外,还有16bit的 short 类型,32bit的 long 类型(不同的编译器可能会有差异)。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就出现了大端存储模式和小端存储模式的区别。
2.3 练习
2.3.1 练习1
题目:设计一个小程序来判断当前机器的字节序、
了解了上面大小端字节序的知识后,我们知道,大小端字节序的区别在于存储顺序之上,那么,想要判断当前机器的字节序,就要判断该机器的存储顺序。
那么,我们就可以通过创建一个整型变量a,并且赋值为1。二进制形式下,1只有最低位为1,其余位均为0。所以,我们接下来就可以通过指针来取出这个整型变量的第一个字节即低地址的数据。如果为0,说明是大端字节序;如果为1,说明是小端字节序。
代码形式如下:
#include <stdio.h>
void check_sys()
{int a = 1;int i = *(char *)&a;if(i == 1)printf("该机器为小端字节序");elseprintf("该机器为大端字节序");
}
int main ()
{check_sys();return 0;
}
该机器为小端字节序
可见,我使用的计算机属于小端字节序。
2.3.2 练习2
推测打印结果:
#include <stdio.h>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;}
可以发现,这段代码创建字符型变量a、b、c,最后以整型数据的形式打印出a、b、c。
我们知道,无论是signed char 类型或是unsigned char类型数据,其大小均为一个字节;而int型数据大小为4个字节。因此,在以整型数据形式打印字符型数据时,编译器会对数据进行整型提升。那么,编译器的大致处理如下:
char a = -1;
char类型默认为有符号字符型数据,数据在内存中以补码形式存储,在进行整型提升时会在其前面补上符号位的数据直至补齐32位:
原码:10000001
反码:11111110
补码:11111111
补码整型提升后:11111111111111111111111111111111
编译器在打印时会将数据以原码的形式打印出:
整型提升后反码:10000000000000000000000000000000
整型提升后原码:10000000000000000000000000000001
因此,打印结果为-1
signed char b = -1;
同理,打印结果也为-1unsigned char c = -1;
unsigned char类型为无符号字符型数据,范围为0~255,当赋值为有符号型的-1时,会进行有符号型数据到无符号型数据的转换。在进行整型提升时,会在其补码前补上0直至补齐32位
有符号型转换为无符号型:转换结果 = 有符号型数据 + k * (对应无符号型数据最大值 + 1);k为整数,保证转换结果值在无符号型数据范围内
在此处,转换结果 = -1 + 1 *(255 + 1) = 255
无符号型原码,反码,补码相同:11111111
整型提升后:00000000000000000000000011111111
因此,打印结果为255
a=-1,b=-1,c=255
2.3.2 练习3
#include <stdio.h>
int main()
{char a = -128;printf("%u\n",a);return 0;
}
我们可以发现,这段代码也是创建一个char 类型变量a,最后以无符号型整型数据形式打印出来,思路上与上一题大体一致,但是特殊地方在于:-128是char 类型数据能表达的最小值,其补码直接定义为10000000 ,无对应的反码与原码,编译器在处理这个值时也会直接识别为-128,而不是通过计算得到其表示的值。
char a = -128;
补码:10000000
整型提升后补码:11111111111111111111111110000000
int类型下,此值依旧表示-128,但因以%u形式打印,会作为32位无符号数解读,最终结果为:
4294967168
4294967168
那么,如果将a赋值为128,打印结果还相同么:
#include <stdio.h>int main(){char a = 128;printf("%u\n",a);return 0;}
答案是相同的,这是因为,char类型数据范围为-128~127,128已经超出了表示范围,所以在转换之后,数据会变为-128,最后打印结果相同。
4294967168
2.3.4 练习4
推测打印结果:
#include <string.h>
#include <stdio.h>
int main()
{char a[1000];int i;for(i=0; i<1000; i++){a[i] = -1-i;}printf("%d",strlen(a));return 0;
}
这题很有迷惑性,创建一个拥有1000个元素字符数组。通过for循环给每个元素赋值。最后,用strlen函数计算从数组a首元素开始直至第一个'\0'之间的元素个数。看到strlen函数时,我们第一反应便是寻找'\0'的位置。但是,我们发现,在给数组元素赋值时,似乎并没有出现'\0'。难道这个代码的结果是未定义的么?
这个时候,我们就应该想到,'\0'也是字符型数据。既然是字符,那么就有对应的ASCII码值,而字符'\0'对应的ASCII码值就为0。而这个数组是字符数组,每个元素都是字符型数据,而字符型数据在内存中的本质也是整型数据,大小为其对应的ASCII码值。因此,现在我们只需要找到第一个值为0的元素的位置即可。
那么,根据代码,我们发现,似乎a[i]的值怎么都无法为0。那么,这个时候,我们就需要考虑到,char 类型数据范围为-128~127。从-1开始递减到-128后,此时一共有128个数。再进行-1,值会变为127;然后从127开始递减,直到出现第一个0,此时,一共有128+127 = 255个数。因此,打印结果为255。
255
3. 浮点数在内存中的存储
常见的浮点数类型有:float、double、long double
浮点数表示的范围在头文件 float.h 中定义。
3.1 浮点数的存储
我们知道,浮点数与整数的最大区别便是,其可以表示小数,那么,浮点数的存储方式与整数一样么?我们可以用代码测试一下:
#include <stdio.h>
int main()
{int n = 9;float *pFloat = (float *)&n;printf("n的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);*pFloat = 9.0;printf("n的值为:%d\n",n);printf("*pFloat的值为:%f\n",*pFloat);return 0;
}
n的值为:9
*pFloat的值为:0.000000
n的值为:1091567616
*pFloat的值为:9.000000
通过打印结果我们可以明显发现,明明n与*pFlaot表示的是同一个数,但无论是以整数形式打印浮点数或是用浮点数形式打印整数,结果都不正确。那么,浮点数在内存到底是如何存储的呢?
根据国际标准IEEE(电气和电子工程协会)754,任意⼀个二进制浮点数V可以表示成下面的形式:
其中:
表示符号位,当S=0,V为正数;当S=1,V为负数
表示有效数字,M是大于等于1,小于2的
表示指数位
例如:十进制的5.0用二进制表示就是101.0,相当于1.01 * 2 ^ (2)
那么,S = 0,M = 1.01,E = 2
IEEE 754规定:
对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M 对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
3.1.1 浮点数存储的过程
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是可以出现负数的,所以IEEE754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
3.1.2 浮点数取出的过程
指数E从内存中取出还可以再分成三种情况:
E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
比如:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0 * 2 ^ (-1),其阶码为-1 + 127(中间值) = 126,表示为 01111110 ,而尾数1.0去掉整数部分为0,补齐0至23位:00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
E全为0
这时,浮点数的指数E等于 1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
0 00000000 00100000000000000000000
E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位S)。
3.2 题目解析
了解了浮点数存储与取出的相关知识后,我们现在来看看一开始的代码:
n的值为:9
*pFloat的值为:0.000000
n的值为:1091567616
*pFloat的值为:9.000000
那么,为什么以浮点数形式打印整数10会打印出0.000000呢?
9以整数形式存储在内存中的二进制形式为:
0000 0000 0000 0000 0000 0000 0000 1001
那么,将 9 以二进制的浮点数形式来拆分。
第一位为符号位,S = 0;后面八位为指数E = 00000000;剩余23位为有效数字M = 00000000000000000001001
因此,以浮点数形式取出的结果就应该是:
V = (-1) ^ 0 * (0.00000000000000000001001) * 2 ^ (1 - 127) = 1.001(二进制) * 2 ^ (-146) = 1.125(十进制) * 2 ^ (-146)
很明显,这是一个接近0的非常小的数字。打印结果就为 0.000000
那为什么浮点数9.0以整数形式打印出来是那么大一个数呢?
9.0的以浮点数形式存储在内存中的二进制形式如下:
符号位S = 0;9.0的科学计数法形式为 :1.001 * 2 ^ (3),有效数字后再加上20个0凑齐23位得到M,指数部分值E为 3 + 127 = 130,二进制形式为10000010
那么,最终形式就是
0 10000010 00100000000000000000000
那么,以整数形式取出并打印,因为是正数,原码与补码相同,结果就是:1091567616
完