深入理解計算機系統第二章(完整!!)第一節 信息存儲

第二章 信息的表示和處理

 現代計算機存儲和處理的信息以二值信號表示。 這些微不足道的二進制數字, 或者稱爲位(bit), 形成了數字革命的基礎。 大家熟悉並使用了 1000多年的十進制(以 10爲基數)起源千印度, 在12世紀被阿拉伯數學家改進, 並在13世紀被意大利數學家Leonardo Pisano(大約公元1170-1250, 更爲大家所熟知的名字是Fibonacci)帶到西方。 對千有 10個手指的人類來說, 使用十進制表示法是很自然的事情, 但是當構造存儲和處理信息的機器時, 二進制值工作得更好。 二值信號能夠很容易地被表示、 存儲和傳輸, 例如, 可以表示爲穿孔卡片上有洞或無洞、 導線上的高電壓或低電壓, 或者順時針或逆時針的磁場。 對 二值信號進行存儲和執行計算的電子電路非常簡單和可靠, 製造商能夠在一個單獨的硅片上集成數百萬甚至數十億個這樣的電路。
 孤立地講, 單個的位不是非常有用。 然而, 當把位組合在一起, 再加上某種解釋(inter­pretation) , 即賦予不同的可能位模式以含意, 我們就能夠表示任何有限集合的元素。 比如, 使用一個二進制數字系統, 我們能夠用位組來編碼非負數。 通過使用標準的字符碼,我們能夠對文檔中的字母和符號進行編碼。 在本章中, 我們將討論這兩種編碼, 以及負數表示和實數近似值的編碼。
 我們研究三種最重要的數字表示。 無符號(unsigned)編碼基千傳統的二進制表示法,表示大於或者等於零的數字。 補碼(two’s-complement)編碼是表示有符號整數的最常見的方式, 有符號整數就是可以爲正或者爲負的數字。 浮點數(floating-point)編碼是表示實數的科學記數法的以 2爲基數的版本。 計算機用這些不同的表示方法實現算術運算, 例如加法和乘法, 類似於對應的整數和實數運算。
 計算機的表示法是用有限數量的位來對一個數字編碼, 因此, 當結果太大以至不能表 示時, 某些運算就會溢出(overflow)。 溢出會導致某些令人吃驚的後果。 例如, 在今天的大多數計算機上(使用32位來表示數據類型int), 計算表達式200300400*500會得出結果-884 901 888。這違背了整數運算的特性,計算一組正數的乘積不應產生一個負的結果。
 另一方面,整數的計算機滿足人們所熟知的真正整數運算的許多性質。例如,利用乘法的結合律和交換律,計算下面任何一個C表達式,都會得出結果-884 901 888:
(500 * 400) * (300 * 200)
((500 * 400) * 300) * 200
((200 * 500) * 300) * 400
400 * (200 * (300 * 500))
計算機可能沒有產生期望的結果, 但是至少它是一致的!
浮點運算有完全不同的數學屬性。 雖然溢出會產生特殊的值+=, 但是一組正數的乘積總是正的。 由千表示的精度有限, 浮點運算是不可結合的。 例如, 在大多數機器上, c 表達式(3.14+1e20)-le20 求得的值會是o. 0, 而3.14+(le20-le20)求得的值會是3. 14。整數運算和浮點數運算會有不同的數學屬性是因爲它們處理數字表示有限性的方式不同 整數的表示雖然只能編碼一個相對較小的數值範圍, 但是這種表示是精確的;而浮點數雖然可以編碼一個較大的數值範酣, 但是這種表示只是近似的。
通過研究數字的實際表示, 我們能夠了解可以表示的值的範圍和不同算術運算的屬性。 爲了使編寫的程序能在全部數值範圍內正確工作, 而且具有可以跨越不同機器、 操作系統和編譯器組合的可移植性, 瞭解這種屬性是非常重要的。 後面我們會講到, 大量計算 機的安全涌洞都是由千計算機算術運算的微妙細節引發的。 在早期, 當人們碰巧觸發了程 序漏洞, 只會給人們帶來一些不便, 但是現在,有衆多的黑客企圖利用他們能找到的任何漏洞, 不經過授權就進入他人的系統。 這就要求程序員有更多的責任和義務, 去了解他們 的程序如何工作,以及如何被迫產生不良的行爲。
計算機用幾種不同的二進制表示形式來編碼數值。 隨着第3章進入機器級編程,你需 要熟悉這些表示方式。 在本章中,我們描述這些編碼, 並且教你如何推出數字的表示。
通過直接操作數字的位級表示,我們得到了幾種進行算術運算的方式。 理解這些技術對於理解編譯器產生的機器級代碼是很重要的,編譯器會試圖優化算術表達式求值的性能。
我們對這部分內容的處理是基千一組核心的數學原理的。 從編碼的基本定義開始, 然後得出 些屬性, 例如可表示的數字的範圍、 它們的位級表示以及算術運算的屬性。 我們相信從這樣一個抽象的觀點來分析這些內容, 對你來說是很重要的, 因爲程序員需要對計 算機運算與更爲人熟悉的整數和實數運算之間的關係有清晰的理解。

