数值范围与溢出
类型使数据有了空间边界,比如short占用2个字节,int占据4个字节;这表明了不同的数据类型可表达的数据范围是有界的,数值类型有有符号与无符号之分,因此我们从这两个方面分开讨论。
无符号数
计算机对于整数是按补码存储的,无符号数的补码就是自身,因此可以直接存储,直接表示不会对原数值进行任何处理。
对于1个字节的unsigned char:
- 将1字节的所有bit位置为0,二进制表示为00000000,十进制值为0,这表示1字节可表达最小值是0。
- 将1字节的所有bit位置为1,二进制表示为11111111,十进制值为255,这表示1个字节可以存储的最大值是255。
对于2个字节的unsigned short:
- 将16位全部置为0,即00000000 00000000,十进制值为0,这表示2字节可表达最小值是0。
- 将16位全部置为1,即11111111 11111111,十进制值为0,这表示2字节可表达最大值是65535。
对于4字节unsigned int:
- 最小值是0
- 最大值是4294967295
即对于无符号数,最小值是0,最大值为2n-1,n是数据类型的位数。我们可以从limits.h中查询有限的数值范围。
#include <stdio.h>
#include <limits.h>
int main()
{
printf("type | max-value\n");
printf("---------------+-------------------------\n");
printf("unsigned char | %u\n", UCHAR_MAX);
printf("unsigned int | %u\n", UINT_MAX);
printf("unsigned long | %u\n", ULONG_MAX);
return 0;
}

有符号数
有符号数使用 “符号—数值” 数制表达,一个数是由表示该数为正或负的符号位和数值两部分组成。“符号—数值” 数制应用于二进制数时,需用1个附加位来表示符号,称做符号位(sign bit)。一般而言,用位串的的最高有效位(MSB)表示符号位(0=正,1=负),其余较低位表示数值。比如:
01010101 = +85 11010101 = -85
00000000 = +0 10000000 = -0
在 “符号—数值” 数制下,
- 具有相同数目的正整数和负整数。
- 零有两种可能的表示,即+0和-0。
- 一个n位整数可表达的范围是-(2n-1-1) ~ +(2n-1-1)。
“符号—数值” 数制通过改变其符号将一个数变为负数,而补码数制将一个数变负的方法是按照数制的定义求其补码。我们把 “符号—数值” 表示的数值可称为基数,对二进制数制而言,基数补码称做二进制补码。大多数计算机采用二进制补码数制来表示负数。其计算方法是对各位取反后加一。
我们来看看-127在程序中的表现。在内存视图中,-127的16进制值为81(10000001),我们对基数+127(01111111),求补码(取反加一)得(10000000 + 1),二者一致。

我们再来看一些示例,它们在8比特位上,分别在正数与负数上执行补码操作。数值17是一个正常操作;对于0的补码产生了进位,在所有的二进制补码操作中,要忽略符号位产生的进位,只利用余下的n位结果。可以看出,在二进制补码数制中,0属于正数,因为其符号位为0。对-127求补码,得到了基数127;对-128求补码,以无符号数解释得到了基数128;以有符号数解释得到了-128。
17 = 00010001 -127 = 10000001
11101110 01111110 -> 取反
+1 +1 -> +1
------------------- ---------------------
11101111 = -17 01111111 = 127
0 = 00000000 -128 = 10000000
11111111 01111111 -> 取反
+1 +1 -> +1
------------------- ---------------------
1 00000000 = 0 10000000 = 128
在二进制补码中0现在仅有一种表示方法,不需要-0与之对应,因此多出了一个位序列,用于表达最小的负数,即一个n位数的最小值是-2n-1,而补码表示的最大正数为2n-1-1,故而对于有符号数:
- 1字节数值,如signed char,值范围是[-128, 127]。
- 2字节数值,如short,值范围是[-32768, 32767]。
- 4字节数值,如int, long int, 值范围是[-2147483648, 2147483647]。
#include <stdio.h>
#include <limits.h>
int main()
{
printf("type | %11s %11s\n", "min", "max");
printf("------------+-------------------------\n");
printf("signed char | %11d %11d\n", SCHAR_MIN, SCHAR_MAX);
printf(" int | %11d %11d\n", INT_MIN, INT_MAX);
printf("long int | %11d %11d\n", LONG_MIN, LONG_MAX);
return 0;
}

溢出
对于有符号或无符号数,如果加法操作产生的结果超出了数制定义的范围,就说发生了溢出。两个异号数相加绝不会溢出,但是两个同号数相加有可能溢出。
#include <stdio.h>
#include <limits.h>
int main()
{
char c = SCHAR_MAX;
printf("[signed char]: Positive -> Negative\n");
printf("======================================\n");
printf("%d\n", c);
++c; printf("%d\n", c);
++c; printf("%d\n", c);
++c; printf("%d\n", c);
c = -SCHAR_MIN;
printf("\n[signed char]: Negative -> Positive\n");
printf("======================================\n");
printf("%d\n", c);
--c; printf("%d\n", c);
--c; printf("%d\n", c);
--c; printf("%d\n", c);
printf("\n[unsigned char]\n");
printf("======================================\n");
unsigned char uc = 255;
printf("%d\n", uc);
printf("%d\n", ++uc);
return 0;
}

图例展示了signed char在两个边界值溢出的情形。你可以在脑海中构造一个数轴,左端负数的尽头是最小值-128,右端正数的尽头是最大值127,然后将数轴两个边界连接起来围成一个圈;数值0在正北方,正数代表顺时针累进加一,负数代表逆时针顺序减一。当数值是+127时,再累进+1,就变成了-128;反之当数值是-128时,再继续-1,就跑到了+127的位置了;这种边界溢出极有可能会造成程序潜在的BUG。

#include <stdio.h>
unsigned char get_res_cnt()
{
return 2;
}
void destroy_res(unsigned char id)
{
printf("kills %u\n", id);
}
int main()
{
for (unsigned char i = get_res_cnt() - 1; i >= 0; i--)
{
destroy_res(i);
}
return 0;
}
程序的意图是倒序删除指定索引的资源,当i为0时,减1操作导致i溢出到unsigned char的最大值255,最终使得0-255号资源全部被删除了!
陕公网安备61011202001108号