物理 [栖岸计划]智英带你学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来存储。这么做可行的原因是很好理解的,大家可以自己分析)
接下来的内容是关于整型变量的运算。我们知道,我们可以对整数进行加法、减法、乘法。计算结果与顺序与数学上的计算相同。对于这些计算,需要注意以下几点:
①溢出。开始已经提到过了,不再赘述。
②隐式变量类型转换。为了让不同类型的整型变量也能进行运算,在运算过程中,变量的类型会发生转换。对于仅限于整型变量之内的运算,遵循以下规则:
1)将存储范围小于int的类型(如short,unsigned short,char,unsigned char)提升为int;
对于这一点,我们看一个例子:

分析一下,为什么结构完全一样,但只有第一句话被打印?
先解释下面为什么不被打印。因为我们将c赋值为INT_MAX,将它加上1之后,发生了溢出,导致返回值小于0。因此第二句话没有被打印。
问题来了:照理来讲,同样的结构,同样的运算,a+b也应该发生溢出,为什么第一句话被打印了?
答案:因为计算a+b时,实际进行的步骤如下:
先将a提升为int型变量,即将式子中的a替换为int类型的0x0000007f,而非char类型的0x7f;
再将b提升为int型变量,即将式中的b替换为int类型的0x00000001;
最终相加,得到的结果是0x00000080,即128。这当然是个正数,因此第一句话被打印。
在参与运算的所有数都是整数时,这一类型转换的优先级最高。
2)如果提升后两变量类型不同,将较低级的类型转为较高级的类型。具体准则如下:
如果均为有符号整型,那么long long>long>int;
如果均为无符号整型,那么unsigned long long>unsigned long>unsigned;
如果有有符号整型,有无符号整型,若有符号整型覆盖了无符号整型的所有数,则有符号整型更高级,否则无符号整型更高级。
这套规则会有如下结果(需要考虑long和unsigned long是32位还是64位):
若long和unsigned long为32位:unsigned long long>long long>unsigned long>unsigned>long>int;
若为64位:unsigned long long>unsigned long>long long>long>unsigned>int。
我们举一个例子:


这个程序在Windows系统(long为32位)下面会有输出,但在Linux系统(long为64位)下不会。
因为long为32位的话,long无法覆盖unsigned的所有取值范围(不小于 $2^{31}$ ,不大于 $2^{32}-1$ 的数无法被long覆盖)。由前面的规则,long被提升为unsigned,转换为一个很大的整数,一般是 $2^{32}-20$ 。这个数加上10之后,不会发生溢出,从而和为正数,打印语句执行。
但long为64位的话,long可以覆盖unsigned的全部取值范围,由前面的规则,unsigned被提升为long,结果只是普通的-20+10=-10,当然小于0,什么也不发生。