怎樣閱讀本章
本章我們研究在計算機上如何表示數宇和其他形式數據的基本屬性, 以及計算機對 這些數據執行操作的屬性。 這就要求我們深入研究數學語言,編寫公式和方程式, 以及 展示重要屬性的推導。
爲了幫助你閱讀, 這部分內容安排如下: 首先給出以數學形式表示的屬性, 作爲原 理。 然後,用例子和非形式化的討論來解釋這個原理。 我們建議你反覆閱讀原理描述和 它的示例與討論,直到你對該屬性的說明內容及其重要性有了牢固的直覺。 對於更加複雜的屬性, 還會提供推導, 其結構看上去將會像一個數學證明。 雖然最終你應該嘗試理 解這些推導, 但在第一次閱讀時你可以跳過它們。
我們也鼓勵你在閱讀正文的過程中完成練習題, 這會促使你主動學習, 幫助你理論聯 系實際。 有了這些例題和練習題作爲背景知識, 再返回推導,你將發現理解起來會容易許 多。 同時, 請放心, 掌握好高中代數知識的人都具備理解這些內容所需要的數學技能。
C++編程語言建立在C語言基礎之上, 它們使用完全相同的數字表示和運算。 本章 中關於C的所有內容對C++都有效。 另一方面,Java語言創造了一套新的數字表示和運算標準。 C標準的設計允許多種實現方式, 而Java標準在數據的格式和編碼上是非常精確具體的。 本章中多處着重介紹了Java支持的表示和運算。

C編程語言的演變
前面提到過,C編程語言是貝爾實驗室的Dennis Ritchie最早開發出來的, 目的是> 和Unix操作系統一起使用(Unix也是貝爾實驗室開發的)。 在那個時候, 大多數系統程 序, 例如操作系統,爲了訪問不同數據類型的低級表示, 都必須大量地使用匯編代碼。比如說, 像malloc庫函數提供的內存分配功能,用當時的其他高級語言是無法編寫的。 Brian Kernighan和Dennis Ritchie的著作的笫1版[60]記錄了最初貝爾實驗室的C 語言版本。 隨着時間的推移, 經過多個標準化組織的努力,C語言也在不斷地演變。1989年, 美國國家標準學會下的一個工作組推出了ANSIC標準, 對最初的貝爾實驗室的C 語言做了重大修改。 ANSIC與貝爾實驗室的C 有了很大的不同, 尤其是函數聲明的方式。 Br ian Ker nigh an 和DennisR it chie在著作的第2版[61]中描述了ANSIC, 這本書至今仍被公認爲關於C語言最好的參考手冊之一。
國際標準化組織接替了對C語言進行標準化的任務, 在1990年推出了一個幾乎和 ANSI C 一樣的版本, 稱爲 “ISO C90”。 該組織在1999年又對C語言做了更新, 推出 “ISO C99”。 在這一版本中, 引入了一些新的數據類型, 對使用不符合英語語言字符的文本字符串提供了支持。 更新的版本201 1年得到批准, 稱爲 “ISO Cll”, 其中再次添加了更多的數據類型和特性。 最近增加的大多數內容都可以向後兼容, 這意味着根據早期標準(至少可以回溯到ISOC90)編寫的程序按新標準編譯時會有同樣的行爲。
GNU編譯器套裝(GNUCom piler Col lec­tion, GCC )可以基於不同的命令行選項, 依照多個不同版本的C語言規則來編譯程序prog.c,我們就使用命令行:

linux> gcc -std=c11 prog.c

在這裏插入圖片描述

