本文将十分直白地讲解C语言数据在内存中的存储,让大家更加了解不同数据在内存中是如何被存储的,详解整型提升,同时解开浮点型在内存如何存储的疑惑,还会穿插一些题目来加深大家的印象。
计算机有三种2进制表示方法,都有符号位和数值位两个部分,
- 原码:直接将该数按照二进制的方式翻译过来就是原码。符号位为1。
- 反码:符号位不变,其他位置按位取反即为反码
- 补码:反码加1得到的数就是补码。
要记住的是:对于整形数据(包括字符类型数据)来说,内存中存放的是补码。
如图
在计算机系统中,使用补码会方便很多,可以将符号位和数值域统一处理,加法减法也可以统一处理(因为CPU只有加法器)。
如果想进行10减2的运算,但计算机之中只有加法器,转化为10加上(-2)进行计算
符号位进的1超出整型的范围,就被截断了,这样就可以将符号位和数值位进行统一的计算。
什么是整型提升呢?
C语言的整型运算总是至少以缺省类型的精度来进行的,为了获取这个精度,表达式中的短整型和字符类型的数据在进行计算时,通常会提升到普通整形。说白了就是将两个短整型数据运算时以整型数据来进行运算,运算完成后得到的结果将发生截断。
整型提升的规则
int main()
{
char a, b, c;
a = 127;
//补码为0111 1111
b = 5;
//补码为0000 0101
c = a + b;
//在进行运算时进行整型提升
//a提升为0000 0000 0000 0000 0000 0000 0111 1111
//b提升为0000 0000 0000 0000 0000 0000 0000 0101
//相加后 0000 0000 0000 0000 0000 0000 1000 0100
//因为c也是char类型,必将发生截断
//1000 0100,前边的位置补1
//补码1111 1111 1111 1111 1111 1111 1000 0100
//转化为原码,补码减1取反,符号位不变
//1000 0000 0000 0000 0000 0000 0111 1100
printf("%d", c);
//以%d形式打印,结果为-124.
return 0;
}
注:整型提升和普通的算术类型转换不一样,类型转换是因为两个或多个不同类型的数据进行计算时,将每个数据都转化为同一类型即范围最大的类型,而整形提升尽管计算的数据类型相同,还是可能会发生整型提升。
一个很直观的梨子:
a+b是一个算术表达式,a和b都提升至int型,所以占4个字节。
c为a+b的返回值,发生截断,变回char类型,所以只占一个字节。
short类型或char类型运算时,会提升至int类型,要将计算结果放在一个char类型或short类型的变量时,发生截断。
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("%d %d %d", a, b, c);
return 0;
}
运行结果如何呢?
-1的补码为1111 1111以%d形式打印,原码为补码减1取反,结果为-1,所以a和b的结果相同,皆为-1,而c为无符号类型,原反补相同,就算进行提升,前位也补0,11111111的十进制为255,故c的打印结果为255.
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
运行结果如何?
这里以%u的方式打印,-128在内存中的补码为1111 1111 1111 1111 1111 1111 1000 0000,a是char类型,所以a的补码为1000 0000。%u的意思是以十进制的方式打印无符号整型。在打印1000 0000时必将发生整型提升,整型提升时前边补零或是补一看的是符号位,所以前位补1。
提升后的补码为1111 1111 1111 1111 1111 1111 1000 0000,正数的原反补都相同,换成十进制
放在VS里运行后结果确实如此
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
道理相同,128的补码为0000 0000 0000 0000 0000 0000 1000 0000,a的补码为1000 0000,他和a是-128时存进内存的补码一样,a的类型同样为有符号类型,整型提升前位补1,所以打印结果一模一样。
大端小端是两种存储的方式,大端储存方式是数据的低位保存到内存的高地址中,数据的高位保存在内存的低地址处,小端存储方式是指数据的地位保存在低地址中,而数据的高位保存在内存的高地址处。
char类型的数据,在内存中只有一个字节,就不存在大小端问题,然而除了一个字节的char字符类型外还有2个字节的short,4个字节的int,等等等等,对于位数大于八位的处理器,由于寄存器的宽度大于一个字节,那么必然会出现如何对多个字节进行安排的问题,这就分化出了大小端问题。
如图详细介绍一下大小端
那么如何知道一个编译器具体是大端还是小端?
这是百度在2015年工程师笔试题里的问题,设计一个小程序判断当前机器的字节序是大端还是小端。
大端的话低位放在高地址,高位放在低地址。小端的话相反。
直接写一个代码调试先看一看
将右上角的列改为1,地址输入取地址a
可以看到低地址储存的是低位,高地址存的是高位。
int main()
{
int a = 1;
char* p = (char*)&a;
if (*p == 1)
{
printf("小端\n");
}
else
printf("大端\n");
return 0;
}
利用强制类型转换,取地址a取出首地址,即四个字节中的低地址位,强制类型转换为字符类型的指针,解引用后从内容即可判断其为0还是1从而判断大小端。
常见的浮点数类型有float,double,long double类型。
浮点数分为整数部分和小数部分,他们在内存中的存储和整型有什么区别?
看代码
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", *pfloat);
return 0;
}
运行后结果如图
num和*pfloat在内存中明明是同一个数,为什么结果差别如此之大,说明了浮点数和整形数据在内存中的储存方式不同。
根据国际标准,任何一个二进制浮点数V都可以用以下方式表示
(-1)^S *M *2^E (-1)^S表示符号位,如果S为0,V为正数,S为1,V为负数。
M表示有效数据,大于等于1,小于2。
2^E表示指数位。
举例
十进制的5.0,二进制形式为101.0,相当于1.01*2^2。用二进制浮点数表示(-1) ^0 * 1.01 * 2 ^2。
在这里S为0,M为1.01,E为2。
类比于十进制123,相当于1.23 * 10^2。
例如
十进制的浮点数5.5
二进制形式可以写作101.1,相当与1.011 * 2^ 2,用(-1)^0 * 1.011 * 2 * 2。
浮点数0.5
二进制形式0.1,相当于1.0 * 2^(-1)。
根据国际标准IEEE(电气和电子工程协会)规定:
对于32位的浮点数float而言,最高的1位是符号位S,接下来的8位是指数E,剩下的23位为M。
对于64位的浮点数double,最高的1位仍为S,接下来的11位是指数E,剩下的52位为有效数字M。
前边已经说过了,M大于1小于2,所以无论什么时候M都可以写作1.XXXXXX的形式,所以在保存M时,只保存1后边的部分,这样可以省出来一位。就比如32位float类型,留给M的只有23位,不保留小数点前的1的话,就可以保存24位有效数字。
对于指数E
首先E为一个无符号整数(unsigned int)
如果E为8位,他的取值范围为0~255;如果E位11位,它的取值范围为0
~2047。但是就像上述所说的0.5,E是可能出现负数的,所由IEEE规定,存入内存时,E的真实值必须要加上一个中间数,对于8位的E,这个中间数是127,对于11位E,这个中间数是1023。
比如2^10的E为10,在保存为32位的浮点数时,在内存中保存的数是10+127=137。即10001001。
(按照32位的float来说)取出直接指数E的值直接减去127。得到E的真实值后,将M前边的1补上,再根据S是0还是1,判断这个数的正负。
例如前边所说的0.5,二进制形式为0.1,正数部分必须为1,小数点前进一位。变为1.0*2^(-1)。
E的值为(-1)+127为126,表示为01111110。1.0去除1,只留下0,补齐至23位,就是23个零。而且因为他是正数。S为0.
在内存中为0 0111 1110 0000 0000 0000 0000 0000 000。
2. 当E为全0的情况
如果E为全零,那他原来的值就是-127。那将是一个超级小的数字,2的10次方就是1024,2的-127次方接近于无穷小,这时直接判定E的值为1-127。有效数字M不再加上第一位的1,而是还原为0.XXXXX的小数,这样做是为了表示±0,以及接近于0的很小的数字。
3. 当E全为1
如果E全为1,那么在没有加上中间值时,E得值为128,2的128次幂是一个很大很大的数,如果有效数字M全部为零,就表示无穷大,是正无穷还是负无穷取决于S。
现在再来解决上边的疑问。
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”, *pfloat);
return 0;
因为pfloat强制类型转化为float类型的指针,就是把该二进制序列看作浮点数的形式用%f的形式打印。
0 00000000 00000000000000000001001
用浮点数的储存方法解读,S为0,E为全零,是一个很小很小的数,而%f打印默认只能保留到小数点后六位。故打印结果为0.000000。
这个时候就变为了浮点数的形式储存9.0。
9.0的浮点数形式储存,S等于0,1.001 *2^3,E加上127,即130,二进制形式为10000010,M的001,后边全部补0。
综合为 0 10000010 0010 0000 0000 0000 0000 000
对比上边的十进制结果发现确实如此。
到了这里一切就都柳暗花明了。本章完成,如果哪里有错误的话还请大家指出。