5.8. 進位.2
5.8.1十六進位
說
到十六進位,首先會問:總共只有10個阿拉伯數字:0、1、2、3、4、5、6、7、8、9,如何表達“逢16進1”的概念呢?方法是用英文字母(大小寫
均可):A、B、C、D、E、F表達10~15。所以,如果我告訴你:這是一個數:“17FCA0”,你肯定能猜到它是一個十六進位的數,不過如果我說的
是:“12390”,就不好區分是什麼進位了。
在C++語言中,並不支援直接在代碼中寫一個2進位數(後面談為什麼),但倒是支援直接寫一個十六進位的數,為了能和十進位數區分,C++規定十六進位數需要一個首碼:“0x”(數字0和小寫字母x),比如:
int a = 0x17FCA0;
為
什麼已經有十進位數了,還要再搞出個十六進位數呢?是不是現實生活中有這個需求,比如有什麼東西的計數特別適合用“十六進位”呢?生活中確實不僅僅只有十
進位。比如“襪子”就很像二進位計數:沒有襪子是0隻襪子,1隻襪子是1隻襪子,2隻襪子……逢二進一:成1雙襪子,果然只需要0和1。
〖小提示〗:生活中的各種進位
時間是60進位,角度也是60進位。說到“一打東西”時,用的是12進位。請各位課後可以想想還有什麼進位。
其
實十六進位的存在,純粹是為了二進位。二進位是肯定必定以及一定要存在的,因為機器用的就它。但位元實在太“佔位置”了,雖然最直觀,但讀寫都不方
便。如果用十進位呢——當然,編程式我們最常用還是十進位,僅當要需要表達和機器相關的資料,一些記憶體位址時,才會考慮二進位——十進位數和二進位轉換比
較複雜,十六進位就不一樣了,它和二進位的之間的轉換非常簡單快速。
一個數用二進位表達,實際就是由多個“2的N次方的數”相加。因為16是2的4次方,因此非常這兩個進位之間的轉換特別容易(想想10是2的幾次方呢?)。
或
許還會問:還有很多數是2的整數次乘方啊,比如8是2的3次方,32是2的5次方,為什麼偏偏是16呢?原來。一個位元組(byte)是8位,但用“2的8
次方”作為進位,顯然太大了。另外,我們還會有把一個位元組分為“高位元組(高4位)”和“低位元組(低4位”的需要,所以,用16(2的4次方)來作為進位最
方便。說千道萬,試一試會更清楚些:下面就讓我們做一些位元與十六進位數的互換運算。
我們首將就以“半個位元組(4位)”為例,看看4位的位元如何轉換成十六進位。因為是剛開始,所以我們先保留轉換到十進位的中間步驟,但慢慢的,我們要學會直接轉換。
1000(2) => 8+0+0+0 = 8 => 0x8
1001(2) => 8+0+0+1 = 9 => 0x9
1010(2) => 8+0+2+0 = 10 => 0xA
1111(2) => 8+4+2+1 = 15 => 0xF
最後一個例子最說明問題:轉換位元,請以4位為一組,然後從高位看到低位,各位的權值依次是:“8、4、2、1”。如果該位是1,就加上權值,否則不加,就可以得到一個十進位數,然後再快速換算成十六進位(要求你熟記A~F對應的十進位值)。
真正能體現十六進位的方便,在於位元更多時,比如當面對一個完整的一個位元組時,比如(為了不引起眼花,我們每4位加一個空格): 0101 1110。這個位元如何轉換成十六進位呢?
第一種方法:轉換成十進位數,再轉換為十六進位:
先換算成十六進位:0101 1110 = 0 + (2的6次方) + 0 + (2的4次方) + (2的3次方) + (2的2次方) + (2的1次方:2) + 0 = 94。
還得把十進位的94換算成十六進位,如何算?我們還沒教呢,這裡先給出答案:94 = 0x5E。
〖課堂作業〗:體驗位元換算至10進位的“笨方法”
請拿起筆,立即用以上方法中步驟一,將以下位元換算成十進位數:
8位元:11011100、01110110、10010101、11011101;
16位元:1101101000110101
第二種方法:每4位直接換算成十六進位數,得到的就是結果:
高4位:0101(2) -> 4+1 -> 0x5
低4位:1110(2) -> 8+4+2 -> 0xE
結果就是:0x5E。
來一個痛點的:16位的位元:1101101000110101,如何換算成十六進位呢?
首先,每4位拆成一組:1101 1010 0011 0101
1101 -> 13 -> 0xE
1010 -> 10 -> 0xA
0011 -> 0x3
0101 -> 0x6
結果就是:0xEA36。
十六進位數的換算成位元,也有兩種方法,不過我們直接學習“聰明”的方法, “笨”的方法,留到後面“進位換算”時再提。
前面給出“8、4、2、1”口訣,現在只需要學會給出一個16以內的數,你能拼出它的位元即可,比如:
0xA -> 10 = 8 + 2 -> 1010(2)
0xD -> 13 = 8 + 4 + 1 -> 1101(2)
0xF -> 15 = 8 + 4 + 2 + 1 -> 1111(2)
再如:
0xC9D5 要換算成進二進位,類似:
0xC -> 12 = 8 + 4 -> 1100(2)
0x9 -> 8 + 1 -> 1001(2)
0xD -> 1101(2)
0x5 -> 4 + 1 -> 0101
結果是:11001001 11010101。請特別注意最後0x5的轉換,不足4位,前面補0。
有關十六進位的內容,我們就一直講它如何與二進位制互換?沒錯,因為在我們編程時,我們絕大多數要把十六進位就當成是二進位的一個馬甲——位元穿上這馬甲,圖的就是讓人類看起來舒服一些,僅此而已。
5.8.2.八進位
八進位,二進位的另一個小尺寸的馬甲。不過我們用得並不多。關於它,首先要說的就是:在C++程式中,當一個數字以0開始時,它就是八進位。這是一件不太直觀的事情。
int a = 010; //對不起,這不是4,也不是10,它是8。
八進位,使用了0~7這8個阿拉伯數字。逢8則進1,所以看到八進位的“10”,你應該可以換算出它其實是一個十進位的8。八進位各位的權值,以8為基數。下面的算式示範了八進位的173如何換算成十進位的123。
173(8) -> 1×(8的2次方:64) + 7×(8的1次方:8) + 3 -> 123(10)
我
們再看看:一個二進數,如何換成八進位:“11101111”
是一個8位的位元。我們從低位往高位,每三位分一組(想想,為什麼是每三位),得到:11、101、111,然後每仿效換成十進位(但3位的二進位
數,肯定不會超過7,所以,說是換算成八進位,或許更好理解):
11(2)-> 3
101(2) -> 5
111(2) -> 7
結果是:357(8)。
八位元換算成位元,應該是張口就來。請熟記八位元,每個數的二進位值:
| 7 -> 111(2) |
3 -> 011(2) |
| 6 -> 110(2) |
2 -> 010(2) |
| 5 -> 101(2) |
1 -> 001(2) |
| 4 -> 100(2) |
0 000(2) |
(表格 4 八位元與位元速換表)
或許,你頭腦裡閃現出一個,八位元與十六進位數互換的方法了,沒有嗎?
5.8.3.進位換算
先說一下前小節的答案吧。八位元與十六進位數互換的方法之一,就是用二進位做橋樑。
以下是我們已經學會的換算過程:
二進位 -> 八進位、十六進位
十六進位、八進位 -> 二進位
二進位、八進位、十六進位 -> 十進位
本節我們主要學習,十進位數,如何換算成其它進位,它們都有一個統一的方法,稱為“連除法”。
將十進位數除以2,得到商和餘數,將商再除以2,直到商為0;然後將本過程中得到的餘數(肯定不是0就是1),先得到的排後邊,後得到的排前邊,就是結果了。
例子:將6換算成位元:
| 運算式 |
商 |
餘數 |
| 6÷2 |
3 |
0 |
| 3÷2 |
1 |
1 |
| 1÷2 |
0 |
1 |
表格 5 十進位數6換算成二進位過程表
將餘數按得到的先後,倒序排列,得到“110”,正是6的二進位表達。
如果要應付考試,慢騰騰地畫上面那張表,顯然會急死人,所以通常是在草稿紙上如所示進行演算:
圖 5-22 連除法演算過程草圖
方法類似換算成位元,只不過除數變成8。
例子:將289換算成八位元:
| 運算式 |
商 |
餘數 |
| 289÷8 |
36 |
1 |
| 36÷8 |
4 |
4 |
| 4÷8 |
0 |
4 |
表格 5-6 十進位轉八進位樣本
將餘數按得到的先後,倒序排列,得到“441”,正是289的八進位表達。
方法類似換算成位元,只不過除數變成16。
例子:將872換算成十六進位數:
| 運算式 |
商 |
餘數 |
| 874÷16 |
36 |
1 |
| 54÷16 |
4 |
4 |
| 3÷16 |
0 |
4 |
表格 5-7 十進位轉十六進位樣本
將餘數按得到的先後,倒序排列,得到“0x36A”,正是874的十六進位表達。
5.8.4.浮點數
在C++程式中,以“位”的形式訪問一個整數(尤其是不帶正負號的整數),比較常見,比如“位移”操作(將一個位元中的每一位,同時向左向右移動若干位)、或者按位“與”、“或”、“異或”等。但我們很少對一個“非整數”進行此類操作。
“
浮點數/floating point
numbers”是電腦用來“近似”表達實數的一種方法。中學時學習的“科學計數法”,就有點類似。另外,並不能將“浮點數”和“N進位”二者可以是表
達同一個數的兩上不同切入點,換句話說,存在十進位的浮點數,也存在二進位的浮點數。要不,我們就來看一個十進位數的浮點標記法:
9.57 × (10的2次方)
這個數是什嗎?其實它就是實數“957.0”的浮點標記法。
一個規範的“浮點數”表達方式,需要滿足如下特徵.
運算式:d.dd...d × (B 的 e 次方)。
其中,d.dd...d 稱為“尾數”(更通俗的說法是:“有效數字”),B 稱為“基數”,e稱為“指數”。
對於不同進位,則首先表現在B的值,B是2,就是二進位,是10,就是十進位。當然,“尾數”中每一個d的取值範圍,也要隨進位變化而變,當為十進位數,d就是0~9,為位元時,d就只能是0~1。
再者,“尾數”中的整數部分(即小數點的左部),不能為0。因此,類似下面的數,它們都不是規範的浮點數:
10.123 × (10的2次方)
2.0101 × (2的3次方)
0.0134 × (10的-1次方)
〖課堂作業〗:浮點數的規範表達
請指出三個浮點數運算式的錯誤,並以正確方式寫出(數值大小必須保持一致)。
如果明確使用二進位表達的話,則是:d.dd...d × (2 的 e 次方)。其中每一位d都只能是0或1,同樣,整數部分不能是0。
第一個規範的浮點數,都可以通過以下運算式計算得到:
±(d0 + d1×(B的-1次方) + d2×(B的-2次方) + d3×(B的-3次方) + ... + dn×(B的-n次方))×(B的e次方)
別被這個長長和式子嚇倒!它不過是讓我們回想起小學時光而已。我們舉一個例子,假設有一個十進位的浮點數:3.456×(10的3次方)
我用“0.1”表示“10的-1次方”,則有:
則有:3.456 = 3 + 4×0.1 + 5×0.01 + 6×0.001
所以:3.456×(10的3次方) = (3 + 4×0.1 + 5×0.01 + 6×0.001)×(10的3次方)。
沒錯,小學時,我們都會念:“小數點之後第一位,表示0.1,第二位,表示0.01,第三位,表示0.001……”那二進位呢?假設也用最直觀的方式表示一個帶小數的二進位:
1111.1111
2的0次方(=0),1次方(=1),2次方(=4),3次方(=8)……
小數點往右,則是——
2的-1次方(=0.5),-2次方(=0.25),-3次方(0.125)、-4次方(=0.0625)……
〖課堂作業〗:0.1 如何表達?
不需要寫出“尾數”、“指數”,請以前述內容為例,請嘗試寫出十進位數0.1的小數部分,如何二進位表達:0.000______。
顯然,小數點後面三位,必須都為0,因為它們權值,都比0.1大,然後大概可以這樣拼湊:
首先我們讓2的-4次方,加上2的-5次方:
0.0625 + 0.03125 = 0.09375。
很
接近了,但後面不能再加上2的-6次方(0.015625),加上就超出0.1了,加2的-7次方(0.0078125)也不行……加上2的-8次方
(0.00390625),得到0.09765625,越發逼近,但它終究還不是0.1。沒關係,這樣一直拼湊下去,或許就能剛好湊出一個0.1。如果一
直湊不出來呢?也沒關係嘛,讓我們用上高中的數學知識,假如允許我們這樣無限地——注意,是無限地——湊下去,那麼什麼數都可以逼近逼近,直到在數學被認
為相等。
〖小提示〗:0.9999999…… 等於1嗎?
高中在學習“極限”時,數學老師一定告訴過你了,當0.9999……後面有無窮盡個9時,那麼0.9999999……就等於1。
可惜,“極限”存在於數學世界中,在現實中,電腦的記憶體容量不僅不是“無窮盡”的,而且是寶貴的,所以,再說一次:“浮點數是電腦用來‘近似’表達實數的一種方法”。如果以四個位元組來儲存,那麼總共才32位。
當
然,似乎也並不是所有數字,都無法精確地用浮點表達,比如,0.5,那就是2的-1次方,非常精確。但是,這也只是說,你直接在代碼裡,寫一個0.5時,
它是精確的,如果它是通過一個“算式”計算得到,而該算式中又存在一些個無法精確表達的浮點數,比如:0.01×10.0÷0.2,誰也不好保證,它能在
電腦中得到一個精確的0.5。這就是比較浮點數時,所需要注意的精度問題,請看下面代碼:
if (0.01f * 10.0f / 0.2f == 0.5f)
{
cout << "yes!" << endl;
}
else
{
cout << "no." << endl;
}
C++
程式用*代表乘號,/代表除號。而“0.01f”末尾的f,表示這是一個單精確度的浮點數。上面程式運行時,將輸出“no.”。也就是說,此時
“0.01×10.0÷0.2”並不等於0.5。這種問題如何解決呢?我們並不急於在此處提出答案,重要的是,你必須記住這一問題,並理解它的原因。
〖小提示〗:電腦不可信了嗎?
曾經有學生問我,我一直以為,電腦計算比人類精確多
了,可是今天看起來電腦的計算也很不可信啊?連小學生都會的題,它也能算錯?放心吧,儘管浮點數不精確,但它也是一種“非常精確的不精確”,問題完全可
溯可查可解決。另外,我看到一些不知其所以然的程式員,認為寫一個這樣的判斷: “if (0.1 == 0.1)
...”,也要擔心個半天,放心吧,如果這樣的判斷出錯,那電腦真的可以扔了。
電腦通常提供兩種精度的浮點數表達方法,其中一種不太精確,另一種則更不精確——噢,這樣說有點消極,重來:“其中一種不太精確,另一種則比較精確。”。不太精確的,就是“單精確度浮點數”,比較精確的,就是“雙精確度浮點數”。
C++用float類型表示單精確度浮點數。它佔用4個位元組,合32位。其中符號、指數、尾數各自佔用的位域如所示:
圖 5-23 單精確度浮點數位域
如果代碼中寫一個字面常量的浮點數,需要在數字尾部,加上字母f,比如:0.1f,或10f,預設情況下,C++將字面上的一個帶小數字,當成雙精確度浮點數。
C++用double類型表示雙精確度浮點數。它佔用8個位元組,合64位。其中符號、指數、尾數各自佔用的位域如所示:
圖 5-24 雙精確度浮點數位域
當在代碼中寫一個數字,如果不帶小數,則預設是整型數,比如:10,如果加上小數點,比如:10.0,並且沒有尾隨字母f,則C++將之當成double類型。
以上並不是有關浮點數表達的全部知識,要表達一個浮點數,還需要很多約定。考慮到編寫C++程式時,我們只會(也只允許)對整數類型(包括int, long, char等)的資料進行按位操作,浮點數的基礎知識,我們暫時只講到這裡。
〖課堂作業〗:浮點數精度練習
請將前述有關“0.01×10.0÷0.2”判斷是否等於0.5代碼,寫成一個控制台測試專案。並且分別針對float類型和double類型進行測試,看看結果是否有區別。