众所周知,C中有符号char型的取值范围是-128——127,无符号char型的取值范围是0——255。而在这些基础知识点背后,有着许多更加底层的东西值得我们去挖掘与思考。
本篇文章将从数据在内存中的存储形式展开,解释为什么char型取值范围如上所述,并回答一个经典的面试题:为什么127+1=-128
一、 数据类型的基本分类
整型分类
字符型:char unsigned char
短整型:short [int] unsigned short [int]
整型: int unsigned int
长整型:long [int] unsigned long [int]
浮点型分类
单精度浮点型 : float
双精度浮点型 : double
二、占用存储空间:
char
- char类型通常占用1个字节(8位)的内存空间。
- 存储的数据范围为-128到127(有符号char)或0到255(无符号char)。
short
- short类型通常占用2个字节(16位)的内存空间。
- 存储的数据范围为-32768到32767(有符号short)或0到65535(无符号short)。
int
- int类型的大小通常为系统的字长,例如在32位系统中占用4个字节(32位),在64位系统中占用8个字节(64位)。
- 存储的数据范围为-2147483648到2147483647(有符号int)或0到4294967295(无符号int)。
long
- long类型通常占用4个字节(32位)或8个字节(64位)的内存空间,取决于系统的字长。
- 存储的数据范围与int类型相似,但更大。
float
- float类型通常占用4个字节(32位)的内存空间。
- 存储的数据范围为IEEE754标准中的单精度浮点数范围。
double
- double类型通常占用8个字节(64位)的内存空间。
- 存储的数据范围为IEEE754标准中的双精度浮点数范围。
三、数据在内存中的存储
1、整型在内存中的存储
原码、反码、补码
整型分为符号位和数值位,符号位为0表示正数,为1表示负数。
对于正数来说,原码、反码、补码均相同,求出原码即可
对于负数来说:原码:直接将数值按照正负形式转为二进制就是原码
反码: 符号位不变,其他位置数按位取反
补码: 反码+1得到补码
对于整型来说,数据在内存中存储的是补码
2、大小端字节序
大端存储:是指数据的低位保存在内存的高地址中,数据的高位保存在内存的低地址中
例如,十六进制数0x12345678,在内存中的存储顺序是:12 34 56 78。
小端存储:是指数据的低位保存在内存的低地址中,数据的高位保存在内存 的高地址中
例如,十六进制数0x12345678,在内存中的存储顺序是:78 56 34 12。
3、浮点数的存储
根据国际标准IEEE(电和电程协会)754,任意个进制浮点数V可以表成下的形式:
对于32位的浮点数,最⾼的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
4、char类型的存储
无符号char的范围是0——255,有符号char的范围是-128——127。明明是字符类型,为什么还会跟整形一样用数字来规定范围呢?实际上,char类型在内存中跟整形一样,也是以二进制形式存储,它只占一个字节,所以是八位二进制数。最高位是否作为符号位的问题将char类型分成了无符号char和有符号char,范围也因此不同,将这些二进制数转化为十进制,就得到字符对应的ASCII码值。也就是说char类型存储的实际上是字符的ASCII码值。当我将一个字符存储进char类型变量时,就是将它的ASCII码存进了char变量。
那么在这里讨论一下int与 char在scanf读入,print打印,以及互相赋值存储上的问题
-
关于scanf,它会读取缓冲区一个标准光标大小的内容,这部分内容会以什么形式存储进数据取决于占位符。比如说我在缓冲区打一个一个1,用%c读取就是视为字符‘1’,会将其ASCII码(49)存储,注意,此时的49是八位二进制形式;而如果以%d读取,就会视为整数1存储(三十二位二进制形式)。但要注意,不要出现以下这两种写法:
int a; scanf("%c",&a);
char a; scanf("%d",&a)
这两种写法违背scanf的使用约定,不满足其在%d或%c时的期望参数,这会导致未定义行为,例如存储进a的值是随即乱码。
-
至于打印,则是根据占位符将变量中存储的数据打印出来。比如我将一个char类型以%d打印,就是将其中存储的八位二进制视为整形打印,会发生整型提升;如果将整形以%c打印,会将其中存储的32位二进制数截取最低八位,然后将其视作ASCII码,打印出对应的字符。
-
如果将int类型赋值给char,会发生截断,取最低八位存储,相当于ASCII码。
-
如果将char类型赋给int,会发生整型提升。
四、整型提升
谈完了数据的存储,我们再来了解一下什么是整形提升。
1、整形提升概念
C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符(char)和短整型(short)操作数在使用之前被转换为普通整型,这种转换称为整型提升。
2、整形提升规则
-
对于有符号数(char, signed char ,short, signed short)整形提升转化成标准长度(int)时,在左端补得是最高位(符号位),正数补0,负数补1
-
对于无符号数(unsigned short ,unsigned char),在左端补得就是0。
在这里先补充一下char型存储的相关知识并对整形提升这一概念进行举例:
char型占一个字节,即八个比特位,无符号char范围是0——255,而有符号char范围是-128——127,这是因为有符号char的最高位是符号位(0表示整数,1表示负数)。
当我们定义一个char型变量并将一个整形赋给它,然后用%d打印出来时:
127是一个整形数字,它的原码,反码,补码都是00000000000000000000000001111111
char只存储一个字节,所以截断得到01111111并存储在a的八个比特位里。
当使用%d对char型的a进行打印时,就会发生一个整型提升,a是有符号char型变量,所以按照规则,在最高位前面补0直至32位,重新得到了127的补码00000000000000000000000001111111,这就是整形提升。
另外,在char型进行计算时,也会发生整形提升:
注意这里的计算逻辑一定要清晰:
* 计算时先提升a和b为整形,再将两个整形相加得到一个新的整形。
* 再将这个整形放进char型变量c中,发生上一个示例中的截断现象,仅剩八个字节存储在c中。
* 使用%d打印c,再一次发生了整形提升,将c提升为32位整形并进行打印,输出一个整形的打印值。
其实,由上述内容不难发现,整形提升实际上就是将一个只有8位的二进制数强行视作32位二进制数的过程,这一操作的目的是为了处理一些对char类型变量施以整形操作的行为,例如前面提到的将两个char类型相加,char类型是字符类型,两个字符怎么相加?所以将它们都提升为整形,再进行加法操作。
**实际上,一切将char类型视作整形的操作都要用到整形提升,**就连char类型的范围这一定义都与整形提升有关,一个字符类型的范围为什么会是整形区间?就是将其化作了整形来划分的。一个字符就8位,最高位是否为符号位决定了范围是-128到127还是0到255,超出这一范围的整形数字被当作char 类型存储时会发生截断,只取最低八位。
一个char 类型被视为整形后得到的数字就是它的ASCII 码,实际上,无符号char的128——255和有符号char的-128——-1都没有对应的符号,他们在内存中的二进制数是一样的,是否有符号位影响的是整形提升后整形的正负,符号本身是没有正负的。
五、经典问题:为什么127+1=-128?
相信经过上面的两个示例,大家对于整形提升已经有了一定的理解,但是大家如果仔细看代码的话,应该也已经发现了上述示例二的奇怪之处:
127+3=-126 ???
这是什么原理呢?
这个问题,就不得不回到我们的示例一来看。当我们将整形127赋给char型的a时,因为截断,所以只取了最低的八位进行存储,也就是说除了最高的符号位以外,只有七位可以表示数值,最大的七位二级制数就是127。因此有符号char的范围才是-128——127。
那么,当存储数值大于127或者小于-128时又该怎么办呢。
我们先多列几个测试样例:
按照这个规律不难发现,char的取值其实可以看作一个圆,这种现象被称为数值回绕,是指在计算中,数值超出了某个数据类型的表示范围时,数值会“回绕”到该范围的另一端,产生不符合预期的结果。
相应的,无符号char的取值规律是这样的:
最后,回到最开始那个有意思的式子:127+1=-128。
由于127以二进制存储的后八位是01111111,数据的相加实际上是补码相加,所以127和1都整形提升成32位二进制位进行相加,所得结果取最低八位是10000000。按照最高位是符号位来看,这个10000000是-0吗?或者说它就是-128,-128取代了-0的位置?
我在网上看到了许许多多的解释,都有些摸棱两可,难辨对错,在这说下我自己的看法:
数据在内存中存放和加减的都是补码,127的补码+1得到的10000000也是个补码,这个数被存储在一个char型的变量中,当被以%d打印的时候发生整形提升,对于补码10000000进行补位,得到11111111111111111111111110000000,化成反码为10000000000000000000000001111111,化为原码为10000000000000000000000010000000,正是-128。
这也正好解释了数值回绕现象。