跳到主要内容

二进制

致谢

本文源自 SA-MP 论坛的一篇教程,作者为 Kyosaur

什么是二进制?

二进制是一种使用两个独特符号表示数字的计数系统。虽然更常见的十进制系统使用十个数字(基数为 10),但二进制仅使用 0 和 1。这听起来在日常生活中毫无用处,但二进制在计算机领域却至关重要。计算机在最底层通过控制电流的开关状态来进行所有计算,而这正是二进制的本质——无数的开关在开与关之间切换。对于大多数人来说,这是一个陌生的概念,因此让我们将十进制和二进制系统并列来看。

十进制(基数为 10)

0
1
2
3
4
5
6
7
8
9
10
11
12
13

二进制(基数为 2)

0 //0
1 //1
10 //2
11 //3
100 //4
101 //5
110 //6
111 //7
1000 //8
1001 //9
1010 //10
1011 //11
1100 //12
1101 //13

将两种系统并列观察,你会发现它们的行为完全相同。当你达到最后一个可用数字时,必须移动到下一个位置。在二进制中,这些位置被称为位(binary digits),它们是 2 的幂;正如十进制系统中的位置是 10 的幂一样。为了证明这一点,让我们以标准形式来看数字 13。

注意: 在接下来的几个示例中,'^' 表示幂,而不是位异或(稍后会介绍)。

十进制(基数为 10)

13

//等于

1 * (10^1) + 3 * (10^0)

//等于

10+3

//等于

13

二进制(基数为 2)

1101

//等于

1 * (2^3) + 1 * (2^2) + 0 * (2^1) + 1 * (2^0)

//等于

8+4+0+1

//等于

13

从前面的例子可以看出,如果某一位为 0,我们可以忽略它并继续;毕竟,任何数乘以 0 都是 0。前面的例子有点过于复杂,只是为了确保绝对清晰。当你从二进制转换时,真正需要做的就是将所有开启的位的幂相加。

以下是 12 个 2 的幂,随手列出:

4096,2048,1024,512,256,128,64,32,16,8,4,2,1

如果你对幂运算一无所知,这可能对你来说毫无意义。幂是一个数乘以自身 x 次。有了这个信息,前面的幂列表可能更有意义了;当然,除了 1。你可能会好奇为什么 2 的 0 次幂等于 1,我只能说事实就是如此。

2^1 = 2, 2^3 = 4, 2^4 = 8

我们可以看到,当我们向右移动时,前一个值乘以 2;因此可以假设,当我们向左移动时,新值就是前一个数除以 2。有了这个想法,你可以看到 2 的 0 次幂如何等于 1。如果这还不够令人信服,我相信你可以在 ** 上找到更多证据。说了这么多,让我们来看最后一个例子,并让它稍微复杂一点!

111011001011111000 //242424

//记住,忽略未开启的位。

1 * (2^17) = 131072

1 * (2^16) = 65536

1 * (2^15) = 32768

1 * (2^13) = 8192

1 * (2^12) = 4096

1 * (2^9) = 512

1 * (2^7) = 128

1 * (2^6) = 64

1 * (2^5) = 32

1 * (2^4) = 16

1 * (2^3) = 8


131072+65536+32768+8192+4096+512+128+64+32+16+8
=
242424

记住转换时:第一个幂是 0,所以不要错误地将第 18 位视为 2^18。确实有 18 个幂,但这包括 0 次幂,因此 17 实际上是我们的最高幂。

深入探讨位

大多数编程语言允许不同的数据类型,这些数据类型的位数范围不同,用于存储信息;然而,pawn 是一种无类型的 32 位语言。这意味着 pawn 始终有 32 位可用于存储信息。那么,当信息过多时会发生什么?答案在于有符号和无符号整数。

有符号整数

你是否注意到,当 pawn 中的整数过高时,它会变成负数?这种“环绕”现象是因为你超过了 pawn 中的最大值,即:

2^31 - 1 //幂,而不是位异或。此外,-1 是因为我们计算了 0(有 2,147,483,648 个值,但其中包括 0,因此技术上 2,147,483,647 是最大值)。

//等于

2,147,483,647

//在二进制中为

1111111111111111111111111111111 //31 位 - 全部开启

你可能会想知道为什么最大值是那个,而不是 2^32-1(4,294,967,295)。这就是有符号和无符号整数的用武之地。有符号整数能够存储负值,而无符号整数则不能。这听起来好像我偏离了问题,但我向你保证并非如此。最大整数不是 2^32-1 的原因是第 32 位被用作负值和正值的切换开关。这被称为 MSB(最高有效位),如果 MSB 开启,则该数为负;如果关闭,则该数为正。很简单,对吧?

在展示一些负值之前,我需要解释 pawn 中如何表示负值。pawn 使用一种称为 2 的补码系统来表示负值,这基本上意味着你将数字中的每一位都翻转,并将新数字加 1 以使其为负。

让我们在脑海中还有这个想法时,看看几个负值:

11111111111111111111111111111111 //所有 32 位开启

//等于

-1

//和

11111111111111111111111111111110

//等于

-2

//最后

10000000000000000000000000000000

//等于

-2147483648

看,所有负数都是原始正数所有位翻转并加一的结果。这在我们的最后一个例子中非常明显,因为最大的正数是 2147483647。

