《深刻理解計算機系統》閱讀筆記--信息的表示和處理(上)

在開始先來看一個有意思的東西:linux

 root@localhost: lldb (lldb) print (500 * 400) * (300 * 200) (int) $0 = -884901888 (lldb) print ((500 * 400)* 300) * 200 (int) $1 = -884901888 (lldb) print ((200 * 500) * 300) * 400 (int) $2 = -884901888 (lldb) print 400 * (200 * (300 * 500)) (int) $3 = -884901888 (lldb) 

結果是負數!!!! 這個結果理論上是很是不該該的,這已經違背了咱們的常識,畢竟正數的乘積,最後的結果應該仍是一個正數,可是這裏出現負數的狀況,雖然結果不對,可是好在即便咱們各類交換順序,結果都是一致的程序員

咱們再來試試浮點數呢編程

root@localhost: lldb (lldb) print (3.14 + 1e20) - 1e20 (double) $0 = 0 (lldb) print 3.14 + (1e20 - 1e20) (double) $1 = 3.1400000000000001 (lldb) 

從結果看浮點數好像也沒好到哪裏去,也算錯了,這個時候你確定和我同樣在想,計算機機計算機,你一個以計算文明著稱的東東也計算不對了,也太不靠譜了,其實出現這種狀況是有緣由的,不知道你小時候有沒有和我同樣拿着家裏的計算器上讓幾個很是的大的數連着相乘,最後發現結果也是現實的亂七八糟,還給你不停的報錯誤錯誤,哎想一想當時若是多思考思考,去探究探究說不定本身早已經成爲大神了,哈哈哈.....windows

言歸正傳,計算機是用有限數量的爲來對一個數字編碼的,因此當結果太大以致於不能表示時,運算就會出現相似上面兩種狀況的錯誤,這裏稱爲溢出(這裏先有一個概念)。數組

整數運算和浮點運算會有不一樣的數學屬性是由於它們處理數字表示有限性的方式不一樣。整數的表示雖然只能編碼一個相對小的數值範圍,可是這種表示是精確的,浮點數雖然能夠編碼一個較大的數值範圍,可是這種表示是近似的網絡

由上面這個小問題來引出此次的內容,來好好探究探究操做系統是如何在表示和處理這些信息,爲何會出現溢出,爲何會計算錯誤,如何在本身之後寫代碼的過程當中避免一些潛在的問題,讓本身寫出更高質量的代碼編程語言

咱們學習一門開發語言的時候,開始學習基礎語法的時候都會學習各類數據類型,這些數據類型在系統中又是如何存儲的呢?接着往下看。函數

信息的存儲

二進制 十六進制 十進制
這裏關於十進制和十六進制的轉換有一個挺有意思的地方:
當值x是2的非負整數n次冪時,也就是x = 2n,能夠很是容易的將x寫成十六進制形式
其實咱們看這個時候x的二進制就是1後面跟了n個0,而十六進制數字0表示4個二進制0
先來看看幾個轉換:
當x = 32 即32 = 2^5, 5 = 4*1 + 1 轉換成十六進制爲0x20學習

當x = 64 即64 = 2^6, 6 = 4*1 + 2 轉換爲十六進制爲0x40編碼

當x = 128 即128 = 2^7 7 = 4*1 + 3 轉換爲十六進制爲0x80

當x = 256 即 256 = 2^8 8 = 4*2 + 0 轉換爲十六進制爲0x100

當x = 512 即 512 = 2^9 9 = 4*2 +1 轉換爲十六進制爲0x200

當x = 1024 即 1024 = 2^10 10 = 4*2 + 2 轉換爲十六進制爲0x400

因此從上面的規律能夠將公式總結爲i+4j j就是後面0的個數,而最前的數就是2的i次方

字數據大小

字長,指明指針數據的標稱大小,虛擬地址是以這樣一個字來編碼的,字長決定了虛擬地址空間的最大大小。
咱們總是聽到別人說系統是32位,系統是64位的,其實就是和這個相關,若是是32位的機器虛擬地址的範圍爲:

