之前写过一篇关于C语言内存管理的文章,对在C语言中使用内存中需要注意的一些问题和解决办法做了一些总结。实际上,内存终归是要存储数据的,这次对C语言中的数据存储做一些讨论。
(资料图)
本文结构:
C语言数据类型可以分为两大类:内置数据类型和构造数据类型
内置类型包含整形家族、浮点型、指针类型和空类型;构造数据类型可以由基本数据类型进行组合以实现数据类型的自定义,包含数组、结构体、枚举和联合。
整型家族包含各种类型的int
类型,因为char
类型在内存中以ASCII
码值的形式存储,所以也被归为整型家族;浮点型包含float
和double
类型;指针类型种类较多,可以是任意内置类型的,指针类型的意义主要是解引用时访问空间的大小和决定指针加一的步长;void
类型常见于函数的参数和返回值。
构造数据类型相对自由,可以由不同内置数据类型进行构造。关于构造数据类型相对详细的讨论可以参考我之前的一篇文章。
为什么数组属于构造数据类型:
#includeint main(){ int arr1[10] = { 0 };//数组arr1的类型为 int [10] int arr2[11] = { 0 };//数组arr2的类型为 int [11] return 0;}
数据类型的意义主要体现在存储数据和读取数据两个角度。
一方面,存储数据时,可以明确所需存储空间的大小,以更有效地利用内存空间;另一方面,在读取数据时,能够明确如何看待内存块,准确地将数据读出。
作为内置数据类型最庞大的家族,整形的存储相对简单。整型在内存中以补码存储,正数的原码反码补码相同,负数需要另做计算。
原码即十进制数直接转化为二进制之后的二进制序列;原码的符号位不变,其他位按位取反得到反码;反码加一得到补码。
规定正数的原码反码补码相同,补码实际上是为了存储负数和进行减法运算而存在的。因为CPU只有加法器,计算减法时实际上计算的是正数加负数,如果直接将两者的原码形式相加,便会得到错误的结果。
另外,由于原码与补码之间的转换过程是相同的,原码按位取反加一可以得到补码,补码按位取反加一可以得到原码,所以二者之间的转换不需要额外的硬件电路。
int main(){ int a = 1;//00000000 00000000 00000000 00000001 int b = 1;//00000000 00000000 00000000 00000001 int c = a - b;//a + (-b) 1 + -1 //11111111 11111111 11111111 11111111 - -1的补码 //00000000 00000000 00000000 00000001 - 1的补码 //00000000 00000000 00000000 00000000 - 相加结果 return 0;}
如果你在X86平台上有过内存调试经验,就一定会发现数据并没有乖乖地按顺序排列起来,而是一个倒序排列。这与机器的内存存储模式有关。
对于大小大于一个字节的数据,必然会存在如何安排多个字节的问题,这就产生了大端存储模式和小端存储模式。
当前存在两种存储模式:大端存储和小端存储。大端存储又称为大端字节序存储,这种模式在存储数据时,将数据的低位存在高地址处,将数据的高位存在低地址处;小端存储模式在存储数据时恰好相反,体现出数据呈倒序存储的现象。不管是大端或小端,两者都是以字节为单位进行存储的。
浮点型数据虽然种类少,但是存储方式相对复杂。
int main(){ int n = 9; float *pFloat = (float*)&n; printf("n的值为:%d\n",n); printf("*pFloat的值为:%f\n",*pFloat); *pFloat = 9.0; printf("num的值为:%d\n",n); printf("*pFloat的值为:%f\n",*pFloat); return 0;}
程序的输出情况是什么?如果你运行程序,大概率会发现程序的输出并不如你所料。为了弄清楚这个现象,就需要了解浮点型的存储规则。
根据 IEEE 754标准规定,任意一个二进制浮点数 F
都可以写成以下形式:
(-1)^S
表示符号位,S = 0
时表示正数,S = 1
时表示负数M
是有效数字,M >= 1 && M < 22^E
表示指数位举例:
5.5在内存中的存储:5.5转化为二进制:5.0 = 101.1101.1转化为标准形式:101.1 = (-1)^0 * 1.011 * 2^2所以5.5在内存中存储时:S = 0; M = 1.011; E = 2
IEEE 754标准规定:
32位的浮点数在内存中以这种规则存储:最高的 1位是符号位S
,接着是8位的指数E
,最后的 23位为有效位。
64位的浮点数在内存中以这种规则存储:最高的 1位是符号位S
,接着是 11位的指数E
,最后的 52位为有效位。
由于有效位总是大等于一小于二的,所以在存储时只存储小数点后面的数字,这样可以节省一位有效数字。
对于指数 E 有些特别的规定:
E为一个无符号整数。如果E为 8位,则它的取值范围是0~255
;如果E为11位,则它的取值范围是0~2047
。但实际上,E 可能出现为负数的情况,为了解决这个问题,需要在存入内存时将E加上一个中间值(127或1023)。比如存储5.5时,E为2,在存储时需要加上127,即在内存中存储的是129,即1000 0001
。
将E从内存中取出时有三种情况:
一.E即含 0 又含 1
这种情况直接将E减去相应的中间值即得到原值。
二.E为全0
这时E为 1减去中间值即为真实值(-126或-1022),且 M不再加上一,而是还原为0.xxxxxx
以表示接近0的数字。
三.E为全1
这时可以将E的原值看做128或1024,结合S的值,表示正/负无穷大。
这时我们可以对开始的例子作出解释。
int main(){ int n = 9; //00000000 00000000 00000000 00001001 - 9的补码 float *pFloat = (float*)&n; //0 00000000 00000000000000000001001 - 在pFloat的视角下9的存储模式是浮点型 //S E M //(-1)^0 * 0.00000000000000000001001 * 2^(-126) //以浮点型打印即为:0.000000 printf("n的值为:%d\n",n);//9 printf("*pFloat的值为:%f\n",*pFloat);//0.000000 *pFloat = 9.0;//对n进行修改 //1001.0 - 9.0 //(-1)^0 * 1.001 * 2^3 //0 10000010 00100000000000000000000 - 9.0 //01000001000100000000000000000000 - 以%d形式打印的n printf("num的值为:%d\n",n);//1,091,567,616 printf("*pFloat的值为:%f\n",*pFloat);//9.0 return 0;}