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