short、int、long 是C语言中常用的三种整数类型,分别称为短整型、整型、长整型。

在现代操作系统中,short、int、long 的长度分别是 2、4、4 或者 8,它们只能存储有限的数值,当数值过大或者过小时,超出的部分会被直接截掉,数值就不能正确存储了,我们将这种现象称为溢出(Overflow)。

溢出的简单理解就是,向木桶里面倒入了过量的水,木桶盛不了了,水就流出来了。

要想知道数值什么时候溢出,就得先知道各种整数类型的取值范围。

无符号数的取值范围

计算无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。

以 unsigned char 类型为例,它的长度是 1,占用 8 位的内存,所有位都置为 1 时,它的值为 28 - 1 = 255,所有位都置为 0 时,它的值很显然为 0。由此可得,unsigned char 类型的取值范围是 0~255。

前面我们讲到,char 是一个字符类型,是用来存放字符的,但是它同时也是一个整数类型,也可以用来存放整数,请大家暂时先记住这一点

有读者可能会对 unsigned char 的最大值有疑问,究竟是怎么计算出来的呢?下面我就讲解一下这个小技巧。

将 unsigned char 的所有位都置为 1,它在内存中的表示形式为1111 1111,最直接的计算方法就是:

20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 = 255

这种“按部就班”的计算方法虽然有效,但是比较麻烦,如果是 8 个字节的 long 类型,那足够你计算半个小时的了。

我们不妨换一种思路,先给 1111 1111 加上 1,然后再减去 1,这样一增一减正好抵消掉,不会影响最终的值。

给 1111 1111 加上 1 的计算过程为:

0B1111 1111 + 0B1 = 0B1 0000 0000 = 28 = 256

可以发现,1111 1111 加上 1  后需要向前进位(向第 9 位进位),剩下的 8 位都变成了 0,这样一来,只有第 9 位会影响到数值的计算,剩下的 8 位对数值都没有影响。第 9 位的权值计算起来非常容易,就是:

29-1 = 28 = 256

然后再减去 1:

28 - 1 = 256 - 1 = 255

加上 1 是为了便于计算,减去 1 是为了还原本来的值;当内存中所有的位都是 1 时,这种“凑整”的技巧非常实用。

按照这种巧妙的方法,我们可以很容易地计算出所有无符号数的取值范围(括号内为假设的长度):

  unsigned char unsigned short unsigned int(4字节) unsigned long(8字节)
最小值 0 0 0 0
最大值 28 - 1 = 255 216 - 1 = 65,535 ≈ 6.5万 232 - 1 = 4,294,967,295 ≈ 42亿 264 - 1 ≈ 1.84×1019

有符号数的取值范围

有符号数以补码的形式存储,计算取值范围也要从补码入手。我们以 char 类型为例,从下表中找出它的取值范围:

补码 反码 原码
1111 1111 1111 1110 1000 0001 -1
1111 1110 1111 1101 1000 0010 -2
1111 1101 1111 1100 1000 0011 -3
…… …… …… ……
1000 0011 1000 0010 1111 1101 -125
1000 0010 1000 0001 1111 1110 -126
1000 0001 1000 0000 1111 1111 -127
1000 0000 -- -- -128
0111 1111 0111 1111 0111 1111 127
0111 1110 0111 1110 0111 1110 126
0111 1101 0111 1101 0111 1101 125
…… …… …… ……
0000 0010 0000 0010 0000 0010 2
0000 0001 0000 0001 0000 0001 1
0000 0000 0000 0000 0000 0000 0

我们按照从大到小的顺序将补码罗列出来,很容易发现最大值和最小值。

淡黄色背景的那一行是我要重点说明的。如果按照传统的由补码计算原码的方法,那么 1000 0000 是无法计算的,因为计算反码时要减去 1,1000 0000 需要向高位借位,而高位是符号位,不能借出去,所以这就很矛盾。

是不是该把 1000 0000 作为无效的补码直接丢弃呢?然而,作为无效值就不如作为特殊值,这样还能多存储一个数字。计算机规定,1000 0000 这个特殊的补码就表示 -128。

为什么偏偏是 -128 而不是其它的数字呢?

首先,-128 使得 char 类型的取值范围保持连贯,中间没有“空隙”。