0-2^32-1 程序最多訪問2^32個字節,64位同理,這裏你也就明白了64位可以支持更大的內存空間,32位字長限制虛擬地址空間爲4千兆字節即4GB,這也是早起不少電腦的標配,由於32位的系統,你安裝再多的內存,你右鍵電腦屬性,看到識別的依然只是不到不到4GB,而64位的虛擬地址空間爲16EB,因此你能支持更多的內存。

 

 

上圖是32位和64位典型值,整數或者有符號的,便可以表示負數,零和正數;無符號的只能表示非負數

尋址和字節順序

在大多數計算器上,對於多字節對象都被存儲爲連續的字節序列,對象的地址爲所使用字節中最小的地址,這裏有個例子假設一個int的變量x的地址爲0x100 也就是地址表達式&x 的值爲0x100 那麼x的4個字節將被存儲在內存0x100,0x101,0x102和0x013位置

而這個地址就涉及到一個概念就是尋址的問題,這裏兩種方式:大端法和小端法
假設變量x 的類型爲int, 位於地址0x100 它的十六進制值爲0x01234567 地址範圍0x100-0x103的字節序列兩種尋址方式以下圖表示:

大端法和小端法 對咱們大多數程序員實際上是不可見的,咱們也不多關心這個東西,可是在如下三種狀況是須要注意的:

第一種:不一樣類型的機器之間經過網絡傳遞二進制數據時,也就是當小端法機器給大端法機器發數據或者返回來發送數據,在接收數據的時候,字節順序對接收者來講都是反的,因此爲了不這個問題出現,網絡應用程序的代碼編寫應該遵照已經創建的關於字節順序的規則

第二種:主要是於都表示整數數據的字節序列時字節順序也是很是重要,主要發生在檢查機器級程序時。

第三種:當編寫規避正常的類型系統的程序時。在C語言中一般會使用強制類型轉換cast或者聯合union來容許一種數據引用一個對象,而這種數據類型與建立這個對象時定義的數據類型是不一樣的。

其實上面三種狀況咱們做爲一名普通的開發者也不多回去關注,畢竟高級語言已經對作了更高級的抽象,同時替咱們也作了不少事情來規避一些錯誤的發生


在這部分的練習題中有個挺有意思的題:
這裏已經計算的出整數3510593的十六進制爲0x00359141
而浮點數3510593.0的十六進制爲0x4A564504
咱們先看看這兩個十六進制的二進制表示分別爲:
00000000001101011001000101000001
                   *********************
    01001010010101100100010100000100
咱們發現中間有星號的部分是徹底同樣的,而且咱們整數的除了最高位1,其餘全部位都嵌在浮點數中,這是巧合麼,固然不是啦,繼續深刻研究

 

表示字符串

C語言中字符串被編碼爲一個以null其值爲0字符結尾的字符數組,每一個字符都由某個標準編碼來表示
最多見的是ASCII字符編碼,使用ASCII碼做爲字符碼的任何系統上都將獲得相同的結果,與字節順序和字大小無關。也正是這樣文本數據比二進制數據具備更強的平臺獨立性

表示代碼

其實咱們的代碼在不一樣類型的機器上編譯時,生成的結果也是不一樣的,因此你在linux上編譯的代碼確定是不能再windows上運行的,反之亦然

布爾代數

與 And:A=1 且 B=1 時,A&B = 1
或 Or:A=1 或 B=1 時,A|B = 1
非 Not:A=1 時,~A=0;A=0 時,~A=1
異或 Exclusive-Or(Xor):A=1 或 B=1 時,AB = 1;A=1 且 B=1 時,AB = 0

對應與集合運算則是交集、並集、差集和補集,假設集合 A 是 {0, 3, 5, 6},集合 B 是 {0, 2, 4, 6},全集爲 {0, 1, 2, 3, 4, 5, 6, 7}
& 交集 Intersection {0, 6}
| 並集 Union {0, 2, 3, 4, 5, 6}
^ 差集 Symmetric difference {2, 3, 4, 5}
~ 補集 Complement {1, 3, 5, 7}

邏輯運算

C語言提供了邏輯運算符|| && ! 分別對應命題邏輯中的OR AND NOT 運算
邏輯運算任務全部非零的參數都表示TRUE, 而參數0表示FALSE
邏輯運算符和對應的位級運算的第二個重要區別是:若是對第一個參數求值就能肯定表達式結果,那麼邏輯運算符就不會對第二個參數求值

