-
前言
我们都知道数据,不论整型,浮点型等,在内存中都是以二进制形式存储的。今天主要以整型和浮点型为例,来看一下它们在内存中的存储形式及原理。
内置的基本数据类型(32位机器下):
char 字符型, 占1个字节
short 短整型,占2个字节
int 整型,占4个字节
long 长整型,占4个字节
float 单精度浮点型,占4个字节
double 双精度浮点型,占8个字节
-
整型
我们都知道,整型不论是字符型char,还是短整型short等,都包含两种形式:无符号数和有符号数。
char:unsigned char ; signed char
short:unsigned short ; signed short
int :unsigned int ; signed int
long:unsigned long ; signed long
那么,对于整型数据来说,其在内存中存储的其实是它的补码形式(可以将符号位和数值位统一处理)。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
int a = 20;
int b = -10;
system("pause");
return 0;
}
以上边代码为例,我们来看看变量a和b在内存中的存储。
变量a:二进制补码为0000 0000 0000 0000 0000 0000 0001 0100,其转化为十六进制为00 00 00 14
变量b:二进制补码为1111 1111 1111 1111 1111 1111 1111 0110,其转化为十六进制为FF FF FF F6
这正如我们所说,整型数据在内存中是以二进制补码形式存储的。但是有的人会发现为什么它们存储的时候高低位跟我们平常所见的高低位排列顺序不太一样呢?
这是因为有大小端:
大端(存储)模式:数据的高位存储在内存的低地址,低位存储在内存的高地址。
小端(存储)模式:数据的低位存储在内存的低地址,高位存储在内存的高地址。
那么为什么会出现大小端呢?
在计算机系统中,一个存储单元等于一个字节,即8个比特位。但是在C语言中,除了占1个字节的char型外,还有占2个字节的short型和4个字节的int型(看具体的编译器)。对于一次性处理位数大于8位的处理器来说,如16位或32位处理器,由于寄存器位数大于一个字节,所以必然存在如何安排多个字节的问题,所以就出现了大小端存储模式。
以16位处理器下的short型变量x为例,假如x = 0x3344,在内存中的地址为0x0011,其中0x33为高字节,0x44为低字节
大端存储模式下:高字节0x33存储在0x0011地址,低字节0x44存储在0x0012地址;
小端存储模式下:低字节0x44存储在0x0011地址,高字节0x33存储在0x0012地址。
在知道了什么是大小端之后,对于上边所举例子,我们便可知道变量a、b是以小端模式存储的。
接下来我们看几个例题:
例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf(" a = %d\n b = %d\n c = %d\n", a, b, c);
system("pause");
return 0;
}
解析:因为char型默认是有符号型,所以此题我们相当于只求b和c。
因为b、c均为char型,而输出时要以%d,即signed int型形式输出,所以必然要进行整型提升:当原变量类型为无符号数时补0,否则补符号位。 值得注意的是,在进行整型提升的时候我们看的是原变量的类型而不是输出类型。
假如将此题的变量b以无符号整数形式输出,结果又是什么呢?
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf(" a = %d\n b = %u\n c = %d\n", a, b, c);
system("pause");
return 0;
}
我们发现再次输出b时,b已经变成了一个很大的数。再次验证了在进行整型提升的时候,我们看的是原变量的类型而不是输出类型。
例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
char a = -128;
printf(" a = %u\n", a);
system("pause");
return 0;
}
这道题需要注意的是 a = -128,所以其补码为1000 0000。
如果以%d形式输入a,则
解析如下图:
例3:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
char a = 128;
printf(" a = %u\n", a);
system("pause");
return 0;
}
解析:这道题跟例2结果相同,但是此题a为128。其原码、补码均为1000 0000。由于有符号char的取值范围为-128~127,所以当1000 0000存入内存时,计算机是将其当做-128的补码来看待的。
将a以有符号整数形式再次输出,其结果为:
解析如下:
例4:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
system("pause");
return 0;
}
解析如下:
例5:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
Sleep(1000);
printf(" %u\n", i);
}
system("pause");
return 0;
}
通过结果图我们可以很明显地看出这是一个死循环 。这是为什么呢?
原因是变量i是一个无符号数,而无符号数永远是大于等于0的。当i变成0后,i再减1就变成了最大值4294967295,然后又从最大值开始往下减1,直到减到0,然后又从最大值开始往下减,所以此题是一个死循环。
例6:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
system("pause");
return 0;
}
解析:我们知道char型数据的取值范围为-128~127,所以此题当-128减1后就变成了127,一直继续,直到减为0,字符串停止计数,所以从-1到-128,127到1共有255个数。
a[0] = -1 - 0 = -1;
a[1] = -1 -1 = -2;
....
a[127] = -1 - 127 = -128;
a[128] = -1 - 128 = 127;
a[129] = -1 - 129 = 126;
...
a[254] = -1 - 254 = 1;
a[255] = -1 - 255 = 0; //字符串结束标志“\0”的ASCII码为0
例7:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
unsigned char i = 0;
for (i = 0; i <=255; i++)
{
Sleep(1000);
puts("happy new year!\n");
}
system("pause");
return 0;
}
通过结果图片我们可以很明显地看出此题也是死循环。因为i是unsigned char型,所以255加1后变成了0。
-
浮点型
数学中我们通常可以用科学记数法(-1)^(s)*M*10^(e)来表示一个十进制数,其中0<M<10。那么我们是否可以用相同方式来表示二进制浮点数呢?
根据国际标准IEEE754,任意一个二进制浮点数V可以表示成 (-1)^(s)*M*2^(E) 的形式。
(-1)^(s)表示符号位(s=0时V为正数,s=1时V为负数)
M表示有效数字,大于等于1,小于2
2^(E)表示指数位
如十进制的9.0,写成二进制形式为1001.0,相当于1.001*2^(3) = (-1)^(0)*1.001*2^(3) (s=0,M=1.001,E=3)
IEEE754规定:
1.对于32位浮点数来说,最高位是符号位s,接着8位是指数E,剩余23位是有效数字M。
2.对于64位浮点数来说,最高位是符号位s,接着11位是指数E,剩余52位是有效数字M。
3.对于有效数字M来说,因为1<=M<2,也就是说,M可以写成1.xxxx的形式,其中xxxx表示小数部分。IEEE754规定,在计算机存储M时,默认该数的第一位总是1,因此可以被舍去,只保留小数部分。如保存1.001时,只存001,当读取的时候再加上1,这样可以空出一位来多表示一个有效数字。以32位浮点数为例,M可以表示24位有效数字。
4.对于指数E来说,E是一个无符号整型(unsigned int),这就意味着8位的E的表示范围为0~255,11的E的表示范围为0~2047.但是如果用科学记数法表示一个数时,E是可以出现负数的,所以IEEE754规定,存入E的真实值时必须在此基础上加上一个中间数,对于8位的E来说是127,对于11位的E来说是1023.
如2^(2),此时E=2,保存成32位浮点数时,必须保存成2+127=129,即1000 0001.
总结:当8位的E的真实值存入内存时,我们应该加上127;取的时候减去127.
接下来我们来看一个例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
int n = 9;
float *pF = (float *)&n;
printf("n的值为:%d\n\n", n);
printf("*pF的值为:%f\n\n", *pF);
*pF = 9.0;
printf("n的值为:%d\n\n", n);
printf("*pF的值为:%f\n\n", *pF);
system("pause");
return 0;
}
解析:
程序前半部分中,由于n为int型,所以以"%d"的形式输出n的值为9,这个是显然的; 当以"%f"的形式输出*pF时,由于s=0,E=00000000,M=00000000000000000001001,所以取出时E-127=-127,即*pF=(-1)^(0)*1.00000000000000000001001*(2)^(-127)近似为0。我们知道在判断一个浮点数等于0时,只要该浮点数在一定精度下趋近于0,便认为该浮点数为0,所以*pF=0
程序下半部分中,*pF=9.0,转化成二进制为1001.0=(-1)^(0)*1.001*(2)^(3),此时s=0,M=001,E=3。存入内存后E=3+127=130->1000 0010(二进制)。此时内存中n的二进制为0100 0001 0001 0000 0000 0000 0000 0000,当以“%d”形式输出n时,其值为1091567616,以“%f”形式输出n时自然为9.000000.