张银峰的编程课堂

数值范围与溢出

类型使数据有了空间边界,比如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;
}

glimix.com

有符号数

有符号数使用 “符号—数值” 数制表达,一个数是由表示该数为正或负的符号位和数值两部分组成。“符号—数值” 数制应用于二进制数时,需用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),二者一致。

glimix.com

我们再来看一些示例,它们在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;
}

glimix.com

溢出

对于有符号或无符号数,如果加法操作产生的结果超出了数制定义的范围,就说发生了溢出。两个异号数相加绝不会溢出,但是两个同号数相加有可能溢出。

#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;
}

glimix.com

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

glimix.com

#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号资源全部被删除了!