物理 [栖岸计划]智英带你学C语言(第三章)
返回目录:[栖岸计划]智英带你学C语言(新版回归)
第三章 变量
3.1 整型变量
C语言中的整型变量有很多种,区别主要在于能否表示负数(即是否有正负号),以及表示整数的大小范围。根据是否有符号,我们可以分为signed和unsigned。通常情况下,signed可以省略。
我们知道,计算机以二进制的方式存储数据。所以,使用 $n$ 比特的存储空间就可以表示出 $2^n$ 种不同的数。下面给出常用的几种整型变量,它们通常需要的存储空间如下:
char:1字节,即8比特;
short:2字节,即16比特;
int:4字节,即32比特;(注:一些古老的机器上,int只占2字节,不过现在基本见不到了)
long:存在不同标准,一般为4/8字节,即32/64比特;
long long:8字节,即64比特。
对于无符号整数,由其二进制表示推算表示的整数方法如下:
将最低位记为第0位,从低往高,如果第 $i$ 位的值是1,那么表示的数就应该要加上 $2^i$ 。
例如,我们考虑
unsigned char a = 0b10011011;
这里0b表示后面的内容是二进制数。从低往高,第0, 1, 3, 4, 7位的值为1,故a表示的整数就是
$2^0+2^1+2^3+2^4+2^7=155$ 。
我们验证一下:

对于有符号整数,情况要更为复杂,下面详细展开:
我们规定,有符号整数的最高位为符号位,值为0表示为正数,值为1表示为负数。如果变量为 $n$ 位,那么绝对值不超过 $2^{n-1}-1$ 的整数,均可以用除了最高位以外的部分进行编码,方法同上。
例如,因为7是正数,所以其符号位为0,而 $7=2^0+2^1+2^2$ ,其对应的编码就是00000111;-7是负数,其符号位为1,其对应的编码就是10000111。这种编码被称为原码。
实际的运算过程中,原码会带来很大的不便。例如,7+(-7)=0,这点在数学上是显然的;但是我们比较这三个数的原码:
7:00000111
-7:10000111
0:00000000
我们发现,直接类比竖式计算,对7和-7的原码相加,得到的结果并不是0。为了避免类似问题的出现,引入反码和补码:
对于正数,反码和补码都等于原码;
对于负数,反码是将原码除符号位的部分按位取反,补码等于反码加1。
例如,前面提到,-7的原码是10000111,将除了最高位的每一位按位取反,得到其反码为11111000;再将其加上1,就得到其补码为11111001。我们与7相加:
7:00000111
-7:11111001
类比竖式,相加之后每一位都是0。(最高位当然不会再往上进位,否则就不是“最高位”了。)可以说明,对于不超出存储范围的计算,结果都是正确的。这点对计算机非常有利。所以,计算机内一般都使用补码存储有符号整数。
对比上面,如果考虑
char a = 0b10011011;
那么我们由补码反推原码,并得到对应的整数。将上述过程倒过来即可。
将数减去1,得到10011010;除了最高位,每位按位取反,得到11100101;再将除了最高位,其他值为1的位表示的数加在一起:
$2^0+2^2+2^5+2^6=101$ 。
所以a存储的数就是-101。
我们验证一下:

没有问题。
使用上面的方法,有一个小问题,即:符号位是1,但其余所有位都是0的编码不表示任何一个数。因此我们规定:对于这种情况,如果是 $n$ 位整数,表示的数就是 $-2^{n-1}$ 。
例如,我们考虑
char a = 0b10000000;
char是8位整数,所以a存储的值就是 $-2^7=-128$ 。验证一下:

因此,我们通过这种方式,用n位整数表示了 $-2^{n-1}$ 到 $2^{n-1}-1$ 之间的所有整数。
在头文件limits.h中,存有无符号整数能存储的最大值,以及有符号整数能存储的最大和最小值。通过如图的程序,可以查看这些整型变量的存储范围:

输出结果如下:
CHAR_MAX: 127
CHAR_MIN: -128
SHRT_MAX: 32767
SHRT_MIN: -32768
INT_MAX: 2147483647
INT_MIN: -2147483648
LONG_MAX: 2147483647
LONG_MIN: -2147483648
LLONG_MAX: 9223372036854775807
LLONG_MIN: -9223372036854775808
UCHAR_MAX: 255
USHRT_MAX: 65535
UINT_MAX: 4294967295
ULONG_MAX: 4294967295
ULLONG_MAX: 18446744073709551615
(LONG_MAX, LONG_MIN, ULONG_MAX可能受标准影响)
一般来讲,我们要控制数据以及它们运算得到的值不能超出上面说的范围。例如,对两个int类型变量作加法时,就必须尽量让和不大于INT_MAX,不小于INT_MIN。那如果突破了这个界限会发生什么呢?我们来看一下:

当我们把INT_MAX加上1之后,得到的是一个很小的负数,刚好是INT_MIN。类似地,把LLONG_MAX加上1之后,得到的是LLONG_MIN。这仅仅是加法,如果是乘法,我们可能会得到一些看似没有意义的数值:

当然,我们知道,INT_MAX乘以它自己绝对不可能等于1。
这种现象出现的原因是什么呢?因为存储数据范围的限制,当数学${\displaystyle}$运算的结果超出范围时,程序运算得到的输出与之并不相等。超出存储范围的更高位,在计算结果中完全“消失”了。我们将这种现象称为“溢出”。绝大多数情况下,这一现象都对程序编写是不利的,可能会导致许多超出预期的行为。因此,在编程时,一定要选取合适的变量类型存储数据。
(当然,有时我们也会利用溢出这一特性来简化很多问题。例如,当我们不关心某个巨大数字的具体值,只关心它模 $2^{32}$ 的余数的话,我们就可以使用unsigned int来存储。这么做可行的原因是很好理解的,大家可以自己分析)
