书的第 31 页给出了一段例程,用以打印 C 语言中变量对应内存的内容。其具体代码如下:
#include
typedef unsigned char *byte_pointer;
void show_bytes(byte_pointer start, size_t len)
{
size_t i;
for(i = 0; i < len; i++)
printf(" %.2x", start[i]);
printf("\n");
}
这一段代码具有很强的实用性,尤其在需要深入查看内存内容的时候。虽说 VS 也有查看内存内容的功能,但 VS 缺点在于太过臃肿,除了做大型项目一般也很少打开;相反这样一段精简的 C 程序更能在各种情况下发挥作用。
在书的第 36 页第一次出现了一个奇怪的数学符号: ≐ \doteq ≐。
经网络查询得知,该符号与 ≈ \approx ≈ 是等价的,也就是说是“约等于”的意思,但再看看书中的内容,总是不太对劲儿,“位向量 a ≐ [ 01101001 ] a\doteq[01101001] a≐[01101001]”难道表示的是“位向量 a a a 约等于位模式 [ 01101001 ] [01101001] [01101001]”吗?
到了第 44 页,终于给出了对这个奇怪数学符号的官方定义:符号“ ≐ \doteq ≐ ”表示左边被定义为等于右边。也就是说,“位向量 a ≐ [ 01101001 ] a\doteq[01101001] a≐[01101001]”实际的含义应该是“位向量 a a a 被定义为位模式 [ 01101001 ] [01101001] [01101001] ”。
异或是布尔代数中的加法
将“异或”与“加法”联系起来的思路让我豁然开朗。以前从来没有想到过还可以以加法的视角来看待异或运算,虽说经常会看到底层的加法运算电路使用异或门来完成相应操作,但也只是以为是人为设计的缘故,并没有思考过为什么都是“异或”。直到在树上看到这样的说法,才终于发现自己以前被逻辑运算和算术运算的条条框框限制住了,哪怕我一向自认是比较善于发散思维的人,也不曾尝试过将逻辑运算和算术运算的内部原理联系起来。
到这里,其实给我更多震动的还不是异或的神奇,而是又一次发现了数学之美。数学的美有很多种,这种深藏于原理内部的相似性就是其中一种。
异或的中文我一直理解为“相异则或”,也就是说参与异或运算的两个元素如果相异,则进行“或”运算;而其英文exclusive or我则一直理解为“互斥型或运算”。也不知是否靠谱,不过在我自己的逻辑中能够自洽。
综合以上三条性质,可以解决一个十分有趣的问题:在一堆数中,除了一个数只出现奇数次外,其他数都出现了偶数次,问如何找出这个出现了奇数次的数。
运用异或运算,答案很简单,将所有数按位进行异或,最后得到的结果所表示的数,就是要找的出现奇数次的数。简化表示就是: ( a ∧ b ) ∧ a = b (a\wedge b)\wedge a=b (a∧b)∧a=b。
在书中,补码由下述公式定义:
B 2 T w ( x ⃗ ) ≐ − x w − 1 2 w − 1 + ∑ i = 0 w − 2 x i 2 i B 2 T_{w}(\vec{x}) \doteq-x_{w-1} 2^{w-1}+\sum_{i=0}^{w-2} x_{i} 2^{i} B2Tw(x)≐−xw−12w−1+i=0∑w−2xi2i
但是这样的定义并不直观,因此我想介绍另一个更加直观一些的理解。
在《计算机是怎样跑起来的》一书中对补码的定义十分精到和巧妙,正是看了那一段,我才真正地理解了补码到底是怎么来的。
《计算机是怎样跑起来的》中,定义补码的计算为:将对应正数的原码逐位取反,再在结果的基础上加 1,所得即为对应负数的补码表示。
这一点与惯常的说法没有什么不同,但点睛之笔在于说出了为什么要这样计算。就是看了这一段,我才真正明白为什么补码的计算方式会这么奇怪不自然,甚至有点“造作”。实际上一切的不合理都有一个合理的解释。
下面我用自己的理解尽量清楚地解释一下。
在学习数字电路的时候讲过三种编码:原码、反码和补码,但我从来没有理解到过补码的公式是怎么来的,为什么要叫“补码”这么奇怪的名字。原码容易理解,就是“原始的二进制编码”。反码也容易理解,就是在原码的基础上,按位取反。但是这个“补码”到底是什么意思呢?什么叫补?怎么补?跟谁补?
实际上,“补码”之所以叫“补码”,并不是随意取的名字。其中“补”的意思是“和为零”,也就是说 1 + ( − 1 ) = 0 1+(-1)=0 1+(−1)=0,则称 1 和 -1 互补;对二进制数亦然。有符号整数的补码表示正是基于“互补”这个定义而来。
以四位二进制数为例,所能表示的最大无符号数就是位模式 [1111] 对应的无符号整数 15,一旦在此基础上再加 1 想表示 16 的话,就会产生溢出,理论上应该得到的是 [10000],但由于位模式只有四位,因此产生截断变成了 [0000],而 [0000] 对应的无符号数就是整数 0。显然我们可以得到 [ 1111 ] + [ 0001 ] = [ 0000 ] [1111]+[0001]=[0000] [1111]+[0001]=[0000]。好了,根据上面对补码的定义,到这里我们可以得出结论:[1111] 和 [0001] 是互补的,即“互为补码”。
而根据数学常识,“相加为 0 的两个数互为相反数”,也就是说 [1111] 和 [0001] 在这种位有限的情况下,还应该是事实上的相反数。我们将 [0001] 视作正常表示的无符号数 +1,那么自然而然地,[1111] 表示的就应该是 -1 了。
根据这个步骤,我们可以求出每个可表示的正数对应的负数的补码表示。
但有一个问题,知道了一个正数,怎么快速确定其对应负数的补码表示呢?
回忆一下前面提到过的,“对原码按位取反”。显然这个反码与原码之和一定是 [1111],因为按位取反之后,原码是 1 的位,反码一定是 0;原码是 0 的位,补码一定是 1。于是相加之后得到的一定是一个全为 1 的位模式。“再在所得结果的基础上加 1”。众所周知,加法具有交换律。计算过程说要在反码的基础上加 1,我们可以考虑将反码与原码求和之后,再在和的基础上加 1。这样就相当于在 [1111] 的基础上加 1,所得的结果一定是 0,即 [0000]。
好了,讨论到这一步,再回过头来看“互补”的定义:和为零即为互补。于是我们可以说,假设位向量 a ⃗ \vec{a} a 是一个补码表示的正数 a a a 对应的位模式,而 b ⃗ \vec{b} b 则是一个补码表示的负数 b b b 对应的位模式。要使 b = − a b=-a b=−a,显然应该有
a ⃗ + b ⃗ = 0 \vec{a}+\vec{b}=0 a+b=0
假如 a ⃗ ′ \vec{a}' a′ 是 a ⃗ \vec{a} a 对应的反码,则容易有
a ⃗ + a ⃗ ′ + 1 = 0 \vec{a}+\vec{a}'+1=0 a+a′+1=0
比较上述两式,可以得到:
b ⃗ = a ⃗ ′ + 1 \vec{b}=\vec{a}'+1 b=a′+1
也就是说,从补码的定义,我们可以推导出计算补码的方式:将对应正数的原码逐位取反,再在结果的基础上加 1,所得即为对应负数的补码表示。
以上。