位移運算

表達式x << k 表示x想左移動k位 ,x向左邊移動k位,丟棄最高的k位,並在有點補k個0
表示是x << k 這個分兩種:邏輯右移(左邊補0) 和算術右移(右邊補符號位)

如今幾乎全部的編譯器或者機器組合都對有符號使用算術右移面對無符號數,右移必須是邏輯的

整數的表示

咱們對整數主要分爲:有符號和無符號

先記一些術語:

 

 

 關於32位程序上C語言以及64位程序上C語言的典型取值範圍:

 

 

 

 

在上面兩個圖中咱們均可以看出負數的範圍比正數的範圍大1,爲啥會這樣的,繼續往下看

無符號數的編碼

下面是幾種狀況B2U 給出的從爲向量到整數的映射

 

 

因此咱們能夠考慮w位所能表示的值的範圍,最小值用位向量表示[000...0] ,也就是整數值0
而最大值的表示則是2^w - 1

補碼編碼

其實在不少時候咱們仍是但願用到負數,最多見的有符號的計算機表示方式就是補碼形式

最高有效位解釋爲負權 用函數B2T表示補碼編碼

最高有效位稱爲符號位,它的權重爲-2^w-1 是無符號表示中權重的負數

符號位被設置爲1 時,表示爲負,當設置爲0 時表示爲非負,經過下面理解:

這個時候再看補碼所能表示的值的範圍:

最小值的的位向量爲[1000...0] 其整數值爲-2^w-1

最大值的位向量爲[01111...1] 其整數值爲2^w-1 - 1

咱們仍是以4位表示:

TMin = B2T([1000]) = 2^3 = -8

TMax = B2T([0111]) = 2^2 + 2^1 + 2^0 = 4+2+1 = 7

同無符號表示同樣,在可表示的取值範圍內的每一個數字都有一個惟一的w位的補碼編碼

這個屬性總結爲一句話:補碼編碼的惟一性

 

小結:其實咱們經過上面的無符號的編碼和補碼編碼就能夠看出,補碼的範圍是不對稱的

|TMin| = |TMax| + 1

咱們學習編程語言的時候,通常在基礎部分都會講到關於整數和負數的表示範圍,尤爲是強類型語言中

當時老是說負數表示的最大範圍一直被-1 當時不少時候老師都會告訴你是由於符號位佔了一位,當時多是一個模糊的概念,爲啥是符號位佔了一位,從補碼的這個概念,其實你就應該徹底明白了爲啥符號位佔了一位

其次這裏咱們還能夠知道一個規律就是無符號數值恰好比補碼的最大值的2倍 再加1:UMax = 2TMax + 1

 

 

有符號和無符號之間的轉換

c語言容許在各類不一樣的數字數據之間作強制類型轉換

其實在c語言中,強制類型的轉換的結果是保持位值不變,只是改變了解釋這些位的方式

-12345 的16 位補碼錶示與53191 的16位無符號表示是同樣的
上面數字太大了,經過簡單的數字來表示,可能更好理解:
對於數字16 ,二進制表示爲1111 十六進制表示爲0xF
這個時候的UMax 的值爲:16,TMin 的值爲:-8,Tmax 的值爲7 
其實這個時候還有一個有意思的點是,若是就是這個4位的話,表示-1 的表示方式:
二進制形式爲:1111 發現其實和 最大的無符號數的表示方式是同樣的

因此在c語言中,假設咱們定義了一個無符號的數 u= 4194967295 ,若是咱們經過(int)u 進行強制轉換,咱們獲得的結果就是-1,代碼內容以下:

#include <stdio.h>

int main() { unsigned u = 4294967295u; int tu = (int)u; printf("u=%u,tu=%d\n",u,tu); return 0; }

程序的打印的結果和咱們上面說的是一致的,這裏須要知道的是4294967295 這個數字是32位能表示的最大數字,即這個是32位中的Umax
再看一個代碼:

#include <stdio.h>

