許多初學(xué)者對 《《(左移)和 》》(右移)運(yùn)算符在 C/C++ 等編程語言中的工作方式感到困惑。在本專欄中,所有(嗯,相當(dāng)多)都將被揭示,但在我們滿懷熱情地投入戰(zhàn)斗之前,我們首先需要確保我們都了解一些基本概念。
位、半字節(jié)和字節(jié)
可以在計(jì)算機(jī)內(nèi)部存儲和操作的最小數(shù)據(jù)量是二進(jìn)制數(shù)字或位,它可用于存儲兩個不同的值:0 或 1。這些值在任何特定情況下實(shí)際體現(xiàn)的內(nèi)容時(shí)間取決于我們。例如,我們可能決定一位代表開關(guān)的狀態(tài)(例如,向下或向上)或燈(例如,關(guān)閉或打開)或邏輯值(例如,假或真)?;蛘撸覀兛赡軟Q定使用我們的位來表示數(shù)值 0(零)或 1(一)。
只是為了增加樂趣和輕浮性,我們可以隨時(shí)更改我們希望我們的位代表的內(nèi)容。在程序的一部分中,我們可以將位視為表示一個邏輯值;稍后,我們可能會決定將同一位視為體現(xiàn)一個數(shù)字量。電腦不在乎。它所看到的只是 0 或 1。它不知道我們在任何特定時(shí)間使用 0 或 1 來表示什么。
我們只能用一個單獨(dú)的部分做很多事情。因此,計(jì)算機(jī)內(nèi)部的數(shù)據(jù)通常使用比特組進(jìn)行存儲和操作。常見的分組有 4 位、8 位、16 位、32 位和 64 位。一組 8 位稱為byte,而一組 4 位稱為nybble(或nibble)。“兩個 nybbles 組成一個字節(jié)”的想法是一個工程笑話,從而同時(shí)證明 (a) 工程師確實(shí)有幽默感和 (b) 他們的幽默不是很復(fù)雜。
已經(jīng)零星地嘗試采用其他大小的位組的術(shù)語。例如,tayste(或crumb)用于 2 位組;playte(或chawmp)用于 16 位組;32 位組的dynner(或gawble );和table用于 64 位組。但是到目前為止,您只能開個玩笑,因此使用標(biāo)準(zhǔn)術(shù)語byte和nybble(或nibble)以外的任何內(nèi)容都極為罕見。
字節(jié)、字符和整數(shù)
在嘗試解釋與計(jì)算機(jī)相關(guān)的主題時(shí)遇到的問題之一是,您經(jīng)常會陷入“雞或蛋”的境地,理想情況下,您需要理解概念 A 才能理解概念 B ,但是您確實(shí)需要熟悉概念 B 才能將您的大腦包裹在概念 A 上(有一個古老的編程笑話說:“要理解遞歸,必須先了解遞歸”)。
我們只是說,稍后我們將介紹無符號二進(jìn)制數(shù)的概念。稍后,我們將介紹有符號二進(jìn)制數(shù)的概念。關(guān)鍵是》》(右移)運(yùn)算符執(zhí)行其魔法的方式可能取決于我們是否告訴計(jì)算機(jī)將其正在操作的值視為有符號或無符號。
C/C++ 中兩種常用的數(shù)據(jù)類型是 8 位char(字符)和int(整數(shù))。Arduino IDE/編譯器也支持 8-bit byte,但 ANSI-C 標(biāo)準(zhǔn)不支持這種類型。在 Arduino 草圖中使用這些類型的示例變量聲明如下:
byte myByte = 65;
char myChar = ‘A’;
int myInt = 65;
請注意,在 char 類型的情況下,字符在計(jì)算機(jī)內(nèi)部使用ASCII 標(biāo)準(zhǔn)存儲為數(shù)字。在 ASCII 中,數(shù)字 65 代表大寫“A”,因此“myChar = ‘A’;” 和“myChar = 65;” 兩者都會以包含數(shù)字 65 的變量 myChar 結(jié)束。
不幸的是,int 的大小是未定義的,并且因一臺計(jì)算機(jī)而異。例如,對于 Arduino,int 是 16 位寬,但在另一種類型的計(jì)算機(jī)上可能是 16、32 或 64 位寬。
請記住,我們將在下面解釋有符號和無符號二進(jìn)制數(shù)之間的區(qū)別。然而,當(dāng)我們在這里時(shí),我們應(yīng)該注意,一個字節(jié)將被 Arduino IDE 的編譯器視為未簽名,而一個 int 將被視為由任何 C/C++ 編譯器簽名。只是為了咯咯笑和笑,C/C++ 標(biāo)準(zhǔn)允許將 char 類型視為有符號或無符號,具體取決于平臺和編譯器。
十進(jìn)制數(shù)和約定
十進(jìn)制(以 10 為底)數(shù)字系統(tǒng)由十位數(shù)字組成——0、1、2、3、4、5、6、7、8 和 9——并且是一個位值系統(tǒng)。這意味著十進(jìn)制數(shù)中的每一列都有一個與之關(guān)聯(lián)的“權(quán)重”,而一個數(shù)字的值取決于它所在的列。
如果我們?nèi)∫粋€像 362 這樣的數(shù)字,那么右邊的一列代表 1(個),左邊的下一列代表 10(十),下一列代表 100(百),依此類推。 因此,當(dāng)我們看到 362 時(shí),我們將其理解為代表三個百、六個十和兩個一。
另外,當(dāng)我們用十進(jìn)制寫一個數(shù)字時(shí),我們可能會在它后面加上一個符號來表示它是負(fù)數(shù)還是正數(shù);例如,–42 和 +42。按照慣例,沒有符號的數(shù)字(例如 42)被理解為正數(shù)。
無符號二進(jìn)制數(shù)
二進(jìn)制(以 2 為底)數(shù)系統(tǒng)僅包含兩個數(shù)字,0 和 1。讓我們考慮一個包含 0 和 1 的隨機(jī)模式的 8 位二進(jìn)制字段,例如 11001010。這種位模式的含義是什么我們決定它是。例如,每個位都可以表示現(xiàn)實(shí)世界中相關(guān)燈的邏輯狀態(tài),其中 0 表示關(guān)閉的燈,而 1 表示打開的燈,反之亦然。
或者,我們可以使用我們的 8 位字段來表示一個數(shù)值。正如我們之前提到的,我們將在本專欄中考慮的兩種格式稱為無符號和有符號二進(jìn)制數(shù)。讓我們從無符號品種開始。顧名思義,我們知道無符號二進(jìn)制數(shù)沒有符號,這意味著它們只能用于表示正值。
在無符號二進(jìn)制數(shù)的情況下,右列表示 1,下一列表示 2,下一列表示 4,下一列表示 8,依此類推。還值得注意的是,在 8 位二進(jìn)制字段的情況下,我們將位編號從 0 到 7,其中位 0 稱為最低有效位 (LSB),位 7 稱為最高有效位位(MSB)。
因此,二進(jìn)制值 11001010 將等于 (1 × 128) + (1 × 64) + (0 × 32) + (0 × 16) + (1 × 8) + (0 × 4) + (1 × 2 ) + (0 × 1) = 202 十進(jìn)制。當(dāng)然,當(dāng)你習(xí)慣了這一點(diǎn)時(shí),你會跳過繁瑣的東西,簡單地說:“二進(jìn)制的 11001010 等于 128 加 64 加 16 加 2 等于十進(jìn)制的 202。”
由于我們目前討論的是 8 位無符號二進(jìn)制數(shù),這意味著我們可以存儲 2 8 = 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 個不同的 0 和 1 模式,我們可以用于表示 0 到 255 范圍內(nèi)的正十進(jìn)制值。
請注意,沒有下標(biāo)的數(shù)字被假定為十進(jìn)制。當(dāng)引用其他基數(shù)中的數(shù)字時(shí),我們通常使用下標(biāo)來反映基數(shù)(例如,11001010 2表示二進(jìn)制/base-2 值),除非它在另一個基數(shù)中的事實(shí)從上下文中是顯而易見的,例如說明它是文本中的二進(jìn)制值。
另請注意,在寫入二進(jìn)制值時(shí),我們通常顯示前導(dǎo)零 (0) 以反映計(jì)算機(jī)內(nèi)關(guān)聯(lián)數(shù)據(jù)類型或存儲位置的大小。例如,如果我們看到二進(jìn)制值 00001010,那么顯示四個前導(dǎo) 0 會立即通知我們正在使用 8 位值。
對無符號二進(jìn)制數(shù)使用 《《(左移)運(yùn)算符
正如我們之前所討論的,int類型的大小是未定義的,并且因一臺計(jì)算機(jī)而異。它的unsigned int對應(yīng)物也是如此。因?yàn)檫@會導(dǎo)致問題,現(xiàn)代 C/C++ 編譯器支持類型uint8_t、uint16_t、uint32_t和uint64_t,它們允許我們分別聲明寬度正好為 8、16、32 和 64 位的無符號整數(shù)變量。例如:
uint8_t myUintA = B00011000;
uint8_t myUintB;
這聲明了兩個名為 myUintA 和 myUintB 的無符號整數(shù),它們的寬度正好是 8 位。此外,在 myUintA 的情況下,我們還為其分配了一個 8 位二進(jìn)制值 00011000,這等于十進(jìn)制的 (1 × 16) + (1 × 8) = 24(我們也可以使用“myUintA = 24 ;”以十進(jìn)制分配值,或“myUintA = 0×18;”以十六進(jìn)制分配值)。
現(xiàn)在假設(shè)我們執(zhí)行以下操作:
myUintB = myUintA 《《 1;
這將在 myUintA 中獲取我們原來的 00011000 值,將其向左移動一位,并將結(jié)果存儲在 myUintB 中。作為其中的一部分,它將一個新的 0 移到最右邊的列中。同時(shí),最左邊的位將“掉到最后”并被丟棄。。
當(dāng)然,所有這些動作都是在計(jì)算機(jī)內(nèi)部同時(shí)進(jìn)行的。我們只是以這種方式將其拆分,以便我們更容易想象正在發(fā)生的事情。
觀察我們得到的二進(jìn)制值 00110000 等于十進(jìn)制的 (1 × 32) + (1 × 16) = 48。因?yàn)槲覀冊嫉亩M(jìn)制值 00011000 等于十進(jìn)制的 24,這意味著將其向左移動一位與將其乘以 2 相同。
事實(shí)上,向左的每一個位移都等于乘以 2。例如,記住 myUintA 仍然包含 00011000,考慮當(dāng)我們執(zhí)行以下操作時(shí)會發(fā)生什么:
myUintB = myUintA 《《 2;
這將采用我們原來的 00011000 值并將其向左移動兩位。作為其中的一部分,它將兩個0 移到最右邊的列中,而最左邊的兩個位將“掉到最后”并被丟棄。再一次,我們可以將這個序列形象化如下:
在這種情況下,我們得到的二進(jìn)制值 01100000 等于十進(jìn)制的 (1 × 64) + (1 × 32) = 96。因?yàn)槲覀兊脑级M(jìn)制值 00011000 等于十進(jìn)制的 24,這意味著將其向左移動兩位與將其乘以四(2 × 2 = 4)相同。
同樣,執(zhí)行“myUintB = myUintA 《《 3;”的操作 將我們的初始值 00011000 向左移動三位,得到 11000000,相當(dāng)于十進(jìn)制的 192。這意味著將我們的原始值向左移動三位與將其乘以八(2 × 2 × 2 = 8)相同。
當(dāng)然,當(dāng)我們開始將 1 移到值的末尾時(shí),就會出現(xiàn)問題。例如,“myUintB = myUintA 《《 4;” 會將我們的初始值 00011000 向左移動四位,得到 10000000,相當(dāng)于十進(jìn)制的 128。雖然這是一個完全合法的操作,但我們必須知道 128 不等于 24 × 16。如果這對我們來說是個問題,那么解決方案是將 myUintA 和 myUintB 聲明為 uint16_t 或更大的類型。
對無符號二進(jìn)制數(shù)使用 》》(右移)運(yùn)算符
假設(shè)我們像以前一樣聲明了 myUintA 和 myUintB 變量,但這一次,我們執(zhí)行以下操作:
myUintB = myUintA 》》 1;
這將采用我們原來的 00011000 值并將其向右移動一位。作為其中的一部分,它將一個新的 0 移到最左邊的列中。同時(shí),最右邊的位將“從末端脫落”并被丟棄。
在這種情況下,我們得到的二進(jìn)制值 00001100 等于十進(jìn)制的 (1 × 8) + (1 × 4) = 12。因?yàn)槲覀冏畛醯亩M(jìn)制值 00011000 等于十進(jìn)制的 24,這意味著將其向右移動一位與將其除以 2 相同。
事實(shí)上,每一次右移就等于除以二。例如,使用“myUintB = myUintA 》》 2;”的操作 將我們的初始值 00011000 向右移動兩位,得到 00000110,相當(dāng)于十進(jìn)制的 6。這意味著將我們的原始值向右移動兩位與將其除以四相同。
同樣,使用“myUintB = myUintA 》》 3;” 將我們的初始值 00011000 向右移動三位,得到 00000011,相當(dāng)于十進(jìn)制的 3。這意味著將我們的原始值向右移動三位與將其除以八相同。
毫不奇怪,當(dāng)我們開始將 1 移到末尾時(shí),就會出現(xiàn)問題。例如,“myUintB = myUintA 》》 4;” 將我們的初始值 00011000 向右移動四位,得到 00000001,相當(dāng)于十進(jìn)制的 1。再一次,雖然這是一個完全合法的操作,但我們必須知道 1 不等于 24 除以 16……或者是嗎?事實(shí)上,如果我們丟棄(截?cái)啵┤魏斡鄶?shù),24 除以 16 確實(shí)等于 1,這實(shí)際上就是我們在這里所做的。
有符號二進(jìn)制數(shù)
在有符號二進(jìn)制數(shù)的情況下,我們使用 MSB 來表示數(shù)字的符號。其實(shí)比這復(fù)雜一點(diǎn),因?yàn)槲覀円灿眠@個位來表示一個量。
請注意,在這種情況下,第 7 位表示 –128s 列(與其無符號對應(yīng)項(xiàng)中的 +128s 列相反)。同時(shí),其余位繼續(xù)表示與以前相同的正值。
因此,二進(jìn)制值 00011000 仍然等于十進(jìn)制的 24;即 (0 × –128) + (0 × 64) + (0 × 32) + (1 × 16) + (1 × 8) + (0 × 4) + (0 × 2) + (0 × 1 ) = 24。但是,二進(jìn)制值 11001010 以前等于無符號形式的十進(jìn)制 202,現(xiàn)在等于 (1 x –128) + (1 × 64) + (0 × 32) + (0 × 16 ) + (1 × 8) + (0 × 4) + (1 × 2) + (0 × 1) = –128 + 74 = –54 十進(jìn)制。
和以前一樣,因?yàn)槲覀兡壳坝懻摰氖?8 位二進(jìn)制字段,所以我們可以存儲 2 8 = 2 × 2 × 2 × 2 × 2 × 2 × 2 × 2 = 256 個不同的 0 和 1 模式。在有符號二進(jìn)制數(shù)的情況下,我們可以使用這些模式來表示 –128 到 127 范圍內(nèi)的十進(jìn)制值。
這種表示形式稱為二進(jìn)制補(bǔ)碼。雖然一開始可能有點(diǎn)令人困惑,但這種格式在在計(jì)算機(jī)內(nèi)部創(chuàng)建算術(shù)邏輯函數(shù)方面提供了巨大的優(yōu)勢(我們將在我們的“從頭開始構(gòu)建 4 位計(jì)算機(jī)”中更詳細(xì)地討論這些概念》系列文章)。
對有符號二進(jìn)制數(shù)使用《《(左移)運(yùn)算符int8_t、int16_t、int32_t和int64_t數(shù)據(jù)類型允許我們分別聲明寬度正好為 8、16、32 和 64 位的有符號整數(shù)變量(Arduino int數(shù)據(jù)類型等價(jià)于int16_t類型)。
例如:
int8_t myIntA = B00011000;
int8_t myIntB;
這聲明了兩個名為 myIntA 和 myIntB 的有符號整數(shù),它們的寬度正好為 8 位。此外,在 myIntA 的情況下,我們還為其分配了一個 8 位二進(jìn)制值 00011000,它等于十進(jìn)制的 (1 × 16) + (1 × 8) = 24。
現(xiàn)在假設(shè)我們執(zhí)行以下操作:
myIntB = myIntA 《《 1;
和以前一樣,這將在 myIntA 中獲取我們原來的 00011000 值,將其向左移動一位,并將結(jié)果存儲在 myIntB 中。作為其中的一部分,它將一個新的 0 移到最右邊的列中。同時(shí),最左邊的位將“掉到最后”并被丟棄。我們可以將這個序列形象化如下:
再一次,所有這些動作都在計(jì)算機(jī)內(nèi)部同時(shí)發(fā)生。我們只是以這種方式將其拆分,以便我們更容易想象正在發(fā)生的事情。再一次,我們得到的二進(jìn)制值 00110000 等于十進(jìn)制的 48。因?yàn)槲覀冊嫉亩M(jìn)制值 00011000 等于十進(jìn)制的 24,這意味著將其向左移動一位與將其乘以 2 相同。
負(fù)數(shù)呢?假設(shè)我們存儲在 myIntA 中的原始二進(jìn)制值是 11100101,它等于 –128 + 64 + 32 + 4 + 1 = –27。如下圖所示,執(zhí)行“myIntB = myIntA 《《 1;”的操作 將我們的初始值 11100101 向左移動一位,得到 11001010,這相當(dāng)于十進(jìn)制的 –128 + 64 + 8 + 2 = –54。
因?yàn)?–54 = –27 × 2,這意味著將帶負(fù)號的二進(jìn)制數(shù)左移一位與將其乘以 2 相同。
同樣,假設(shè)初始值為11100101,執(zhí)行“myUintB = myUintA 《《 2;”的操作 將產(chǎn)生 10010100,相當(dāng)于十進(jìn)制的 –108。這意味著將我們的原始值向左移動兩位與將其乘以四相同。
在這種情況下,只有將符號位的值從 0 翻轉(zhuǎn)到 1 時(shí)才會開始出現(xiàn)問題,反之亦然。這包括任何中間“翻轉(zhuǎn)”;例如,將 10111111(十進(jìn)制的 –65)向左移動兩位會得到 11111100(十進(jìn)制的 –4)。雖然符號位沒有改變(它仍然是 1),因?yàn)槲覀兛梢詫?0 想象為通過它,所以結(jié)果在數(shù)學(xué)上是不正確的,因?yàn)?–65 × 4 不會導(dǎo)致 –4。
需要注意的是,上述結(jié)果本身并不是無效的。計(jì)算機(jī)只是在做我們告訴它做的事情,我們告訴它使用的 8 位有符號二進(jìn)制數(shù)不足以容納結(jié)果,這不是可憐的小流氓的錯。假設(shè)我們使用了 int16_t 數(shù)據(jù)類型。在這種情況下,我們的起始值應(yīng)該是 1111111110111111,它仍然等于十進(jìn)制的 –65。將這個 16 位值向左移動兩位得到 1111111011111100,相當(dāng)于 –260,這是我們期望看到的。
對有符號二進(jìn)制數(shù)使用 》》(右移)運(yùn)算符
這就是事情開始變得有點(diǎn)棘手的地方,所以請坐起來,深呼吸,并注意。早些時(shí)候,當(dāng)我們對無符號二進(jìn)制數(shù)執(zhí)行左移或右移操作時(shí),這些操作稱為邏輯移位。在邏輯左移的情況下,我們將 0(零)移到 LSB;在邏輯右移的情況下,我們將 0 移入 MSB。
相比之下,當(dāng)我們對有符號二進(jìn)制數(shù)執(zhí)行左移或右移時(shí),這些被稱為算術(shù)移位。在算術(shù)左移的情況下,我們將 0(零)移到 LSB,這意味著算術(shù)左移的工作方式與邏輯左移相同。當(dāng)我們執(zhí)行算術(shù)右移時(shí),棘手的部分就來了。在這種情況下,我們并不總是將 0 移入 MSB。相反,我們將原始符號位的副本轉(zhuǎn)移到 MSB 中。
讓我們從之前使用過的示例位模式開始。假設(shè) myIntA 包含一個正符號二進(jìn)制值 00011000,相當(dāng)于十進(jìn)制的 24。觀察 MSB(最左邊的位,即符號位)為 0。現(xiàn)在讓我們執(zhí)行操作“myintB = myIntA 》》 1;”。
正如預(yù)期的那樣,我們得到的二進(jìn)制值 00001100 等于十進(jìn)制的 (1 × 8) + (1 × 4) = 12。因?yàn)槲覀冏畛醯亩M(jìn)制值 00011000 等于十進(jìn)制的 24,這意味著將其向右移動一位與將其除以 2 相同。
現(xiàn)在假設(shè)我們從包含負(fù)符號二進(jìn)制數(shù)的 myIntA 開始,例如 10110000。觀察 MSB(最左邊的位,即符號位)為 1,因此該值等于 –128 + 32 + 16 = –80 十進(jìn)制?,F(xiàn)在讓我們執(zhí)行“myintB = myIntA 》》 1;”。
在這種情況下,因?yàn)槲覀儗⒃挤栁坏母北荆?1)移至 MSB,所以我們得到的二進(jìn)制值 11011000 等于十進(jìn)制的 –128 + 64 + 16 + 8 = –40。此外,因?yàn)槲覀冏畛醯亩M(jìn)制值 10110000 等于十進(jìn)制的 –80,這意味著將這個負(fù)值向右移動一位,正如我們所期望的那樣,與將其除以 2 相同。
這里要注意的重要一點(diǎn)是,符號位將被復(fù)制到右移操作所需的盡可能多的位中。例如,如果我們從包含 10110000 的 myIntA 開始并執(zhí)行“myintB = myIntA 》》 3;”操作。
在這種情況下,因?yàn)槲覀儗⒃挤栁坏母北荆?1)移到三個 MSB 中,所以我們得到的二進(jìn)制值 11110110 等于十進(jìn)制的 –128 + 64 + 32 + 16 + 4 + 2 = –10。因?yàn)槲覀冏畛醯亩M(jìn)制值 10110000 等于十進(jìn)制的 –80,這意味著將這個負(fù)值向右移動三位,正如我們所期望的那樣,與將其除以八(萬歲)相同。
謹(jǐn)防!根據(jù)官方 C 標(biāo)準(zhǔn),右移有符號二進(jìn)制數(shù)會產(chǎn)生未定義的行為。上面描述的帶符號二進(jìn)制數(shù)右移的行為是大多數(shù)編譯器供應(yīng)商實(shí)現(xiàn)的方式,但不能保證!這就是為什么大多數(shù)標(biāo)準(zhǔn)(例如 MISRA-C)添加的規(guī)則本質(zhì)上是說“位移有符號二進(jìn)制數(shù)是禁忌”,因?yàn)榧僭O(shè)符號將被保留,可以在一個編譯器上創(chuàng)建代碼,只是為了將您的代碼移動到另一個編譯器以發(fā)現(xiàn)它不是。
審核編輯:郭婷
評論
查看更多