由此我们可以看出,pawn 中的数字范围实际上是:

−2^31+2^311

无符号整数

pawn 中没有无符号整数,但我添加这一部分是为了保持平衡。有符号整数和无符号整数之间的唯一区别是无符号整数不能存储负值;整数仍然会环绕,但它们会回到 0,而不是负值。

二进制运算符

二进制运算符允许你操作位模式中的单个位。让我们看看可用的位运算符列表。

  • 位算术移位:>>, 和 <<
  • 位逻辑移位:>>>
  • 位非(即补码):~
  • 位与:&
  • 位或:|
  • 位异或(即异或):^

位与

注意: 不要与逻辑与运算符 '&&' 混淆

二进制与只是对二进制形式中每个位置的位进行逻辑与运算。这听起来有点混乱,所以让我们看看它的实际效果!

1100 //12
&
0100 //4
=
0100 //4,因为它们都有“100”(即 4)

这有点简单,让我们看一个更难的例子:

10111000 //184
&
01001000 //72
=
00001000 //8

通过示例应该能让你很好地理解这个运算符的作用。它比较两个位集,如果它们共享一个为 1 的位,结果将开启相同的位。如果它们没有共享任何位,则结果为 0。

位或

注意: 不要与逻辑或运算符 '||' 混淆

位或的工作原理几乎与位与完全相同。两者之间的唯一区别是,位或只需要两个位模式中的一个开启某一位,结果就会开启相同的位。让我们看几个例子!

1100 //12
|
0100 //4
=
1100 //12.

让我们再看一个例子。

10111000 //184
|
01001000 //72
=
11111000 //248

我认为这非常直观,如果任何一个数字开启了某一位,结果数字也会开启该位。

位异或

这个运算符与位或运算符有点相似,但有一些区别。让我们看看在位或部分使用的相同示例,看看你是否能发现区别。

1100 //12
^
0100 //4
=
1000 //8.

最后:

10111000 //184
^
01001000 //72
=
11110000 //240

位非

这个运算符翻转位模式中的每一位,将所有 1 变为 0,反之亦然。

~0
=
11111111111111111111111111111111 //-1

//和

~100 //4
=
11111111111111111111111111111011 //-5

//和

~1111111111111111111111111111111 //2147483647(不要与 -1 混淆,后者有 32 位,而不是 31)
=
10000000000000000000000000000000 //-2147483648(第 32 位开启)

如果你不理解为什么负值有点“反向”,请阅读有关有符号整数的部分。

位移

位移操作符完全按照你的想象行事;它将数字中的位向某个方向移动。如果你记得文章前面提到的,PAWN 有一个特定的内存范围(32 位可用于存储)。当你将一个数字移出这个范围时会发生什么?这个问题的答案在于你使用的位移操作符以及你移动的方向。

注意: 在以下示例中,所有二进制数字都将完整写出(所有 32 位),以避免混淆。

算术移位

右移

使用此运算符时,数字中的所有位都会向右移动 x 次。让我们看一个简单的例子。

00000000000000000000000000001000  //8
>>
2

=

00000000000000000000000000000010 //2

你可以从前面的例子中看到,每一位都向右移动了两个位置,并在左侧添加了两个零作为填充。这两个零实际上是 MSB(最高有效位)的值,在有符号整数移位时非常重要。使用 MSB 作为填充的原因是为了保持被移位数字的符号。让我们看相同的例子,但这次让它为负数。

11111111111111111111111111111000 //-8
>>
2

=

11111111111111111111111111111110 //-2

显然,这与前面的例子行为完全相同,只是左侧用于填充的位是 1;这证明了右算术移位的填充是 MSB 的值。

左移

这与右算术移位操作符完全相反。它将数字中的所有位向左移动 x 次。让我们看一个例子。

00000000000000000000000000001000  //8
<<
2

=

00000000000000000000000000100000 //32

左算术移位和右算术移位之间的唯一区别(除了移位方向)是它处理填充的方式。对于右算术移位,填充是 MSB(最高有效位)的值,但对于左算术移位,填充只是 0。这是因为没有像数字符号这样的相关信息需要跟踪。

11111111111111111111111111111000 //-8
<<
2

=

11111111111111111111111111100000 //-32

看到了吗?即使填充始终为 0,数字的符号仍然保留。你真正需要担心的是移位过远。如果你将一个正数移过最大可能数,它将变为负数,反之亦然(负数最终会变为 0)。

逻辑移位

右移

这是算术左移的反向。描述它的最佳方式是两种算术移位的混合体。让我们看看它的实际效果!

00000000000000000000000000001000  //8
>>>
2

=

00000000000000000000000000000010 //2

数字 8 中的位向右移动了 2 次。那么这与算术右移有什么不同呢?答案是填充。对于算术右移,填充是 MSB 的值,但对于逻辑右移,填充只是 0(就像算术左移一样)。这意味着它不会保留符号,我们的结果将始终为正。为了证明这一点,让我们移动一个负数!

11111111111111111111111111111000 //-8
>>>
2

=

00111111111111111111111111111110 //1073741822

这证明了我们在使用逻辑右移时不会得到任何负值!

左移

没有逻辑左移,因为它会与算术左移做完全相同的事情。我添加这一部分只是为了避免任何混淆,并保持部分的平衡。