int main() { short int v = -12345; unsigned short uv = (unsigned short)v; printf("v = %d, uv = %u\n",v, uv); return 0; }

從執行結果能夠看出v = -12345, uv = 53191
從上面的兩個例子,均可以看出強制類型轉換的結果都是保持位值不變,只是改變了解釋這些位的方式

而且咱們知道的是-12345 的16位補碼錶示與53191的16位無符號表示是徹底同樣的。咱們代碼中將short強制類型轉換爲unsigned short 改變了數值,可是不改變位表示

小結:
對於大多數C語言的實現,處理一樣的字長的有符號和無符號數之間相互轉換的通常規則是:
數值可能會改變,可是位模式不變
這裏位是固定的,假設爲w位,給定0<=x<=UMax 範圍內的一個整數x, 函數U2B 會給出x的惟一的w位無符號表示,一樣的,當x知足TMin<=x <=TMax 函數T2B 會給出x的惟一的w位的補碼錶示

如今將函數T2U 定義爲T2U = B2U 也就是這個函數的輸入是一個TMin - TMax 的數,而結果獲得的是一個0-UMax的值,這裏兩個數有相同的位模式,除了參數是無符號的,而結果是以補碼錶示的
一樣的對於0-UMax 之間的值x ,定義函數U2T 爲U2T = B2T 生成一個數的無符號表示和x的補碼錶示相同

從上圖咱們能夠看出T2U(-12345) = 53191 而且 U2T(53191) = -12345
因此十六進制表示寫做0xCFC7 的16位位模式及時-12345的補碼錶示,又是53191的無符號表示。同時咱們須要注意12345 + 53191 = 65536 = 2^16
也就是說,無符號表示中的UMax 有着和補碼錶示-1相同的位模式,這二者之間的關係:1+UMax(w) = 2^w 注意:這裏的w表示位數

 

 

擴展一個位表示

一個常見的運算是在不一樣的字長的整數之間轉換,同時保持數值不變。
可是若是目標數據類型過小以致於不能表示想要的值時,就會出問題了,然而,從一個較小的數據類型轉換到一個比較大的類型,老是能夠的

要將一個無符號數轉換爲一個更大的數據類型,只須要在表示的開頭添加0 這種運算被稱爲零擴展

要將一個補碼數字轉換爲一個更大的數據類型,只須要在表示的開頭添加最高有效位的值,這種運算稱爲符號擴展

能夠經過下面的例子理解:
給出字長w= 3 到w = 4的符號擴展的結果位向量[101]表示值-4+1=-3,由於這裏是對補碼的擴展,因此應用的是符號擴展,獲得爲向量[1101] 表示的值-8+4+1 = -3 擴展以後咱們獲得的值仍是-3 ,相似的向量[111] 和[1111]都表示-1

截斷數字

咱們在代碼中一般有時候都會用到強制轉換,即將高位向低位轉換

 

 

總結

有符號到無符號的隱式強制轉換會致使某些非直觀的錯誤,從而致使咱們本身的程序出現咱們意想不到的錯誤
而且這種包含隱式強制類型轉換的細微差異很難被發現。

經過代碼可能更好理解:
這個代碼中,函數sum_elements好的參數length 爲數組a的長度,若是咱們正常賦值這個代碼不會有任何問題,可是若是在整個項目中,你傳遞參數的時候,length傳遞的不是數組a的長度,而傳遞了0 這個時候length -1 就會變成負數,可是最開始咱們定義length的時候定義的是無符號的,因此就會變成當前位數的最大值即UMax 因此《= 是老是知足條件的,這個時候你再取數組的值的時候就會超出數組的最大長度,程序就會出現異常錯誤。

#include <stdio.h>

float sum_elements(float a[], unsigned length) { int i; float result = 0; for (i =0; i< length -1; i++) result = a[i]; printf("%f",result); return result; } int main(){ float a[5] = {1.1,2.3,1.4,3.22,1.24}; sum_elements(a,5); }

其實上面的這個狀況也是有符號到無符號數的隱式轉換會致使錯誤或者漏洞的方式,避免這類錯誤的一種方法就是絕對不使用無符號數,而實際上除了C之外也不多語言支持無符號整數

相關文章
相關標籤/搜索