編譯選項-ansi和-std=c89的用法是一樣的——會`根據ANSI或者ISO C90標準來編譯程序。 (C90有時也稱爲 “C89”, 這是因爲它的標準化工作是從1989年 開始的。)編譯選項-std=c99會讓編譯器按照ISOC99 的規則進行編譯。
本書中, 沒有指定任何編譯選項時, 程序會按照基於ISOC90的C語言版本進行編譯, 但是也包括一些C99、 Cll 的特性, 一些C++ 的特性, 還有一些是與GCC相關的特性。 GNU項目正在開發一個結合了ISOCll 和其他一些特性的版本, 可以通過命令行選項-std=gnull來指定。 (目前, 這個實現還未完成。)今後, 這個版本會成爲默認的版本。

2.1 信息存儲

大多數計算機使用8 位的塊, 或者宇節(byte), 作爲最小的可尋址的內存單位, 而不 是訪問內存中單獨的位。 機器級程序將內存視爲一個非常大的字節數組, 稱爲虛擬內存 (virtual memo ry)。 內存的每個字節 都由一個唯一的數字來標識, 稱爲它的地址Cad­dress), 所有可能地址的集合就稱爲虛擬地址空間(virtual address spa ce)。 顧名思義, 這 個虛擬地址空間只是一個展現給機器級程序的概念性映像。 實際的實現(見第9章)是將動態隨機訪問存儲器(DRAM)、閃存、 磁盤存儲器、 特殊硬件和操作系統軟件結合起來, 爲 程序提供一個看上去統一的字節數組。
在接下來的幾章中, 我們將講述編譯器和運行時系統是如何將存儲器空間劃分爲更可 管理的單元, 來存放不同的程序對象( pro gr am object ), 即程序數據、 指令和控制信息。 可以用各種機制來分配和管理程序不同部分的存儲。 這種管理完全是在虛擬地址空間裏完 成的。 例如, C語言中一個指針的值(無論它指向一個整數、 一個結構或是某個其他程序 對象)都是某個存儲塊的第一個字節的虛擬地址。 C編譯器還把每個指針和類型信息聯繫起來, 這樣就可以根據指針值的類型, 生成不同的機器級代碼來訪問存儲在指針所指向位置處的值。 儘管C編譯器維護着這個類型信息, 但是它生成的實際機器級程序並不包含關於數 據類型的信息。 每個程序對象可以簡單地視爲一個字節塊, 而程序本身就是一個字節序列。

C語言中指針的作用
指針是C語言的一個重要特性。它提供了引用數據結構(包括數組)的元素的機制。與變量類似,> 指針也有兩個方面:值和類型。它的值表示某個對象的位置,而它的類型 表示那個位置上所存儲對象的類型(比如整數或者浮點數)。
真正理解指針需要查看它們在機器級上的表示以及實現。這將是第3章的重點之 一,3.10. 1節將對其進行深入介紹。

2. 1. 1 十六進制表示法

一個字節由8位組成。在二進制表示法中,它的值域是00000000 2 ~ 111111112。如果看 成十進制整數,它的值域就是010~25510。兩種符號表示法對於描述位模式來說都不是非常 方便。二進制表示法太冗長,而十進制表示法與位模式的互相轉化很麻煩。替代的方法是, 以16爲基數,或者叫做十六進制(hexadecimal)數,來表示位模式。十六進制(簡寫爲 “hex”)使用數字’O’~ '9’以及字符 ‘A’~ 'F’來表示16個可能的值。圖2-2展示了16個十 六進制數字對應的十進制值和二進制值。用十六進制書寫,一個字節的值域爲0016 ~FF16
在這裏插入圖片描述

在C語言中,以Ox或 ox 開頭的數字常量被認爲是十六進制的值。字符’A’~ ‘F’ 既可以是大寫,也可以是小寫。例如,我們可以將數字FA1D37B1s寫作OxFA1D37B, 或者 Oxfald37b, 甚至是大小寫混合,比如,OxFa1D37b 。在本書中, 我們將使用C表示法來 表示十六進制值。
編寫機器級程序的一個常見任務就是在位模式的十進制、 二進制和十六進制表示之間人工轉換。二進制和十六進制之間的轉換比較簡單直接,因爲可以一次執行一個十六進制 數字的轉換。數字的轉換可以參考如圖2-2所示的表。一個簡單的竅門是,記住十六進制 數字A 、C和F 相應的十進制值。而對千把十六進制值B、D 和E轉換成十進制值,則可以通過計算它們與前三個值的相對關係來完成。
比如,假設給你一個數字 Oxl73A4C。可以通過展開每個十六進制數字,將它轉換爲 二進制格式,如下所示:
十六進制   1	 7	  3		A	4			C二進制		  0001	0111		0011	1010	0100	1100

這樣就得到了二進制表示000101110011101001001100 。
反過來,如果給定一個二進制數字1111001010110110110011, 可以通過首先把它分爲 每4位一組來轉換爲十六進制。不過要注意,如果位總數不是4的倍數,最左邊的一組可以少千4位,前面用0補足。然後將每個4位組轉換爲相應的十六進制數字:
在這裏插入圖片描述

練習題2.1 完成下面的數字轉換 A. 將Ox39A7F8轉換爲二進制。 B. 將二進制 1100100101111011轉換爲十六進制。 C. 將OxDSE4C轉換爲二進制。 D. 將二進制 1001101110011110110101轉換爲十六進制。 當值x是2的非負整數n次幕時, 也就是x= Z", 我們可以很容易地將x寫成十六進制形式, 只要記住x的二進制表示就是1後面跟n個0。十六進制數字 0代表4個二進制 0。 所以, 當n表示成曰一 句的形式, 其中O􀀏i􀀏3, 我們可以把x寫成開頭的十六進制數字爲l(i=O)、 2(i= 1)、 4(i =2)或者 8(i= 3), 後面跟隨着)個十六進制的0。比如, x= 2048 = 211 , 我們有n= ll =3+4•2, 從而得到十六進制表示 Ox800。
練習題2. 2 填寫下表中的空白項, 給出2的不同次幕的二進制和十六進制表示: 在這裏插入圖片描述 十進制和十六進制表示之間的轉換需要使用乘法或者除法來處理一般情況。 將一個十進制數字 x 轉換爲十六進制 , 可以反覆地用16除x, 得到一個商q和一個餘數r, 也就是 x=q• 16+r。 然後, 我們用十六進制數字表示的r作爲最低位數字, 並且通過對q 反覆進行這個過程得到剩下的數字。 例如, 考慮十進制 314 156的轉換: 在這裏插入圖片描述 從這裏, 我們能讀出十六進制表示 爲Ox4CB2C。 反過來, 將一個十六進制數字 轉換爲十進制數字, 我們可以用相應的16的幕乘以每個十六進制數字。 比如, 給定數字 Ox7AF, 我們計算它對應的十進制值爲7•162 +10-16+15 = 7• 256+10• 16+15 = 1792+160+15 =1967。
練習題2. 3 一個字節可以用兩個十六進制數字來表示。 填寫下表中缺失的項, 給出不同字節模式的十進制、 二進制和十六進制值: 在這裏插入圖片描述

十進制和十六進制間的轉換
較大數值的十進制和十六進制之間的轉換, 最好是讓計算機或者計算器來完成。 有大 量的工具可以完成這個工作。一個簡單的方法就是利用任何標準的搜索引擎, 比如查詢:
把Oxabcd轉換爲十進制數

把123用十六進制表示。

練習題2. 4 不將數字轉換爲十進制或者二進制, 試着解答下面的算術題,答案要用 十六進制表示。 提示: 只要將執行十進制加法和減法所使用的方法改成以16爲基數。
A. Ox503c+Ox8=
B. Ox503c-Ox40=
C.Ox503c+64=
D.Ox50ea-Ox503c=

2.1.2 字數據大小

每臺計算機都有一個字長(word size), 指明指針數據的標稱 大小(nominal size)。 因爲 虛擬地址是以這樣的一個字來編碼的, 所以字長決定的最重要的系統參數就是虛擬地址空 間的最大大小。也就是說,對於一個字長爲w位的機器而言,虛擬地址的範圍爲O~2w- 1, 程序最多訪問2w個字節。
最近這些年,出現了 大規模的從32位字長機器到64位字長機器的遷移。這種情況首先出 現在爲大型科學和數據庫應用設計的高端機器上,之後是臺式機和筆記本電腦,最近則出現在 智能手機的處理器上。 32位字長限制虛擬地址空間爲4千兆字節(寫作4GB), 也就是說,剛剛超過4*9滬字節。擴展到64位字長使得虛擬地址空間爲16EB, 大約是 1.84* 219
大多數64位機器也可以運行爲32位機器編譯的程序, 這是一種向後兼容。 因此, 舉例來說, 當程序prog.c用如下僞指令編譯後linux> gee -m32 prog.e
該程序就可以在 32位或64位機器上正確運 行。 另一方面,若程序用下述僞指令編譯linux> gee -m64 prog.e 那就只能在64位機器上運行。 因此, 我們將程序稱爲" 32位程序 ” 或"64位程序” 時, 區別在於該程序是如何編譯的,而不是其運行的機器類型。計算機和編譯器支持 多種不同方式編 碼的數字格式,如不同長度的整數和浮點 數。 比如,許多機器都有處理單個字節的 指令,也有處理表示爲2字節、 4字節或 者8字節整數的指令, 還有些指令支持表 示爲4字節和8字節的浮點數。C語言支持整數和浮點數的 多種數據格式。 圖2-3展示了爲C語言各種數據類
在這裏插入圖片描述
型分配的字節數。(我們在2.2節討論C標準保證的字節數和典型的字節數之間的關係。)有些數據類型的確切字節數依賴於程序是如何被編譯的。我們給出的是 32 位和 64 位程序 的典型值。整數或者爲有符號的,即可以表示負數、零和正數;或者爲無符號的,即只能表示非負數。C的數據類型char表示一個單獨的字節。儘管“char”是由於它被用來存儲文本串中的單個字符這一事實而得名,但它也能被用來儲存整數值。數據類型short、int 和 long 可以提供各種數據大小。即使是爲 64 位系統編譯,數據類型 int 通常也只有 4個字節。數據類型long一般在 32 位程序中爲 4字節,在 64 位程序中則爲 8字節。
爲了避免由於依賴典型爲了避免由於依賴 大小和不同編譯器設置帶來的奇怪行爲,ISO C99 引入了 一類數據類型,其數據大小是固定的,不隨編譯器和機器設置而變化。其中就有數據類型 int32 t 和 int64 七,它們分別爲 4 個字節和 8 個字節。使用確定大小的整數類型是程序 員準確控制數據表示的最佳途徑。
大部分數據類型都編碼爲有符號數值,除非有前綴關鍵字 unsigned 或對確定大小的數據類型使用了特定的無符號聲明。數據類型 char 是一個例外。儘管大多數編譯器和機器將它們視爲有符號數,但C標準不保證這一點。相反,正如方括號指示的那樣,程序員應該用有符號字符的聲明來保證其爲一個字節的有符號數值。不過,在很多情況下,程序 行爲對數據類型 char 是有符號的還是無符號的並不敏感。
對關鍵字的順序以及包括還是省略可選關鍵字來說,C語言允許存在多種形式。比如,下面所有的聲明都是一個意思:
在這裏插入圖片描述
我們將始終使用圖 2-3 給出的格式。
圖 2-3 還展示了指針(例如一個被聲明爲類型爲 "char * "的變量)使用程序的全字長。大多數機器還支持兩種不同的浮點數格式:單精度(在 C中聲明爲 float) 和雙精度(在 C中聲明爲 double)。這些格式分別使用 4 字節和 8字節。

聲明指針 對於任何數據類型T,聲明 T *p; 表明p是一個指針變量,指向一個類型爲T的對象。例如,char *p;
就將一個指針聲明爲指向一個char類型的對象。

程序員應該力圖使他們的程序在不同的機器和編譯器上可移植。可移植性的一個方面就是使程序對不同數據類型的確切大小不敏感。C語言標準對不同數據類型的數字範圍設置了下界(這點在後面還將講到),但是卻沒有上界。因爲從1980 年左右到2010 年左右,32 位機器和 32 位程序是主流的組合,許多程序的編寫都假設爲圖 2-3 中 32 位程序的字節分配。隨着 64 位機器的日益普及,在將這些程序移植到新機器上時,許多隱藏的對字長的依賴性就會顯現出來,成爲錯誤。比如,許多程序員假設一個聲明爲 int類型的程序對象能被用來存儲一個指針。這在大多數 32 位的機器上能正常工作,但是在一臺 64 位的機器上卻會導致問題。

2.1.3 尋址和字節順序

於跨越多字節的程序對象, 我們必須建立兩個規則: 這個對象的地址是什麼, 以及在內存中如何排列這些字節。 在幾乎所的機器上, 多字節對象都被存儲爲連續的字節序 列,對象的地址爲所使用字節中最小的地址。 例如, 假設一個類型爲int的變量x的地址 爲 OxlOO, 也就是說, 地址表達式 &x 的值爲 OxlOO。 那麼, (假設數據類型 1讓爲 32 位表 示)x 的 4 個字節將被存儲在內存的 OxlOO、 OxlOl、 Oxl02和Oxl03 位置。
排列表示一個對象的字節有兩個通用的規則。 考慮一個w位的整數, 其位表示爲[Xw-1,Xw-2,…, X1, X0], 其中 Xw 1是最高有效位, 而x。是最低有效位。 假設w是8的倍數, 這些位就能被分組成爲字節, 其中最高有效字節包含位[x心氣, Xw-2• …,立-sJ, 而最低有效 字節包含位[x1’ X5’ …, x。 J’ 其他字節包含中間的位。 某些機器選擇在內存中按照從最低 有效字節到最高有效字節的順序存儲對象,而另一些機器則按照從最高有效字節到最低有效 字節的順序存儲。 前一種規則——最低有效字節在最前面的方式, 稱爲小端法(little endian)。後一種規則—-—最高有效字節在最前面的方式, 稱爲大端法(big endian)。
假設變量 x 的類型爲m七, 位於地址 Ox100 處, 它的十六進制值爲 Ox01234567。地 址範圍 OxlOO~ Ox103 的字節順序依賴千機器的類型:
在這裏插入圖片描述
注意, 在字 Ox01234567 中, 高位字節的十六進制值爲 OxOl, 而低位字節值爲 Ox67。
大多數Intel兼容機都只用小端模式。 另 一方面, IBM和Oracle(從其 2010 年收購Sun Microsystems開始)的大多數機器則是按大端模式操作。 注意我們說的是 “大多數”。 這些規則並沒有嚴格按照企業界限來劃分。 比如, IBM和Oracle製造的個人計算機使用 的是Intel兼容的處理器, 因此使用小端法。 許多比較新的微處理器是雙端法(bi-endian), 也就是說可以把它們配置成作爲大端或者小端的機器運行。 然而, 實際情況是: 一旦選擇了特定操作系統,那麼字節順序也就固定下來。 比如, 用於許多移動電話的ARM 微處理 器, 其硬件可以按小端或大端兩種模式操作, 但是這些芯片上最常見的兩種操作系統一—
Android(來自Google)和IOSC來自Apple)-卻只能運行於小端模式。
令人吃驚的是, 在哪種字節順序是合適的這個問題上, 人們表現得非常情緒化。 實際上, 術語 "little endian(小端)” 和 "big endian(大端)” 出自Jonathan Swift 的《格利佛遊記》(Gulliver’s Travels)一書, 其中交戰的兩個派別無法就應該從哪一端(小端還是大端)打開一個半熟的雞蛋達成一致。 就像雞蛋的問題一樣, 選擇何種字節順序沒有技術上的理 由, 因此爭論淪爲關於社會政治論題的爭論。 只要選擇了一種規則並且始終如一地堅持,對千哪種字節排序的選擇都是任意的。

“端的起源” 以下是Jonathan Swift在 1726 年關於大小端之爭歷史的描述: "……我下面要告訴你的是, Lilliput 和
Blefuscu 這兩大強國在過去 36 個月裏一直 在苦戰。 戰爭開始是由於以下的原因:我們大家都認爲, 吃雞蛋前,
原始的方法是打破雞蛋較大的一端,可是當今皇帝的祖父小時候吃雞蛋, 一次按古法打雞蛋時碰巧將一個 手指弄破了, 因此他的父親, 當時的皇帝,
就下了一道敕令, 命令全體臣民吃雞蛋時打破雞蛋較小的一端, 違令者重罰。 老百姓們對這項命令極爲反感。 歷史告訴我們,
由此曾發生過六次叛亂, 其中一個皇帝送了命, 另一個丟了王位。 這些叛亂大多都是由 Ble­fuscu 的國王大臣們煽動起來的。
叛亂平息後, 流亡的人總是逃到那個帝國去尋救避難。 據估計, 先後幾次有 11 000 人情願受死也不肯去打破雞蛋較小的一端。
關於這一爭端,曾出版過幾百本大部著作, 不過大端派的書一直是受禁的,
法律也規定該派的任何人不得做官。”(此段譯文摘自網上蔣劍鋒譯的《格利佛遊記》第一卷第4章。) 在他那個時代, Swift
是在諷刺英國(Lilliput) 和法國 (Blefuscu) 之間持續的衝突。 Danny Cohen, 一位網絡協議的早期開創者,
第一次使用這兩個術語來指代字節順序 [24], 後來這個術語被廣泛接納了。

對於大多數應用程序員來說, 其機器所使用的字節順序是完全不可見的。 無論爲哪種類型的機器所編譯的程序都會得到同樣的結果。 不過有時候, 字節順序會成爲問題。 首先 是在不同類型的機器之間通過網絡傳送二進制數據時, 一個常見的問題是當小端法機器產 生的數據被髮送到大端法機器或者反過來時, 接收程序會發現, 字裏的字節成了反序的。爲了避免這類問題, 網絡應用程序的代碼編寫必須遵守已建立的關千字節順序的規則, 以 確保發送方機器將它的內部表示轉換成網絡標準, 而接收方機器則將網絡標準轉換爲它的 內部表示。 我們將在第11章中看到這種轉換的例子。
第二種情況是, 當閱讀表示整數數據的字節序列時字節順序也很重要。 這通常發生在 檢查機器級程序時。 作爲一個示例, 從某個文件中摘出了下面這行代碼, 該文件給出了一 個針對 Intel x86-64 處理器的機器級代碼的文本表示:
4004d3: 01 05 43 Ob 20 00 add %eax,Ox200b43(%rip)
這一行是由反彙編器(disassembler) 生成的, 反彙編器是一種確定可執行程序文件所表示 的指令序列的工具。 我們將在第3章中學習有關這些工具的更多知識, 以及怎樣解釋像這 樣的行。 而現在, 我們只是注意這行表述的意思是:十六進制字節串 01 05 43 Ob 20 00 是 一條指令的字節級表示, 這條指令是把一個字長的數據加到一個值上, 該值的存儲地址由 Ox200b43 加上當前程序計數器的值得到, 當前程序計數器的值即爲下一條將要執行指令 的地址。 如果取出這個序列的最後 4 個字節:43 Ob 20 00, 並且按照相反的順序寫出, 我 們得到 00 20 Ob 43。 去掉開頭的o, 得到值 Ox200b43, 這就是右邊的數值。 當閱讀像此 類小端法機器生成的機器級程序表示時, 經常會將字節按照相反的順序顯示。 書寫字節序 列的自然方式是最低位字節在左邊, 而最高位字節在右邊, 這正好和通常書寫數字時最高 有效位在左邊, 最低有效位在右邊的方式相反。
字節順序變得重要的第三種情況是當編寫規避正常的類型系統的程序時。 在C語言 中, 可以通過使用強制類型轉換(cast) 或聯合(union)來允許以一種數據類型引用一個對 象, 而這種數據類型與創建這個對象時定義的數據類型不同。 大多數應用編程都強烈不推薦這種編碼技巧, 但是它們對系統級編程來說是非常有用, 甚至是必需的。
圖2-4展示了一段 C 代碼, 它使用強制類型轉換來訪問和打印不同程序對象的字節表示。 我們用 typedef 將數據類型 byte_pointer 定義爲一個指向類型爲“unsigned char“的對象的指針。 這樣 個字節指針引用 個字節序列, 其中每個字節都被認爲是一個非負整數。 第 個例程show_bytes的輸入是 個字節序列的地址, 它用 個字節指針以及一個字節數來指示。 該字節數指定爲數據類型size_t, 表示數據結構大小的首選數據類型。 show—by七es打印出每個以十六進制表示的字節。 C格式化指令 “%.2x” 表明整數必須用至少兩個數字的十六進制格式輸出。
在這裏插入圖片描述
過程show_int、 show_float和show_poin七er展示瞭如何使用程序show_by七es來分別輸出類型爲 int、 flo扛和void* 的C程序對象的字節表示。 可以觀察到它們僅僅傳遞給show_bytes一個指向它們參數x的指針&x, 且這個指針被強制類型轉換爲"un­signed ch ar * "。 這種強制類型轉換告訴編譯器, 程序應該把這個指針看成指向一個字節序列,而不是指向一個原始數據類型的對象。然後,這個指針會被看成是對象使用的最低字節地址。
這些過程使用C語言的運算符sizeof來確定對象使用的字節數。一般來說,表達式sizeof (T)返回存儲一個類型爲T的對象所需要的字節數。 使用sizeof而不是一個固定的值, 是向編寫在不同機器類型上可移植的代碼邁進了 步。
在幾種不同的機器上運行如圖2-5所示的代碼, 得到如圖2-6所示的結果。 我們使用了以下幾種機器:
Linux 32: 運行Linux的IntelIA32處理器。
Windows: 運行Windows的IntelIA32處理器。
Sun: 運行Solaris的SunMicrosystems SP ARC處理器。(這些機器現在由Oracle生產。) Linux 64: 運行Linux的Intelx86-64處理器。
在這裏插入圖片描述
參數12345的十六進制表示爲Ox00003039。對千1讓類型的數據,除了字節順序以 外,我們在所有機器上都得到相同的結果。特別地,我們可以看到在Linux32、Windows 和Linux64上,最低有效字節值Ox39最先輸出,這說明它們是小端法機器;而在Sun上最後輸出,這說明Sun是大端法機器。同樣地,float數據的字節,除了字節順序以外, 也都是相同的。另一方面,指針值卻是完全不同的。不同的機器/操作系統配置使用不同 的存儲分配規則。一個值得注意的特性是Linux32、Windows和Sun的機器使用4字節 地址,而Linux64使用8字節地址。

使用 typedef 來命名數據類型
C 語言中的typedef聲明提供了一種給數據類型命名的方式。這能夠極大地改善代碼的可讀性,因爲深度嵌套的類型聲明很難讀懂。
typedef的語法與聲明變量的語法十分相像,除了它使用的是類型名,而不是變量 名。因此,圖 2-4 中 byte_pointer 的聲明和將一個變量聲明爲類型 "unsigned char * "有相同的形式。
例如,聲明:
typedef int •int_pointer;
int_pointer ip;
將類型 “int主ointer” 定義爲一個指向 int的指針,並且聲明瞭一個這種類型的 變量 ip。我們還可以將這個變量直接聲明爲:int ip;
使用printf格式化輸出
printf函數(還有它的同類fprintf和s printf)提供了一種打印信息的方式,這 種方式對格式化細節有相當大的控制能力。笫一個參數是格式串 (format string), 而其餘的參數都是要打印的值。在格式串裏,每個以"%"開始的字符序列都表示如何格式 化下一個參數。典型的示例包括:’%d’是輸出一個十進制整數,’%f’是扴出一個浮點 數,而 ` %已是扴出一個宇符,其編碼由參數給出。
指定確定大小數據類型的格式,如int 32_t, 要更復雜一些,相關內容參見2.2. 3 節的旁註。
可以觀察到,儘管浮點型和整型數據都是對數值12345編碼,但是它們有截然不同的 字節模式:整型爲Ox00003039, 而浮點數爲Ox4640E400。一般而言,這兩種格式使用不 同的編碼方法。如果我們將這些十六進制模式擴展爲二進制形式,並且適當地將它們移位,就會發現一個有13個相匹配的位的序列,用一串星號標識出來:
在這裏插入圖片描述
這並不是巧合。當我們研究浮點數格式時,還將再回到這個例子。
指針和數組
在函數showbytes( 圖2-4)中,我們看到指針和數組之間緊密的聯繫,這將在3.8節中詳細描述。這個函數有一個類型爲byte _pointer(被定義爲一個指向unsignedchar 的指針 )的參數start,但是我們在第8行上看到數組引用start[i)。在 C 語言中,我們能夠用數組表示法來引用指針,同時我們也能用指針表示法來引用數組元素。在這個例子 中·, 引用start[i)表示我們想要讀取以start 指向的位置爲起始的第i個位置處的字節。
指針的創建嚴間接引用
在圖2-4的第13、17和21行,我們看到對 C和 C++中兩種獨有操作的使用。C的 ”取地址” 運算符&創建一個指針。在這三行中,表達式&x創建了一個指向保存變量x的 位置的指針。這個指針的類型取決於x的類型,因此這三個指針的類型分別爲int入 float 和void
。(數據類型void*是一種特殊類型的指針,沒有相關聯的類型信息。)
強制類型轉換運算符可以將一種數據類型轉換爲另一種。因此,強制類型轉換(byte_pointer )&x表明無論指針&x以前是什麼類型,它現在就是一個指向數據類型爲unsignedchar 的指針。這裏給出的這些強制類型轉換不會改變真實的指針,它們 只是告訴編譯器以新的數據類型來看待被指向的數據。
生成一 張ASCII表
可以通過執行命令man ascii來得到一張 ASCII 宇符碼的表。

