c语言中的整数
整数是编程中常用的一种数据类型,C 语言使用 int 定义整数。
int 一般占用 4 个字节「Byte」,一共 32 bit。如果不考虑符号位:
00000000 00000000 00000000 00000000
: 最小值,数字 011111111 11111111 11111111 11111111
: 最大值, 2 32 − 1 2^{32}-1 232−1
使用 4 个字节保存较小的整数绰绰有余,甚至会空闲出 2-3 个字节来,这些字节就白白浪费掉了。反过来, 2 32 − 1 2^{32}-1 232−1 虽然很大,但要表示全球人口还是不够,必须要让整数占用更多的内存,才能表示更大的值,比如 6 个字节或 8 个字节。
现在个人电脑内存都比较大了,浪费一些内存也不会带来明显的损失;而在 C 语言诞生早期,或者在单片机和嵌入式系统中,内存都是非常稀缺的资源
让整数占用更少的内存可以使用 short
,让整数占用更多的内存可以使用 long
:
short a = 10;
long n = 7000000000;
这样,a 只占用 2 个字节内存,而 m 可能占用 8 个字节内存
整型的长度
一种数据类型占用的字节数,称为该数据类型的长度。
例如:short 占用 2 个字节,那么它的长度为 2
C 语言标准并没有严格规定 short、int、long 的长度,只做了宽泛限制:
- short 至少 2 个字节
- int 建议为一个机器字长。32 位环境下为 4 个字节,64 位环境为 8 个字节
- short 长度不能大于 int,long 的长度不能小于 int
总结如下:
2 ≤ s h o r t ≤ i n t ≤ l o n g 2 \leq short \leq int \leq long 2≤short≤int≤long
一般情况下:
操作系统「32位」 | short | int | long |
---|---|---|---|
Windows | 2 | 4 | 4 |
类Unix系统 | 2 | 4 | 4 |
操作系统「64位」 | short | int | long |
---|---|---|---|
Windows | 2 | 4 | 4 |
类Unix系统 | 2 | 4 | 8 |
正负数
数学中,数字有正负之分;C 语言中也是一样,short、int、long 都可以带上正负号:
如果不带负号,默认就是正数
int a = -10;
int b = +10;
符号只有 正、负 两种情况,用 1 bit 表示即可
C 语言规定,把内存的最高位作为符号位:
- 0:表示正数
- 1: 表示负数
以 int 为例,它占用 32 位内存,最高位表示正负号,其余位表示数值位。
short、int、long 默认都是带符号位的,符号位以外存储的才是数值位。很多情况下,我们确定某个数字只能是正数,这时候符号位就是多余的了,还不如删除符号位,这样能表示的数值范围更大「大一倍」
C 语言允许我们这么做,如果不希望设置符号位,可以在数据类型前加上 unsigned
关键字:
unsigned int a = 1000;
unsigned long p = 7000000000;
所有位都用来表示数值,正数的取值范围更大了。这也意味着,不能再表示负数了!
总结:
将一个数字分为 符号 和 数值两部分:
- 加 unsigned 称为
无符号数
,只能表示正数 - 不加 unsigned 默认为
有符号数
,能表示 正数和负数
整数如何存储的
加法和减法是计算机中最基本的运算,由硬件直接支持。为了提高加减法的运算效率,硬件电路要设计得尽量简单。
目前存在两个问题:
- 有符号数,内存中同时存储符号位和数值位,运算时就不能像无符号数那样直接按位加减了;如果可以把符号位当做数值位一起参与运算,就不用设计额外的电路了
- 加法和减法可以合并为加法运算,
5 - 3
等价于5 + (-3)
如果能实现以上两个目标,只要设计一种简单的,不用区分符号位和数值位的加法电路,就能同时实现 加法和减法 运算!
实际上,在真正的计算机硬件电路中,这两个目标都实现了。然而,简化硬件电路是由代价的,这个代价就是有符号数和无符号数在存储和读取时都需要进行转化。
转化过程是怎么呢?首先需要了解以下几个概念:
正数的 原码、反码、补码 相同,只有负数才需要互相转化
- 原码:一个数的二进制形式
- 反码:负数的反码是将原码中符号位以外其它位取反
- 补码:负数的补码 = 反码+1
在计算机内存中,整数一律采用补码的形式存储
当从内存中读取整数时,需要将 补码 ⇒ 原码
,方法也很简单:
- 逆向运算
- 重复 原码⇒ 补码 步骤
补码如何简化运算
假设 6 和 18 都是 short 类型的,现在要计算 6-18 的结果,直接采用 原码 进行计算:
直接用 原码 表示整数,同时让符号位也参与运算,结果显然是不正确的。于是人们开始探索,不断试错,后来设计出了反码。下面是反码的计算过程:
这样一来,计算结果正确了。然后,这样还不算万事大吉,我们不妨将 减数 和 被减数 交换一下位置,计算一下 18 - 6 的结果:
红色的 1 是加法过程中的进位,它溢出了,直接被截掉
按照反码计算的结果是 11,与真实的结果 12 相差了 1。因此,按照反码来计算:
- 小数减大数没有问题
- 大数减小数始终少 1
相差的 1 要进行纠正,同时不能影响小数减去大数。人们绞尽脑汁又设计出了补码,给反码打一个补丁,终于把相差的 1 纠正过来了:
总结:
- 小数减去大数,先+1「原码⇒补码」,结果为负数,后-1「补码⇒原码」,正好抵消
- 大数减去小数,先+1「原码⇒补码」,结果为正数,不再减去「补码==原码」,相当于给结果多+1
补码这种天才般的设计,一举达成了上文提到的两个目标,简化了硬件电路!
取值范围
上文已经了解到,short、int、long 都只能存储有限的数值,当数值过大或过小时,超出的部分会被直接截掉,数值就不能正确存储了,这种现象称为 溢出「Overflow」
要想知道数值什么时候溢出,就要先知道各种整数类型的取值范围
-
无符号数
计算无符号数「unsigned」的取值范围很简单,将内存中所有 bit 都置为 1 就是最大值,都置为 0 则是最小值。
以
unsigned char
类型为例,它的长度为 1,占用 8 个bit:-
所有位置都是 1 时,值为 2 8 − 1 = 255 2^8-1=255 28−1=255
-
所有位置都是 0,值为 0
-
最大值计算方式
将 unsigned char 所有 bit 置为 1,内存中为
1111 1111
,直接的计算方法是:2 0 + 2 1 + 2 2 + 2 3 + 2 4 + 2 5 + 2 6 + 2 7 = 255 2^0+2^1+2^2+2^3+2^4+2^5+2^6+2^7 = 255 20+21+22+23+24+25+26+27=255
这种方法虽然直接,如果是 8 个字节的 long 类型,就非常麻烦了。
我们不妨换一种思路,先给 1111 1111 加上 1,然后再减去 1,这样一增一减正好抵消,不会影响最终值。
// +1 0b1111 1111 + 1 = 0b1 0000 0000 = 2^8 = 256 // -1 256 - 1 = 255
当内存中所有位都是 1 时,这种 凑整 的技巧非常实用
-
-
有符号数
以 char 为例,我们将补码从大到小排列:
黄色背景的一行需要单独说明,按照传统的由补码计算原码的公式,1000 0000 是无法计算的,计算反码需要 -1,此时就需要向最高位借位,而最高位是符号位,不能借出去,所以这就很矛盾!
int、long 同理
然而,与其把 1000 0000 当做无效的补码丢弃,还不如作为特殊值,这样还能多存储一个数字。于是直接规定,把这个特殊的补码表示 -128,因此我们也得到了 char 类型的取值范围为:-128~127。
-
关于零值
如果直接采用原码存储,
0000 0000
和1000 0000
将分别表示 +0 与 -0仔细看上表可以发现,char 取值范围只有一个 0 ,没有
+0
与-0
的区别,并且多存储了一个特殊值 -128,这也是采用补码小小的优势。
溢出
当数值超出取值范围时,就会发生溢出。溢出时,结果往往变得奇怪:
unsigned int a = 0x100000000;
int b = 0xffffffff;
printf("a=%u, b=%d", a, b);
// a=0, b=-1
-
a 为 unsigned int 类型,长度 4 个字节,能表示最大值为 0xFFFFFFFF,而 0x100000000 = 0xFFFFFFFF + 1,占用 33 位,超出 a 最大范围,导致最高位 1 被截断,剩下 32 位都是 0。也就是 a 存储到内存中就变成了 0
-
b 为 int 类型有符号数,最高位视为符号位,
0xffffffff
的补码为01111...1111
,占 33 位,超出 int 范围,导致最高位 0 被截断,于是内存中实际存储为1111 .... 1111
当 printf 以
%d
「有符号整型」打印 b 时,由于最高位是 1,判定为负数,随后从补码 ⇒ 原码:
类型转换
转换规则
- 小类型 ⇒ 大类型
- 无符号数:高位补 0
- 有符号数:高位补充符号位
- 大类型 ⇒ 小类型:直接截断
类型转换导致的奇怪结果
char a = 128;
printf("%u", a); // 4294967168
解析:
-
128 的补码为:
00000000 00000000 00000000 10000000
-
存储到 char 中,发生截断,实际存储为:
10000000
注意:这里把符号位截断了,数据位 1 变成了新的符号位
-
以
%u
打印,相当于转换为unsigned int
-
a 为有符号数,高位补充符号位,提升后:
11111111 11111111 11111111 10000000
-
无符号数原码、补码相同,因此结果为
4294967168
自动类型转换
编译器隐式的,自动完成的数据类型转换
-
将一种数据类型赋值给另一种类型时会自动发生类型转换
float f = 100; printf("%f", f); // 100.000000 int a = 3.14; printf("%d", a); // 3
-
在不同类型混合运算中,编译器会将参与运算的各方转换为同一数据类型,然后再运算:
转换按照数据长度增加的方向进行,以保证数据不失真
- int、long 参与运算时,先把 int 转换为 long 再运算
- char、short 参与运算时,必须先转换为 int
- 浮点运算都是以双精度进行「即使运算双方只有 float,也要先转换为 double」
整型提升
整型的运算会在 CPU 内执行,CPU内整型运算器「ALU」的操作数的字节长度一般就是 int 的字节长度,同时也是 CPU 通用寄存器的长度。
通用 CPU「general-purpose cpu」难以直接实现两个 8bit 长度数据相加运算「虽然机器指令有这种字节相加运算」。
于是 C 语言标准规定:占用内存小于 int 的类型「 short,char」,在运算时首先需要提升为 int 类型,如果 int 类型不足以表示的话,就提升为 unsigned int,然后再执行表达式的运算
注意:发生的类型转换只是为了本次运算而进行的临时性转换,转换的结果会保存到临时的内存空间,不会影响数据本身的类型或值
char a = 10;
// 类型提升为 int,占用 4 个字节
printf("%zu", sizeof(+a)); // 4
char a = 30, b = 40, c = 10;
// a * b = 1200, 似乎 char 溢出
// 但由于整型提升的存在,仍得到了正确的结果
char d = a * b / c;
printf("%d", d); // 120