其次,我们再按照“传统”的方法计算一下 -128 的补码:

  • -128 的数值位的原码是 1000 0000,共八位,而 char 的数值位只有七位,所以最高位的 1 会覆盖符号位,数值位剩下 000 0000。最终,-128 的原码为 1000 0000。
  • 接着很容易计算出反码,为 1111 1111。
  • 反码转换为补码时,数值位要加上 1,变为 1000 0000,而 char 的数值位只有七位,所以最高位的 1 会再次覆盖符号位,数值位剩下 000 0000。最终求得的 -128 的补码是 1000 0000。

-128 从原码转换到补码的过程中,符号位被 1 覆盖了两次,而负数的符号位本来就是 1,被 1 覆盖多少次也不会影响到数字的符号。

你看,虽然从 1000 0000 这个补码推算不出 -128,但是从 -128 却能推算出 1000 0000 这个补码,这么多么的奇妙,-128 这个特殊值选得恰到好处。

负数在存储之前要先转换为补码,“从 -128 推算出补码 1000 0000”这一点非常重要,这意味着 -128 能够正确地转换为补码,或者说能够正确的存储。

关于零值和最小值

仔细观察上表可以发现,在 char 的取值范围内只有一个零值,没有+0-0的区别,并且多存储了一个特殊值,就是 -128,这也是采用补码的另外两个小小的优势。

如果直接采用原码存储,那么0000 00001000 0000将分别表示+0-0,这样在取值范围内就存在两个相同的值,多此一举。另外,虽然最大值没有变,仍然是 127,但是最小值却变了,只能存储到 -127,不能存储 -128 了,因为 -128 的原码为 1000 0000,这个位置已经被-0占用了。

按照上面的方法,我们可以计算出所有有符号数的取值范围(括号内为假设的长度):

  char short int(4个字节) long(8个字节)
最小值 -27 = -128 -215 = -32,768 ≈ -3.2万 -231 = -2,147,483,648 ≈ -21亿 -263 ≈ -9.22×1018
最大值 27 - 1= 127 215 - 1 = 32,767 ≈ 3.2万 231 - 1 = 2,147,483,647 ≈ 21亿 263 - 1≈ 9.22×1018

上节我们还留下了一个疑问,[1000 0000 …… 0000 0000]补这个 int 类型的补码为什么对应的数值是 -231,有了本节对 char 类型的分析,相信聪明的你会举一反三,自己解开这个谜团。

数值溢出

char、short、int、long 的长度是有限的,当数值过大或者过小时,有限的几个字节就不能表示了,就会发生溢出。发生溢出时,输出结果往往会变得奇怪,请看下面的代码:

  1. #include <stdio.h>
  2. int main()
  3. {
  4. unsigned int a = 0x100000000;
  5. int b = 0xffffffff;
  6. printf("a=%u, b=%d\n", a, b);
  7. return 0;
  8. }

运行结果:
a=0, b=-1

变量 a 为 unsigned int 类型,长度为 4 个字节,能表示的最大值为 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,占用33位,已超出 a 所能表示的最大值,所以发生了溢出,导致最高位的 1 被截去,剩下的 32 位都是0。也就是说,a 被存储到内存后就变成了 0,printf 从内存中读取到的也是 0。

变量 b 是 int 类型的有符号数,在内存中以补码的形式存储。0xffffffff 的数值位的原码为 1111 1111 …… 1111 1111,共 32 位,而 int 类型的数值位只有 31 位,所以最高位的 1 会覆盖符号位,数值位只留下 31 个 1,所以 b 的原码为:

1111 1111 …… 1111 1111

这也是 b 在内存中的存储形式。

当 printf 读取到 b 时,由于最高位是 1,所以会被判定为负数,要从补码转换为原码:

[1111 1111 …… 1111 1111]
= [1111 1111 …… 1111 1110]
= [1000 0000 …… 0000 0001]
= -1

最终 b 的输出结果为 -1。