練習題2. 5 思考下面對show_bytes的三次調用:
int val= Ox87654321;
byte_pointer valp = (byte_pointer) &val;
show_bytes(valp, 1); I* A. I
show_bytes(valp, 2); I
B. I
show_bytes(valp, 3); I
C. *I
指出在小端法機器和大端法機器上, 每次調用的輸出值 。
A. 小端法: 大端法:B. 小端法: 大端法:C. 小端法: 大端法:

練習題2.6 使用show_int和show_float, 我們確定整數3510593的十六進制表示爲Ox00359141, 而浮點數 3510593. 0的十六進制表示爲Ox4A564504。
A. 寫出這兩個十六進制值的二進制表示。
B. 移動這兩個二進制串的相對位置, 使得它們相匹配的位數最多。 有多少位相匹配呢?
C. 串中的什麼部分不相匹配?

2.1.4 表示字符串
C 語言中字符串被編碼爲一個以null(其值爲0)字符結尾的字符數組。 每個字符都由某個標準編碼來表示, 最常見的是ASCII 字符碼。 因此, 如果我們以參數 “12345” 和6 (包括終止符)來運行例程show_bytes, 我們得到結果 31 32 33 34 35 00。 請注意, 十進 制數字x 的ASCII 碼正好是Ox3x, 而終止字節的十六進制表示爲OxOO。 在使用 ASCII 碼作爲字符碼的任何系統上都將得到相同的結果, 與字節順序和字大小規則無關。 因而, 文本數據比二進制數據具有更強的平臺獨立性。
練習題2. 7 下面對show—bytes的調用將輸出什麼結果?
const char *S = “abcdef”;
show_bytes((byte_pointer) s, strlen(s));
注意字母 ‘a’ ~‘z’ 的 ASCII 碼爲Ox6l~Ox7A。

