C语言:整数取值范转及溢出
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 0000和1000 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 的长度是有限的,当数值过大或者过小时,有限的几个字节就不能表示了,就会发生溢出。发生溢出时,输出结果往往会变得奇怪,请看下面的代码:
- #include <stdio.h>
- int main()
- {
- unsigned int a = 0x100000000;
- int b = 0xffffffff;
- printf("a=%u, b=%d\n", a, b);
- return 0;
- }
运行结果:
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语言:整数取值范转及溢出的更多相关文章
- 从C语言的整数取值范围说开去
在ILP32中, char, short, int, long, long long, pointer分别占1, 2, 4, 4, 8, 4个字节,在 LP64中, char, short, int, ...
- C语言数据类型取值范围
一.获取数据类型在系统中的位数 在不同的系统中,数据类型的字节数(bytes)不同,位数(bits)也有所不同,那么对应的取值范围也就有了很大的不同,那我们怎么知道你当前的系统中C语言的某个数据类型的 ...
- 编写一个js函数,该函数有一个n(数字类型),其返回值是一个数组,该数组内是n个随机且不重复的整数,且整数取值范围是[2,32]
首先定义个fn用来返回整数的取值范围: function getRand(a,b){ var rand = Math.ceil(Math.random()*(b-a)+a); return rand; ...
- C语言数据类型取值范围解析
版权声明:本文为博主原创文章,未经博主允许不得转载. 为什么int类型的取值范围会是-2^31 ~ 2^31-1 ,为什么要减一呢? 计算机里规定,8位二进制为一个字节,拿byte来说,一个BY ...
- 编写一个javscript函数 fn,该函数有一个参数 n(数字类型),其返回值是一个数组,该数组内是 n 个随机且不重复的整数,且整数取值范围是 [2, 32]。
function fn(n){ if(n<2 || n>32) { return; } if(!n) { return;} //判断n是否为数字 if(!/^[0-9]+.?[0-9 ...
- 带符号的char类型取值范围为什么是-128——127
以前经常看到带符号的char类型取值范围是-128——127,今天突然想为什么不是-127——127,-128是怎么来的? 127好理解,char类型是8位,最高位是符号位,0正1负,所以011111 ...
- GO语言学习笔记2-int类型的取值范围
相比于C/C++语言的int类型,GO语言提供了多种int类型可供选择,有int8.int16.int32.int64.int.uint8.uint16.uint32.uint64.uint. 1.i ...
- c语言基础表达式, 关系运算符, 逻辑运算符, 位运算符, 数据的取值范围, 分支结构(if...else, switch...case)
1.表达式: 表达式的判断是有无结果(值), 最简单的表达式是一个常量或变量, 如:12, a, 3 + 1, a + b, a + 5 都是表达式 2.BOOL(布尔)数据类型: c语言中除了基本数 ...
- C语言中数据类型取值范围的计算的理解与总结
c语言中,数据类型有short,int,long,char,float,double,然后除了浮点型只有 有符号数(signed)外,其他的数据类型都分为有符号(signed)和无符号(unsigne ...
随机推荐
- Go语言网络通信---string与int互转,int64与[]byte互转,int直接互转,string与[]byte互转
string与int互转 #string到int int,err:=strconv.Atoi(string) #string到int64 int64, err := strconv.ParseInt( ...
- Python+Selenium学习笔记4 - submit&get_attribute
1.submit() submit()方法用于提交表单.如在搜索框输入关键字后按回车键进行查询操作,就可用submit()方法模拟.若不能按回车键进入下一步,则不能用submit()举例,百度的查询提 ...
- Proteus仿真MSP430单片机的若干问题记录
1.支持的具体型号: P7.8: Proteus8.9: Proteus8.9能够支持的类型明显要多于Proteus7.8.但是对于仿真而言,目前个人还是觉得Proteus7.8更稳定.这也是目前能用 ...
- YOLO、SSD、FPN、Mask-RCNN检测模型对比
YOLO.SSD.FPN.Mask-RCNN检测模型对比 一.YOLO(you only look once) YOLO 属于回归系列的目标检测方法,与滑窗和后续区域划分的检测方法不同,他把检测任务当 ...
- springmvc——自定义类型转换器
一.什么是springmvc类型转换器? 在我们的ssm框架中,前端传递过来的参数都是字符串,在controller层接收参数的时候springmvc能够帮我们将大部分字符串类型的参数自动转换为我们指 ...
- 既然有 HTTP 请求,为什么还要用 RPC 调用?
首先,实名赞扬题主的问题.这个问题非常好. 其次,实名反对各个上来就讲RPC好而HTTP不好的答案.因为,题主的观点非常对. HTTP协议,以其中的Restful规范为代表,其优势很大.它可读性好,且 ...
- 解决SpringMVC重复提交的问题
方法一:通过重定向采取请求转发的方式完成表单内容的添加会造成内容的重复插入.当向Servlet发送一条增加记录的请求后,servlet首先向数据库增加一条记录,然后又从数据库中查询出所有数据,接着转发 ...
- 解析 Nebula Graph 子图设计及实践
本文首发于 Nebula Graph 公众号 NebulaGraphCommunity,Follow 看大厂图数据库技术实践. 前言 在先前的 Query Engine 源码解析中,我们介绍了 2.0 ...
- Dagger2入门,以初学者角度
2016-12-21 更新:添加@Subcomponent注解以及Lazy与Provider的使用,本文基本完结!如果有好的建议请提出,感谢大家的支持,谢谢 依赖注入 Dagger2是Android中 ...
- Unity 添加,修改默认创建脚本模板
Unity 默认创建的脚本可以添加也可以修改,不需要修改Editor. 一.找到模板目录 \Editor\Data\Resources\ScriptTemplates 二.如果要修改模板,直接打开修改 ...