C语言:整数取值范转及溢出的更多相关文章

  1. 从C语言的整数取值范围说开去

    在ILP32中, char, short, int, long, long long, pointer分别占1, 2, 4, 4, 8, 4个字节,在 LP64中, char, short, int, ...

  2. C语言数据类型取值范围

    一.获取数据类型在系统中的位数 在不同的系统中,数据类型的字节数(bytes)不同,位数(bits)也有所不同,那么对应的取值范围也就有了很大的不同,那我们怎么知道你当前的系统中C语言的某个数据类型的 ...

  3. 编写一个js函数,该函数有一个n(数字类型),其返回值是一个数组,该数组内是n个随机且不重复的整数,且整数取值范围是[2,32]

    首先定义个fn用来返回整数的取值范围: function getRand(a,b){ var rand = Math.ceil(Math.random()*(b-a)+a); return rand; ...

  4. C语言数据类型取值范围解析

    版权声明:本文为博主原创文章,未经博主允许不得转载.   为什么int类型的取值范围会是-2^31 ~ 2^31-1  ,为什么要减一呢? 计算机里规定,8位二进制为一个字节,拿byte来说,一个BY ...

  5. 编写一个javscript函数 fn,该函数有一个参数 n(数字类型),其返回值是一个数组,该数组内是 n 个随机且不重复的整数,且整数取值范围是 [2, 32]。

    function fn(n){ if(n<2 || n>32) { return; }  if(!n) { return;}  //判断n是否为数字  if(!/^[0-9]+.?[0-9 ...

  6. 带符号的char类型取值范围为什么是-128——127

    以前经常看到带符号的char类型取值范围是-128——127,今天突然想为什么不是-127——127,-128是怎么来的? 127好理解,char类型是8位,最高位是符号位,0正1负,所以011111 ...

  7. GO语言学习笔记2-int类型的取值范围

    相比于C/C++语言的int类型,GO语言提供了多种int类型可供选择,有int8.int16.int32.int64.int.uint8.uint16.uint32.uint64.uint. 1.i ...

  8. c语言基础表达式, 关系运算符, 逻辑运算符, 位运算符, 数据的取值范围, 分支结构(if...else, switch...case)

    1.表达式: 表达式的判断是有无结果(值), 最简单的表达式是一个常量或变量, 如:12, a, 3 + 1, a + b, a + 5 都是表达式 2.BOOL(布尔)数据类型: c语言中除了基本数 ...

  9. C语言中数据类型取值范围的计算的理解与总结

    c语言中,数据类型有short,int,long,char,float,double,然后除了浮点型只有 有符号数(signed)外,其他的数据类型都分为有符号(signed)和无符号(unsigne ...

随机推荐

  1. node.js学习(7)流和管道

    1 导入模块 输入流 # 读取流 # 写入流 # # 管道 # 压缩 # 解压缩

  2. SLAM图优化g2o

    SLAM图优化g2o 图优化g2o框架 图优化的英文是 graph optimization 或者 graph-based optimization, "图"其实是数据结构中的gr ...

  3. deeplearning模型库

    deeplearning模型库 1. 图像分类 数据集:ImageNet1000类 1.1  量化 分类模型Lite时延(ms) 设备 模型类型 压缩策略 armv7 Thread 1 armv7 T ...

  4. MindSpore平台系统类

    MindSpore平台系统类 Q:MindSpore只能在华为自己的NPU上跑么? A: MindSpore同时支持华为自己的Ascend NPU.GPU与CPU,是支持异构算力的. Q:MindSp ...

  5. MindSpore基准性能

    MindSpore基准性能 本文介绍MindSpore的基准性能.MindSpore网络定义可参考Model Zoo. 训练性能 ResNet 以上数据基于华为云AI开发平台ModelArts测试获得 ...

  6. MinkowskiEngine基准测试

    MinkowskiEngine基准测试 介绍卷积层和小型U网络的前馈和后馈通过时间.可以将具有相同张量步幅,步幅和内核偏移的内核映射重新用于其他层,可以在大型nueral网络中使用的所有层上,分摊此页 ...

  7. CVPR2020:基于层次折叠的跳跃式注意网络点云完成

    CVPR2020:基于层次折叠的跳跃式注意网络点云完成 Point Cloud Completion by Skip-Attention Network With Hierarchical Foldi ...

  8. 如何实现一个简易版的 Spring - 如何实现 AOP(终结篇)

    前言 在 上篇 实现了 判断一个类的方式是符合配置的 pointcut 表达式.根据一个 Bean 的名称和方法名,获取 Method 对象.实现了 BeforeAdvice.AfterReturni ...

  9. 137. 只出现一次的数字 II

    2021-04-30 LeetCode每日一题 链接:https://leetcode-cn.com/problems/single-number-ii/ 方法1:使用map记录每个数出现的次数,再找 ...

  10. C# 扫描并读取图片中的文字

    本文介绍如何通过C# 程序来扫描并读取图片中的文字,这里以创建一个.Net Core程序为例.下面是具体步骤,供参考. 程序测试环境: Visual Studio版本要求不低于2017 图片扫描工具: ...