文字編碼的Unicode標準
ASCII宇符集適合於編碼英語文檔, 但是在表達一些特殊宇符方面並沒有太多辦法,例如法語的“C”,它完全不適合編碼希臘語、 俄語和中文等語言的文檔。 這些年, 提出了很多方法來對不同語言的文字進行編碼Unicode聯合會(UnicodeConsortium)修訂了最全 。面且廣泛接受的文字編碼標準。 當前的Unicode標準(7.0版)的字庫包括將近100 000 個字 符, 支持廣泛的語言種類, 包括古埃及和巴比倫的語言。 爲了保持信用, Unicode技術委員會否決了爲Klingon(即電視連續劇《星際迷航》中的虛構文明)編寫語言標準的提議。
基本編碼, 稱爲Unicode的 “統一字符集“,使用32位來表示宇符。 這好像要求文 本串中每個字符要佔用4個宇節 不過, 可以有一些替代編碼, 常見的宇符只需要1個或2個字節, 而不太常用的字符需要多一些的字節數。 特別地, UTF-8 表示將每個字符 編碼爲一個字節序列, 這樣標準 ASCII 字符還是使用和它們在 ASCII 中一樣的單宇節 編碼, 這也就意味着所有的 ASCII 字節序列用 ASCII 碼錶示和用UTF-8 表示是一樣的。Java編程語言使用Unicode來表示字符串。 對於C語言也有支持Unicode的程序庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章