負數位運算的右移操作-C語言基礎
這一篇探討的是“負數位運算的右移操作”,涉及到資料的原始碼、反碼、補碼的轉換操作。屬於C語言基礎篇。
先看例子
#include <stdio.h>
int main(void) {
//正數的位右移
//補碼0000 0101
int x = +5;
//正數補碼右移兩位後
//補碼0000 0001
printf("+5>>2 = %d\n", x>>2); //+5>>2 = 1
//負數的位右移
//補碼1111 1011
int y = -5;
//負數補碼右移兩位後
//補碼1111 1110
printf("-5>>2 = %d\n" , y>>2); //-5>>2 = -2
}
輸出結果。
好了,現在來解釋一下這個輸出結果是怎麼來的。
在討論負數的右移之前,我們先要了解一下什麼是原碼
、什麼是反碼
、什麼是補碼
。
任何一個數據都有其唯一對應的原碼、反碼以及補碼。計算機對於資料的處理都是以補碼
形式來進行的(至於為什麼要這樣就又可以展開一整篇文章來說明了,這裡就不深入討論了)。而且正數和負數的原反補碼的轉換規則是不一樣的。
對於正數來說,其原、反、補碼都是相同的,都是該資料的二進位制形式
。例如+5的原、反補碼均為0000……0101(其中0的個數由該資料的型別以及計算機作業系統的位數決定)。其最高位為0表示正數
對於一個有符號數(既不加unsigned修飾的資料型別)來說,其最高位便是它的符號位。由於有符號數的最高位為
符號位
,這就使得char型別有符號數的取值範圍為-128~+127而不是0~256。
對於負數來說,其原碼是在其正數原碼
的基礎上,最高位改為1以表示負數。所以-5的原碼為1000……0101。負數的反碼是在保持原始碼符號位不變的情況下其餘位取反。而負數的補碼是其反碼加1。
現在,我們知道了原碼、反碼、補碼都是些什麼了,那麼這個例子的結果就很好分析了。
首先是 +5>>2 = 1
+5
原碼 0000 …… 0101
補碼 0000 …… 0101
>>2(正數右移高位補0)
補碼 0000 …… 0001
原碼 0000 …… 0001
= 1
然後是 -5>>2 = -2
-5
原碼 1000 …… 0101
反碼 1111 …… 1010 負數的反碼是保留符號位不變原始碼取反
補碼 1111 …… 1011 補碼是反碼加1
>>2 (負數右移高位補1)
補碼 1111 …… 1110
反碼 1111 …… 1101 補碼轉反碼減1
原始碼 1000 … 0010 負數反碼轉原始碼保留符號位不變取反
= -2
以上就是有符號數的右移操作了,隨便說一下“有符號數的左移
”以及“負數的無符號右移
”(沒有“無符號左移
”這個說法,因為左移是在低位補0,而符號位在高位,左移之後補的資料不能影響最終的符號)
有符號數的左移
無論是有符號的正數還是負數,其左移都是在其補碼的基礎上面左移,而且低位都是補0。看到這裡,你是否就意識到了一個問題,“如果正數的補碼在左移的過程中,剛好有一個1移到了最高位,那麼是否就會變成了負數呢?”嗯,這種情況確實是會發生的。負數左移到一定值的時候也會變成正。
無符號右移
注意:在C語言中是沒有“無符號右移
”運算子的,在Java中用“>>>”表示,C語言中可以利用“((unsigned int)(-5))>>n”來實現
無論是正數還是負數,其無符號右移都是在其補碼的基礎上右移,高位補0。
例如
-5
原碼 1000 …… 0101
反碼 1111 …… 1010 負數的反碼是保留符號位不變原始碼取反
補碼 1111 …… 1011 補碼是反碼加1
>>>2(無符號右移,高位補0)
補碼 0011 …… 1110
反碼 (此時符號位已經變為0了,系統會當成正數來處理,原反補碼均一樣)
原碼 0011 …… 1110
= 這個不好說,得看作業系統的位數(在我這裡int為32位,
結果為:107374182==>00111111 11111111 11111111 11111110)
題外話(純屬瞎扯,感興趣的可以看下)
為什麼我們要討論資料的移位操作呢?關於這點,我想要說一些不太恰當的題外話。
雖然對於嵌入式的開發,倫理上來說應該會經常涉及對於資料的位操作才對。曾經有個老師是這麼對我說的“學微控制器其實就是在學位操作”,但是我對於這句話的解讀卻不是太贊成,我覺得應該這麼說才更符合現在的開發環境,“學微控制器其實就是在學習控制位操作。”為什麼這麼說呢?我不太嚴謹的說一下自己的看法吧!雖然,移位操作在AT89S51的開發中表現的很明顯。但是如今AT89S51的應用場景已經越來越少了。移位操作在STM32的開發中就表現的不太明顯,由於在STM32的開發過程中,我們大多數時候都是用庫函式來完成,利用了別人已經封裝好的函式來開發。而且這些庫函式大多數情況下還不僅僅是封裝一層。這雖然大大提高了程式設計師開發時的方便性,不過也造成了初學者對於自己寫的程式是如何指導晶片正常工作的中間過程模糊不清。如果你不用庫函式來操作,效率又太低,所以大多數開發人員都還是用現成的庫函式來開發。移位操作用的較少。
移位操作在底層的開發中(特別是組合語言)用的很多,但在在應用層上面應用的比較少了。要求的掌握程度幾乎處於“知道有這麼一回事,接觸到的時候,能想的起來知道它幹了什麼就行” 的地步。很多開發人員就只在剛剛開始學習程式設計的時候以及去面試做筆試的時候接觸過資料的移位操作,之後就再也用不上了。但是對於類似資料移位的這種應用範圍比較窄的知識點,我想要說的是,希望大家在學習的過程中多留一個心眼,不要覺得不重要不常用就不去重視。因為這些知識點往往能在某些特定場景下面有奇效。就例如移位操作的妙用。
在介紹這個妙用之前,我們需要知道一個前提。
計算機在處理資料的時候,處理加、減、乘、除所需要的時間是不一樣的,其中加減所需要的時間和移位操作幾乎是一樣的可以忽略不計,但是乘法需要的時間卻是加法的十到二十倍。而除法所需要的時間幾乎是加法的二十到三十倍。具體是多少,這個不好說,在不同的機器上面是不一樣的。但可以確定的是,乘除所需要的時間總是比加減移位多。特別是除法。
在剛剛開始接觸資料的移位操作這個知識點的時候,老師就已經和我們說過移位操作的結果和原資料之間的關係。
其關係大概如下:
對於正數在不溢位的情況下的左移和右移(由於負數的左移和右移後得到的資料和原資料之間的關係不明顯,所以僅僅討論正數)
左移:
左移n位後的資料 = 原資料乘2n
右移:
右移n位後的資料 = 原資料除2n
一開始瞭解到這個知識點的時候,我對此是很不屑的,因為實在是太侷限了,即便移位操作比乘除快很多,但是這個乘的數值或除的數值必須是2^n也太雞肋了吧。而且如今計算機的運算速度這麼快,這點運輸速度之間的差異實在不算什麼。
但是在後來我看的一個例子中,利用移位操作卻有奇效。在這個例子中,有兩個關鍵因素使得移位操作比乘除運算好。一個是對於運算結果精度要求不高,另外一個是運算的資料量巨大。這個例子展開來說,又是一整篇文章了。感興趣的話可以看一下這篇文章。雖然在這個例子中,移位操作並不能真正解決最終問題。但是,卻可以給我們一個啟發“在某些場景下,一些平時不受待見的冷門知識點,卻出奇的會很好用”。