前言
本篇文章将详细介绍计算机中的整数,主要包括以下几个方面:
- 整数在内存中的存储方式
- 不同整数的编码方式
- 不同编码方式之间的转换
- 不同类型整数的相互转换
- 整数之间的运算
整数在内存中的存储方式
现代计算机的内存种类多种多样,我们抛开不同种类内存在物理结构和使用方式上的不同,从宏观上看,所有的内存都是为了存储数据的,并且所有内存的最小单位都是位(Bit),所以我们使用到的内存其实是下面这样的一个图,下图只代表一大块内存的一小部分
大多数计算机使用8位的块,或者字节作为最小可寻址的内存单元,而不是访问内存中单独的位,也就是我们在程序中能看到的无论是指针还是数据偏移都是以字节为单位的
那我如果想在内存中表示一个整数应该怎么表示呢?
有两种表示方法:
大端法
小端法
但是在此之前呢,我们先熟悉一下整数的类型
我们在写代码的过程中,整数是用的最多的一种变量类型,整数的分类按照内存占用大小,大体可以分为short
int
和long
等几种类型,按照内存编码方式可以分为无符号整数
和有符号整数
,既然为了探究计算机中整数的原理,我们不妨以short和int的无符号编码方式和有符号编码方式来讲解,为此,我们做出如下假设:
- short:内存占用两个字节
- int:内存占用四个字节
接下来,以int无符号来讲解大端法和小端法,先给出定义:
- 大端法:在内存中的排列方式按照从最高有效字节到最低有效字节依次排列的方法叫做大端法
- 小端法:在内存中的排列方式按照从最低有效字节到最高有效字节依次排列的方法叫做小端法
注意是字节的排列顺序,不是位的排列顺序
比如short a = 100;
它的二进制表示为0000000001100100
,它占用内存的两个字节,所以在内存中的表示为:
大端法:
小端法:
注意:大多数intel兼容机都使用小端模式,android和ios也是使用小端模式
不同整数的编码方式
整数的编码方式有两种:
- 无符号编码
- 有符号编码
对于一个内存中的整数
a
a
a,在内存中占用
w
w
w位,我们可以这样来表示当前整数:
a
=
[
a
w
−
1
,
a
w
−
2
,
…
,
a
1
,
a
0
]
a=[a_{w-1},a_{w-2},\ldots,a_1,a_0]
a=[aw−1,aw−2,…,a1,a0]
如果
a
a
a是一个无符号整数,那么
a
=
∑
i
=
0
w
−
1
(
a
i
2
i
)
=
a
0
×
2
0
+
a
1
×
2
1
+
…
+
a
w
−
2
×
2
w
−
2
+
a
w
−
1
×
2
w
−
1
a=\sum_{i=0}^{w-1}(a_i2^i) = a_0\times2^0+a_1\times2^1+\ldots+a_{w-2}\times2^{w-2}+a_{w-1}\times2^{w-1}
a=i=0∑w−1(ai2i)=a0×20+a1×21+…+aw−2×2w−2+aw−1×2w−1
如果
a
a
a是一个有符号整数,大部分计算机采用补码的方式进行编码,那么
a
=
−
a
w
−
1
×
2
w
−
1
+
∑
i
=
0
w
−
2
(
a
i
2
i
)
a=-a_{w-1}\times2^{w-1} + \sum_{i=0}^{w-2}(a_i2^i)
a=−aw−1×2w−1+i=0∑w−2(ai2i)
接下来我们用例子来说明一下:
无符号
比如定义一个无符号short整数的二进制表示为unsigned short a = 0b0000000001010101
那么a的十进制值为
a = 1 × 2 0 + 1 × 2 2 + 1 × 2 4 + 1 × 2 6 = 1 + 4 + 16 + 64 = 85 a=1\times2^0+1\times2^2+1\times2^4+1\times2^6=1+4+16+64=85 a=1×20+1×22+1×24+1×26=1+4+16+64=85
相反,如果我们定义一个无符号short整数unsigned short a = 100
那么它在内存中的表示就应该是100的二进制表示,也就是
a = 0000000001100100 a=0000000001100100 a=0000000001100100
有符号
比如定义一个有符号short整数的二进制表示为signed short a = 0b1000000001010101
那么a的十进制值为
a = − 1 × 2 15 + 1 × 2 0 + 1 × 2 2 + 1 × 2 4 + 1 × 2 6 = − 32768 + 1 + 4 + 16 + 64 = − 32683 a=-1\times2^{15}+ 1\times2^0+1\times2^2+1\times2^4+1\times2^6=-32768+1+4+16+64=-32683 a=−1×215+1×20+1×22+1×24+1×26=−32768+1+4+16+64=−32683
相反,如果我们定义一个有符号short整数signed short a = -100
那么它在内存中的表示就应该是-100的二进制补码表示,补码表示的方法为
补码 = { 原码 , 如果是正数 符号位保持不变,其余位全部取反后+1 , 如果是负数 \text{补码}= \begin{cases} \text{原码},&\text{如果是正数} \\ \text{符号位保持不变,其余位全部取反后+1},&\text{如果是负数} \\ \end{cases} 补码={原码,符号位保持不变,其余位全部取反后+1,如果是正数如果是负数
所以 -100的原码表示为 1000000001100100 1000000001100100 1000000001100100,因为是负数,除符号位所有位取反+1
所以 -100的补码表示位 1111111110011100 1111111110011100 1111111110011100
不同编码方式之间的转换
C语言允许在各种不同的数字数据类型之间做强制类型转换,例如,假设声明一下变量
signed int a;
unsigned int b;
那么(unsigned)a
就会将a强制转换成无符号整数
而 (signed)b
则会将b强制转换为有符号整数
那么,计算机进行不同编码之间的转换规则是啥呢,一句话总结就是:
位模式不变,只改变数值
也就是说,转换请求不改变数值在内存中原本的位值,只是改变了解释方式而已,看下面的例子:
有符号整数转无符号整数
// 有符号整数转无符号整数
signed short a =-12345;
unsigned short b = (unsigned short)a;
printf("a = %d, b = %u\n",a,b);
运行结果位:
a = -12345, b = 53191
首先我们先获取a的内存中的二进制表示
- a的原码表示为
1011000000111001
- a的补码表示为
1100111111000111
然后转换为无符号的意思就是将当前补码按照无符号的原码翻译,即把1100111111000111
当成一个无符号整数,根据上面的公式我们可以算出1100111111000111=53191
下面我们推导一下有符号整数转无符号整数的公式
- 当有符号整数>=0时,转换后的值和转换前一致,因为不涉及补码的问题
- 当有符号整数<0时,转换后的值表示为
b
=
2
15
+
m
b=2^{15} + m
b=215+m
其中m为a的补码除了符号为其他位的值,该值是由a的原码除了符号位(也就是12345的原码,我们假设定义为a1)
取反+1获取到的。也就是 m = a 1 取反 + 1 m = a1取反+1 m=a1取反+1,一个很好证明的问题是 a 1 + a 1 取反 + 1 = 2 15 a1+a1取反+1=2^{15} a1+a1取反+1=215
⇒ \Rightarrow ⇒ m + a 1 = 2 15 m+a1=2^{15} m+a1=215
⇒ \Rightarrow ⇒ b = 2 15 + 2 15 − a 1 b=2^{15}+2^{15}-a1 b=215+215−a1
⇒ \Rightarrow ⇒ b = 2 16 + a b=2^{16}+a b=216+a
所以,有符号整数转无符号整数的公式可以表示为:
b
=
{
a
,
a
>
=
0
2
w
+
a
,
a
<
0
,
w
为
a
的字节数
b= \begin{cases} a,&a>=0\\ 2^w+a,&a<0,w为a的字节数 \end{cases}
b={a,2w+a,a>=0a<0,w为a的字节数
无符号整数转有符号整数
// 无符号整数转有符号整数
unsigned short a =53191;
signed short b = (signed short)a;
printf("a = %d, b = %u\n",a,b);
运行结果为:
a = 53191, b = -12345
首先我们先获取a的内存中的二进制表示1100111111000111
我们直接按照补码公式求值即可获得b的值
下面我们推导一下无符号整数转有符号整数的公式,我们先拷贝一下上面提到的补码求值公式
a
=
−
a
w
−
1
×
2
w
−
1
+
∑
i
=
0
w
−
2
(
a
i
2
i
)
a=-a_{w-1}\times2^{w-1} + \sum_{i=0}^{w-2}(a_i2^i)
a=−aw−1×2w−1+i=0∑w−2(ai2i)
可以看到,当符号位为0的时候,其实该公式和无符号整数求值公式是一样的
当符号位为1的时候,
b
=
−
2
15
+
m
b=-2^{15}+m
b=−215+m,而
a
=
2
15
+
m
a=2^{15}+m
a=215+m
⇒
\Rightarrow
⇒
b
=
−
2
15
−
2
15
+
a
b=-2^{15}-2^{15}+a
b=−215−215+a
⇒
\Rightarrow
⇒
b
=
−
2
16
+
a
b=-2^{16}+a
b=−216+a
所以,无符号整数转有符号整数的公式可以表示为:
b
=
{
a
,
a
≤
有符号整数的最大值
a
−
2
w
a
>
有符号整数的最大值
,
w
为
a
的字节数
b= \begin{cases} a,&a\le有符号整数的最大值\\ a-2^w&a>有符号整数的最大值,w为a的字节数 \end{cases}
b={a,a−2wa≤有符号整数的最大值a>有符号整数的最大值,w为a的字节数
注意:
当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐 式地将有符号参数强制类型转换为无符号数,并假设这两个数都是非负的,来执行这个运算。
不同类型整数的相互转换
不同类型整数的转换大体可以分为四种:
- 一个较小的无符号整数转换为较大的无符号整数(这里的较小较大指的是整数占用的字节位数,比如short较小,int较大)
- 一个较小的有符号整数转换为较大的有符号整数
- 一个较大的无符号整数截断为较小的无符号整数
- 一个较大的有符号整数截断为较小的有符号整数
较小的无符号整数转换为较大的无符号整数
规则:新扩展的位直接用0填充
比如:
unsigned short a =53191;
unsigned int b = (unsigned int )a;
printf("a = %hu, b = %u\n",a,b);
运行结果为:
a = 53191, b = 53191
首先我们先获取a的内存中的二进制表示1100111111000111
转换为unsigned int
的时候直接在前面添加扩展位0,也就是b的二进制表示为
0000000000000000 1100111111000111
所以b = 53191
较小的有符号整数转换为较大的有符号整数
规则:新扩展的位直接用符号位填充
之所以规则这样定义是因为源于一个比较常规的想法即数据扩展之后数据要保持和原数据大小一致,因为扩展和截断不一样,扩展不会丢失数据,所以应该保持数据大小不变。
我们先看一下例子,然后我们证明一下这个规则为什么能保持数据大小不变
signed short a =-12345;
signed int b = (signed int )a;
printf("a = %d, b = %u\n",a,b);
运行结果为:
a = -12345, b = -12345
证明:
假设扩展前整数
a
a
a占用
n
n
n位,扩展后整数
b
b
b占用
m
m
m位,其中
m
>
n
m>n
m>n,根据补码的计算公式:
b
=
−
s
m
−
1
×
2
m
−
1
+
∑
i
=
0
m
−
2
(
s
i
2
i
)
b=-s_{m-1}\times2^{m-1} + \sum_{i=0}^{m-2}(s_i2^i)
b=−sm−1×2m−1+i=0∑m−2(si2i)
⇒
b
=
−
s
m
−
1
×
2
m
−
1
+
∑
i
=
n
m
−
2
(
s
i
2
i
)
+
∑
i
=
0
n
−
1
(
s
i
2
i
)
\Rightarrow b=-s_{m-1}\times2^{m-1} + \sum_{i={n}}^{m-2}(s_i2^i)+ \sum_{i={0}}^{n-1}(s_i2^i)
⇒b=−sm−1×2m−1+i=n∑m−2(si2i)+i=0∑n−1(si2i)
如果符号位为
0
0
0,即
a
≥
0
a\ge0
a≥0,扩展位都是0
⇒
−
s
m
−
1
×
2
m
−
1
+
∑
i
=
n
m
−
2
(
s
i
2
i
)
=
0
\Rightarrow -s_{m-1}\times2^{m-1} + \sum_{i={n}}^{m-2}(s_i2^i)=0
⇒−sm−1×2m−1+i=n∑m−2(si2i)=0
⇒
b
=
∑
i
=
0
n
−
1
(
s
i
2
i
)
=
a
\Rightarrow b=\sum_{i={0}}^{n-1}(s_i2^i) = a
⇒b=i=0∑n−1(si2i)=a
如果符号位为
1
1
1,即
a
<
0
a<0
a<0,扩展位都是1
⇒
−
s
m
−
1
×
2
m
−
1
+
∑
i
=
n
m
−
2
(
s
i
2
i
)
=
−
2
m
−
1
+
∑
i
=
n
m
−
2
2
i
=
−
2
n
\Rightarrow -s_{m-1}\times2^{m-1} + \sum_{i={n}}^{m-2}(s_i2^i)=-2^{m-1} + \sum_{i={n}}^{m-2}2^i=-2^n
⇒−sm−1×2m−1+i=n∑m−2(si2i)=−2m−1+i=n∑m−22i=−2n
⇒
b
=
−
2
n
+
∑
i
=
0
n
−
1
(
s
i
2
i
)
=
−
2
n
−
1
+
∑
i
=
0
n
−
2
(
s
i
2
i
)
=
a
\Rightarrow b= -2^n + \sum_{i={0}}^{n-1}(s_i2^i) = -2^{n-1}+\sum_{i={0}}^{n-2}(s_i2^i) =a
⇒b=−2n+i=0∑n−1(si2i)=−2n−1+i=0∑n−2(si2i)=a
较大的无符号整数截断为较小的无符号整数
规则:截断前数据为
a
a
a,截断后数据为
b
b
b,
b
b
b占用
n
n
n位,则
b
=
a
m
o
d
2
n
b = a\ mod\ 2^n
b=a mod 2n
证明:
假设 截断前数据为
a
a
a,
a
a
a占用
m
m
m位,截断后数据为
b
b
b,
b
b
b占用
n
n
n位,其中
m
>
n
m>n
m>n
则
a
=
b
+
∑
i
=
n
m
−
1
(
s
i
2
i
)
a = b+\sum_{i={n}}^{m-1}(s_i2^i)
a=b+i=n∑m−1(si2i)
对于任何
i
≥
n
i\ge n
i≥n,
s
i
2
i
s_i2^i
si2i都可以表示成
k
i
2
n
,
k
i
≥
0
,
并且
k
i
是整数
k_i2^n,k_i\ge0,并且k_i是整数
ki2n,ki≥0,并且ki是整数
⇒
a
=
b
+
∑
i
=
n
m
−
1
(
k
i
2
n
)
\Rightarrow a = b + \sum_{i={n}}^{m-1}(k_i2^n)
⇒a=b+i=n∑m−1(ki2n)
⇒
a
=
b
+
K
2
n
\Rightarrow a = b + K2^n
⇒a=b+K2n
又因为
b
<
2
n
b<2^n
b<2n
所以
b
=
a
m
o
d
2
n
b = a\ mod\ 2^n
b=a mod 2n
较大的有符号整数截断为较小的有符号整数
规则:截断前数据为
a
a
a,截断后数据为
b
b
b,
b
b
b占用
n
n
n位
我们应该按照下面的步骤计算b的值:
- 计算 a a a作为无符号整数的大小 a 1 a_1 a1
- 计算 b 1 = a 1 m o d 2 n b_1 = a_1\ mod\ 2^n b1=a1 mod 2n
- 将 b 1 b_1 b1转换为有符号整数 b b b