【C语言笔记】基本算术类型与其在内存中的存储方式+深入理解
C语言中有两大基本的算术类型,即整型和浮点型,对应现实生活中的整数和小数,本篇将给大家整理这两种基本算术类型的相关知识及其在内存中的存储方式。
一、整形家族
整形家族的成员有很多,主要的有:int、long int、short int和char,而且他们也都还分为有符号signed和无符号sunsigned,不过其中char类型比较特殊,存在既不带signed也不带unsigned的“单独”char类型.
1、整型在内存中的存储方式
计算机中的整数都有三种二进制的表示方式,即原码、反码、补码,三种表示方式的首位都表示符号位,符号位为0表示整数,符号位为1表示负数,其中整数的原反补三码相同,而对于负数的原反补就要进行区分了:
原码:直接将数值按照正负形式转换正二进制得到的就是原码
反码:原码的符号位不变,其他位按位取反就得到了反码
补码:反码+1就得到了补码
为了统一,不管是正数还是负数,内存中一律存储的都是补码。整数在进行计算的时候用的都是补码。
为此我设计了一个可以打印整数补码的程序:
#include <stdio.h>
int main() {
int n = -7;
// -7的原反补
// 原码:10000000 00000000 00000000 00000111
// 反码:11111111 11111111 11111111 11111000
// 补码:11111111 11111111 11111111 11111001
int i = 0;
for (i = 31; i >= 0; i--) {
printf("%d", (n >> i) & 1);
}
return 0;
}
运行结果:
由此我们可以看出,内存中存的确实是补码。
2、“大端”和“小端”?
上面已经说了整型在内存中一律存储的是补码,那我们就到内存中去看看吧,上面的-7为例子:
我们访问变量n的地址,看到的是下面这样的十六进制序列:
可我们知道-7的补码的十六进制序列明明是:ff ff ff f9,这里为什么是导过来的呢?
这里就要说到大端和小端字节序的存储方式了:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在高地址中。
简单点来及就是看数据的低位(权重低)字节序,被放在高地址还是低地址,放在高地址就是大端,放在低地址就是小端。
之所有会有大端和小端之分,是因为在计算机系统中,我们是以字节为单位的,那么当要存储大于一个字节的数据时,就必然要涉及到存储顺序的问题,具体是大端还是小端,要看具体环境。
3、长整型大于短整型?
当我们听到长整型和当整形这两个名字时候,一定下意识的认为长整型比短整型要大,但事实并非如此,C语言标准中并没有明确规定长整型一定要大于短整型:
长整型至少应该和整型一样长,而整形至少应该和短整型一样长。
--------------------《C和指针》
且ANSI标准要求:
long型整数的长度至少应该是32位,而short型和int型整数的长度至少应该是16位。
所以大家以后看到任何长整型大于短整型的说法,就都是错的。
4、字符是无符号整型还是有符号整形?
这个问题其实只有在当我们要把一个字符值转换成一个较大的整数时才显得重要,因为编译器要考虑到底是把字符当做有符号字符还是无符号字符,如果是有符号数,高位补的就是符号位,如果是无符号位,高位就统一补0,这将会直接影响到转化后的值。
因为存在signed char和unsigned char的问题,所以只有当程序所用的char型变量的值位于signed char和unsigned char之间(即signed和unsigned都可以表示)时,这个程序才是可以移植的,也就是因为这样ASLL码字符集中的字符都是位于这个范围之内的。
也正是因为有这个问题,所以在一个把字符当做小整型值得程序中,建议显示的声明这类变量为signed或unsigned这样有助于提高程序的可移植性。
5、字符型和整形能够表示的数值的范围
最低限度的范围如下表所示:
但实际上,各种数据具体能够表示的数值范围因编译器而定,比如许多编译器可以多表示一个负数,比如有些编译器的signed char的取值范围为-128-127,这多出来的-128是怎么回事呢?
原因如下:
上面以signed char为例,其实其他的类型(如signed int)也是一样的。
二、浮点型家族
1、浮点型在内存中存储的方式
根据国际标准IEEE(电器和电子工程协会)754,任意一个二进制浮点数可以表示成下面的形式:
(-1) ^ S * M * 2 ^ E
其中“^”表示的是数学中的次方运算,不再是编程中的运算;
S表示符号位,E表示指数,M则为有效数字;
其中M规定为一个大于等于1且小于2的数字,即M可以写成1.xxxxx的形式。
下面是IEEE 754规定的32位的单精度浮点数二进制位的分配情况:
对于双精度浮点数,它的符号位S还是保持为1位,只是E和M变大了:
因为,M的形式统一为1.xxxxxxx的形式,所以在存储的时候,就没有必要在存储整数部分的1了,只需要存储小数部分的xxxx即可,到使用的时候再把整数1加上就行了。
指数E是一个无符号整数,但是我们知道在现实生活中,指数E是可能
出现负数的,为了保证E不为负数,IEEE 754规定,存入时E的真实值必须加上一个中间数,对于8位的E,这个中间数是127,对于11位的E,中间数是1023。
将指数E从内存中取出来还可以再分为三种情况:
1、E不为全0也不为全1时
这时对于指数E只需要反过来计算就行了,即指数E的值减去127(或1023)。这时需要将有效数字M加上整数1.
2、E为全0时
这时指数E等于1-127或者(1-1023)即为真实值,
但M不再加上1,而是还原为0.xxxxx的小数,这样就可以表示正负0,以及无限接近0的很小的数字。
3、E为全1时
这时,如果有效数字M为全0,则表示正负无穷大。
三、内存中的二进制序列是唯一的
大家看到以下这段程序的时候可能会疑惑:
大家可能会疑惑:这个n不是一个无符号int吗?怎么打印出来一个负数啊?
其实这里并不是,程序出了错误,而是使用n的方法不正确,
其实数据在内存中存储的二进制序列是唯一的,我们平时在打印的时候的格式%的或者%u只是决定以哪种形式解析这个二进制序列,以%d的形式打印就是将这个二进制序列解析成一个有符号整数,以%u型是打印就是将这个二进制序列解析成无符号整数。
就比如-9这个数字:
打个比方:我们计算机存储的数据的二进制序列就像是发电厂存储的电能,无论是以为动力发出来的电(水力或者风力),存入电站都只是电能而已。我们平时使用数据的时候就相当于把电接到各种电器上,接到烧水器上就是把电能转化成热能,接到风扇上就是将电能转换成动能。
所以们平时使用的数据类型也可以说是“数据的使用类型”,因为对于计算机本身而言,数据类型只有一种,那就是“二进制类型”。
本篇参考书籍:
《C和指针》
《C陷阱和缺陷》
《明解C语言》