第 12 講:運(yùn)算符(四):位運(yùn)算符
位運(yùn)算一共有六個(gè):分別是位與運(yùn)算 &
、位或運(yùn)算 |
、位取反運(yùn)算 ~
、位異或運(yùn)算 ^
、位左移運(yùn)算 <<
和位右移運(yùn)算 >>
。
為了引導(dǎo)你學(xué)習(xí)比特位處理,我們先來(lái)學(xué)習(xí)一下整數(shù)在內(nèi)存里的表達(dá)。
實(shí)際上,小數(shù)(浮點(diǎn)數(shù))也有二進(jìn)制表達(dá)的邏輯。因?yàn)樗鼪](méi)有比特位的相關(guān)處理,因此我們不在這里作過(guò)多介紹。如果需要學(xué)習(xí)的話,請(qǐng)參考 IEEE 754 規(guī)范的相關(guān)內(nèi)容。警告:該規(guī)范的學(xué)習(xí)難度較大。
Part 1 整數(shù)的內(nèi)存表達(dá)方式
在早期發(fā)明計(jì)算機(jī)的時(shí)候,我們擁有一種萬(wàn)能的轉(zhuǎn)換邏輯將人類能理解的十進(jìn)制數(shù)值改成二進(jìn)制數(shù)值。為了幫助理解,我們先講正整數(shù),然后講負(fù)整數(shù)。它們的處理是不太一樣的。
1-1 正整數(shù)的表達(dá)
我們將一個(gè)十進(jìn)制數(shù)以二進(jìn)制(只用 0 和 1 兩個(gè)數(shù)碼)來(lái)表示。為了保證表達(dá)的唯一性,采用的辦法是這樣的:
將一個(gè)十進(jìn)制數(shù)不斷向下除以 2,并一直往下寫(xiě)整數(shù)除法運(yùn)算的結(jié)果,并在右側(cè)對(duì)應(yīng)寫(xiě)上除法的余數(shù)(0 或 1);
然后將表達(dá)出來(lái)的余數(shù)序列,從下往上倒著書(shū)寫(xiě)。書(shū)寫(xiě)的結(jié)果就是二進(jìn)制數(shù)值了。
來(lái)看一個(gè)例子:47。我們要表達(dá) 47 這個(gè)數(shù)的二進(jìn)制表達(dá),那么就不斷除以 2。
當(dāng)然,因?yàn)閷?duì) 2 取模運(yùn)算(就是取余數(shù))的話,只有奇數(shù)余數(shù)為 1,偶數(shù)余數(shù)為 0,因此你可以直接這么去記憶規(guī)則:當(dāng)數(shù)字是偶數(shù)的時(shí)候,直接在右邊寫(xiě) 0;否則就是奇數(shù),那么右邊就寫(xiě) 1。
最后,我們從最下面的 1 開(kāi)始往上倒著讀序列:101111,這個(gè)數(shù)字就是 47 的二進(jìn)制結(jié)果。
相反地、我們?nèi)绻枰獙⒍M(jìn)制轉(zhuǎn)換回十進(jìn)制數(shù)值的話,就把二進(jìn)制數(shù)寫(xiě)出來(lái)之后,將每一個(gè)位置上的數(shù)字乘以權(quán)重(Weight),然后加起來(lái)。
我們還是拿 47 的二進(jìn)制來(lái)說(shuō)明。
我們?cè)诿恳粋€(gè)數(shù)位的下方寫(xiě)上 0 到 5(從右往左寫(xiě))。
然后,把標(biāo)記了 1 的地方全部記作 2 的 n 次方。最后,把它們?nèi)悠饋?lái)。
這里每一個(gè)數(shù)位都稱為一個(gè)比特位(Bit),也稱為比特或者位。
1-2 負(fù)整數(shù)的表達(dá),以及補(bǔ)碼的引入
正整數(shù)是最基礎(chǔ)的表達(dá)過(guò)程。但是負(fù)整數(shù)有點(diǎn)不一樣。在二進(jìn)制的處理的過(guò)程之中,為了盡量使用較少的工具完成較多的任務(wù),計(jì)算機(jī)科學(xué)家考慮使用加法器來(lái)計(jì)算減法。舉個(gè)例子,我們想要完成 5 - 3 的任務(wù),那么只需要改成 5 + (-3) 就可以了。這里的 -3 就是用負(fù)整數(shù)的表達(dá)就可以。然后,直接使用加法的算法,將 5 和 -3 兩個(gè)數(shù)字加起來(lái)。
科學(xué)家最開(kāi)始考慮的是用原碼表達(dá)整數(shù)。原碼和前面介紹的十進(jìn)制轉(zhuǎn)換成二進(jìn)制后的結(jié)果基本上一樣。就多了一個(gè)規(guī)則:如果這個(gè)數(shù)字是負(fù)數(shù),那么就將最高位的比特從 0 改成 1。比如說(shuō) 3 的話,我們就可以將最高位從 0 改成 1。最高位在哪里呢?這里就牽扯到了一個(gè)概念:數(shù)據(jù)類型占內(nèi)存多大的問(wèn)題。
一般來(lái)說(shuō),sbyte
占 1 個(gè)字節(jié)(8 個(gè)比特)、short
占 2 個(gè)字節(jié)(16 個(gè)比特)、int
占 4 個(gè)字節(jié)(32 個(gè)比特),而 long
占 8 個(gè)字節(jié)(64 個(gè)比特)。這四個(gè)類型都是帶符號(hào)的類型,即除了正整數(shù)以外,還可以表示負(fù)整數(shù)。
按照一般的道理來(lái)說(shuō),假如這個(gè)數(shù)據(jù)類型是 sbyte
的話,那么我們就需要用到 8 個(gè)連續(xù)的比特來(lái)表達(dá)一個(gè)整數(shù)。當(dāng)這個(gè)數(shù)字是負(fù)數(shù)的時(shí)候,最高位改成 1,其它比特位則依舊是二進(jìn)制的普通表達(dá)。
這樣就可以對(duì)應(yīng)到之前的文章內(nèi)容。8 個(gè)比特的話,一個(gè)比特表示符號(hào),那么剩下的自然就只有 7 個(gè)比特了。7 個(gè)比特通過(guò) 0 和 1 的排列組合,一共能表達(dá) 128 種不同的數(shù)字(7 個(gè)數(shù)位,每一個(gè)數(shù)位能表示 0 和 1 兩種情況,所以組合起來(lái)就是 2 的 7 次方,即 128 種結(jié)果);正是因?yàn)檫@個(gè)原因,外帶一個(gè)符號(hào)位,所以
sbyte
的范圍是 -128 到 127。你可能會(huì)問(wèn)我:“欸,不對(duì)啊,這 -128 哪里來(lái)的;還有,為什么正整數(shù)只到 127,128 哪里去了”。這個(gè)問(wèn)題我們不在這里說(shuō)明。等我把這一點(diǎn)內(nèi)容說(shuō)完了,這個(gè) -128 你自然就知道怎么來(lái)的了。
那么,-3 可以表達(dá)為 10000011
:最高位的 1 表示這個(gè)數(shù)是負(fù)數(shù),而后面 7 個(gè)位置 0000011 剛好是 3 的二進(jìn)制表達(dá),所以這個(gè)數(shù)字是 -3。這個(gè) -3 的二進(jìn)制表示稱為原碼形式。
問(wèn)題來(lái)了。如果我們直接帶入 5 和 -3 的原碼計(jì)算加法,會(huì)得到什么結(jié)果呢?
顯然,負(fù)數(shù)的數(shù)據(jù)要和正數(shù)的數(shù)據(jù)是互補(bǔ)的,才能使得計(jì)算過(guò)程能夠正常進(jìn)行。因此,科學(xué)家發(fā)明了反碼和補(bǔ)碼的概念??茖W(xué)家篤定了,補(bǔ)碼形式一定能讓負(fù)數(shù)變成可帶入加法器運(yùn)算的特殊表達(dá)形式。
補(bǔ)碼是將原碼的非符號(hào)位全部取反,然后再這個(gè)基礎(chǔ)上再自增一個(gè)單位,得到的結(jié)果。比如 -3,我們要經(jīng)過(guò)如下的一番運(yùn)算,才能得到補(bǔ)碼表達(dá):
我們把補(bǔ)碼提取出來(lái),參與剛才的加法運(yùn)算:
就有這么巧。5 + (-3) 結(jié)果恰好等于 2。稍微注意一下最高位的 1 的進(jìn)位邏輯。由于我們拿 sbyte
0000 0010
。而這個(gè)數(shù)據(jù)的最高位是 0,也就是說(shuō)它是一共正整數(shù),故直接讀取數(shù)值信息,就是 2 了。
是的,科學(xué)家發(fā)明的補(bǔ)碼就是為了解決讓負(fù)數(shù)也可以參與加法器的加法運(yùn)算過(guò)程的問(wèn)題。當(dāng)然,除了解決這個(gè)問(wèn)題,還有一個(gè)問(wèn)題是,0 的原碼里,+0 和 -0 是兩個(gè)表達(dá)。一個(gè)是 0000 0000
,而另外一個(gè)是 1000 0000
。這樣顯然不行啊。于是,后者(-0)就使用補(bǔ)碼來(lái)讀取數(shù)據(jù):
在變回去后,1000 0000
0000 0000
了,所以 -0 和 0 就是一樣的數(shù)據(jù)了,確實(shí)很巧妙。
由于
1000 0000
轉(zhuǎn)反碼的時(shí)候需要先減去 1,而后面全 0 的關(guān)系,只能從符號(hào)位去減,因而數(shù)據(jù)成了0111 1111
。
在計(jì)算機(jī)里,1000 0000
被特殊處理,由于符號(hào)位是 1,因此只能讀作負(fù)數(shù),故這個(gè)數(shù)值就是 -128。
好了。我們解釋了補(bǔ)碼的問(wèn)題,下面我們可以來(lái)看一下位運(yùn)算了。
Part 2 位與運(yùn)算
位與運(yùn)算將兩個(gè)數(shù)字對(duì)應(yīng)的比特位作位與運(yùn)算處理。它的操作和邏輯且運(yùn)算是差不多的。我們把 0 當(dāng)成 false
、1 當(dāng)成 true
來(lái)理解:兩個(gè)比特位在參與運(yùn)算的時(shí)候,如果都是 1 才是 1,其它的情況都是 0。
我們使用 a & b
來(lái)表示把兩個(gè)數(shù)字使用位與運(yùn)算。它和貪婪邏輯且運(yùn)算用的是一樣的符號(hào),但是貪婪邏輯且運(yùn)算符的兩側(cè)都是 bool
類型的數(shù)值,而這里的 a
和 b
則是整數(shù)類型。
舉個(gè)例子。我們將 5 和 -3 進(jìn)行位與運(yùn)算。運(yùn)算過(guò)程如下:
我們可以看到 5 & -3
Part 3 位或運(yùn)算
位或運(yùn)算和位與運(yùn)算差不多,也和位與運(yùn)算的過(guò)程是對(duì)稱的:只有兩邊都是 0 的時(shí)候,結(jié)果是 0,否則是 1。
我們依舊拿 5 和 -3 舉例子。
它使用符號(hào) |
來(lái)表示。
Part 4 位異或運(yùn)算
位異或運(yùn)算和邏輯異或運(yùn)算是一樣的。我們依舊把 1 當(dāng)成 true
、0 當(dāng)成 false
。當(dāng)兩個(gè)比特參與運(yùn)算的時(shí)候,當(dāng)且僅當(dāng)兩個(gè)比特位相同的時(shí)候(都是 0 或者都是 1),結(jié)果是 0;否則是 1。
它使用符號(hào) ^
來(lái)表示。
Part 5 位取反運(yùn)算
位取反運(yùn)算和“原碼取反”的過(guò)程基本一樣,但是位取反的邏輯甚至?xí)衙恳粋€(gè)比特位取反,包括符號(hào)位。不過(guò)和邏輯取反運(yùn)算類似,它只針對(duì)于一個(gè)數(shù)字進(jìn)行運(yùn)算,而不是兩個(gè)數(shù)字一起參與運(yùn)算。
不過(guò)稍微不一樣的地方是,位取反運(yùn)算并不是用感嘆號(hào),而是 ~
符號(hào):比如說(shuō) ~(-3)
的結(jié)果就是 2。當(dāng)然,這個(gè) -3 的括號(hào)可以不要,即 ~-3
Part 6 位左移運(yùn)算
位左移運(yùn)算寫(xiě)成 <<
,表示將數(shù)值的比特位直接往左移動(dòng)若干位置;右邊移動(dòng)出去的部分補(bǔ)充 0。比如 3 << 4
的話,將 3 寫(xiě)成二進(jìn)制就是 0000 0011
。<< 4
表示往左邊移動(dòng) 4 個(gè)比特,然后右側(cè)補(bǔ)充 0,就變成了 0000 0011 0000
。顯然,我們拿 sbyte
類型舉例,高 4 個(gè)位置的比特位會(huì)超出存儲(chǔ)范圍,因此會(huì)被舍棄掉,故結(jié)果是 0011 0000
,即十進(jìn)制的 48,故 3 << 4
的結(jié)果就是 48。
這里可以記住一個(gè)結(jié)論。由于比特位是完整左移的,再加上右側(cè)全部自動(dòng)補(bǔ)充 0 來(lái)填補(bǔ)位置,所以實(shí)際上這個(gè)數(shù)據(jù)是被擴(kuò)大了 2 的次冪這么多倍數(shù)。舉個(gè)例子,
3 << 4
就應(yīng)該和 的結(jié)果是一樣的。實(shí)際上一看,確實(shí)是的:。
Part 7 位右移運(yùn)算
同理,位右移運(yùn)算寫(xiě)成 >>
,即將所有比特位往右邊移動(dòng)若干位置;然后左側(cè)多出來(lái)的位置補(bǔ)充 0 占位。比如 47 >> 3
這個(gè)數(shù)值等于多少呢?47 寫(xiě)成二進(jìn)制是 0010 1111
,往右移動(dòng) 3 個(gè)位置就變成了 0000 0101 111
。最后面的三個(gè)位置上的 1 由于超出了表達(dá)范圍,因此被舍去,因此數(shù)據(jù)變成了 0000 0101
,這個(gè)數(shù)值是 5,因此 47 >> 3
的結(jié)果是 5。
同樣地,我們可以發(fā)現(xiàn)位右移運(yùn)算也有類似的結(jié)論。往右移動(dòng)比特位會(huì)將低比特位丟棄,而原始數(shù)據(jù)被縮小,因此實(shí)際上數(shù)據(jù)是被縮小了 2 的次冪這么多倍數(shù)。舉個(gè)例子,
47 >> 3
就應(yīng)該和 是一樣的。注意,外圍的 里,這個(gè)符號(hào)叫向下取整。因?yàn)閿?shù)值本身是整數(shù),而除法會(huì)使得數(shù)據(jù)變?yōu)樾?shù),因此需要取整運(yùn)算。
Part 8 總結(jié)
本文給大家介紹了位運(yùn)算操作。這些操作可能不容易理解,對(duì)于我們以后來(lái)說(shuō),很少用到。如果我們會(huì)用到它們,我們?cè)诤竺娴奈恼聲?huì)再次說(shuō)明。