深入理解計算機(jī)系統(tǒng):第二章 信息的表示和處理 part1

第一部分

我們對計算機(jī)系統(tǒng)的探索是從學(xué)習(xí)計算機(jī)本身開始的,它由處理器和存儲器子系統(tǒng)組成五续。在核心部分,我們需要方法來表示基本數(shù)據(jù)類型龄恋,比如整數(shù)和實數(shù)運(yùn)算的近似值疙驾。然后,我們考慮機(jī)器級指令如何操作這樣的數(shù)據(jù)郭毕,以及編譯器又如何將c程序翻譯成這樣的指令荆萤。接下來,研究幾種實現(xiàn)處理器的方法铣卡,幫助我們更好地了解硬件資源如何被用來執(zhí)行指令。一旦理解了編譯器和機(jī)器級代碼偏竟,我們就能了解如何通過編寫C程序以及編譯它們來最大化程序的性能煮落。本部分以存儲器子系統(tǒng)的設(shè)計作為結(jié)束,這是現(xiàn)代計算機(jī)系統(tǒng)最復(fù)雜的部分之一踊谋。

本書的這一部分將領(lǐng)著你深入了解如何表示和執(zhí)行應(yīng)用程序蝉仇。你將學(xué)會一些技巧,來幫助你寫出安全殖蚕、可靠且充分利用計算資源的程序轿衔。


第2章 信息的表示和處理

現(xiàn)代計算機(jī)存儲和處理的信息以二值信號表示。這些微不足道的二進(jìn)制數(shù)字睦疫,或者稱為位 (bit)害驹,形成了數(shù)字革命的基礎(chǔ)。大家熟悉并使用了1000多年的十進(jìn)制 (以10為基數(shù)) 起源于印度蛤育,在12世紀(jì)被阿拉伯?dāng)?shù)學(xué)家改進(jìn)宛官,并在13世紀(jì)被意大利數(shù)學(xué)家 Leonardo Pisano (大約公元1170—1250,更為大家所熟知的名字是Fibonacci) 帶到西方瓦糕。對于有10個手指的人類來說底洗,使用十進(jìn)制表示法是很自然的事情,但是當(dāng)構(gòu)造存儲和處理信息的機(jī)器時咕娄,二進(jìn)制值工作得更好亥揖。二值信號能夠很容易地被表示、存儲和傳輸圣勒,例如费变,可以表示為穿孔卡片上有洞或無洞摧扇、導(dǎo)線上的高電壓或低電壓,或者順時針或逆時針的磁場胡控。對二值信號進(jìn)行存儲和執(zhí)行計算的電子電路非常簡單和可靠扳剿,制造商能夠在一個單獨的硅片上集成數(shù)百萬甚至數(shù)十億個這樣的電路。

孤立地講昼激,單個的位不是非常有用庇绽。然而,當(dāng)把位組合在一起橙困,再加上某種解釋 (interpretation)瞧掺,即賦予不同的可能位模式以含意,我們就能夠表示任何有限集合的元素凡傅。比如辟狈,使用一個二進(jìn)制數(shù)字系統(tǒng),我們能夠用位組來編碼非負(fù)數(shù)夏跷。通過使用標(biāo)準(zhǔn)的字符碼哼转,我們能夠?qū)ξ臋n中的字母和符號進(jìn)行編碼。在本章中槽华,我們將討論這兩種編碼壹蔓,以及負(fù)數(shù)表示和實數(shù)近似值的編碼。

我們研究三種最重要的數(shù)字表示猫态。無符號 (unsigned) 編碼基于傳統(tǒng)的二進(jìn)制表示法佣蓉,表示大于或者等于零的數(shù)字。補(bǔ)碼 (two's-complement) 編碼是表示有符號整數(shù)的最常見的方式亲雪,有符號整數(shù)就是可以為正或者為負(fù)的數(shù)字勇凭。浮點數(shù) (floating-point) 編碼是表示實數(shù)的科學(xué)記數(shù)法的以 2 為基數(shù)的版本。計算機(jī)用這些不同的表示方法實現(xiàn)算術(shù)運(yùn)算义辕,例如加法和乘法虾标,類似于對應(yīng)的整數(shù)和實數(shù)運(yùn)算。

計算機(jī)的表示法是用有限數(shù)量的位來對一個數(shù)字編碼灌砖,因此夺巩,當(dāng)結(jié)果太大以至不能表示時,某些運(yùn)算就會溢出 (overflow)周崭。溢出會導(dǎo)致某些令人吃驚的后果柳譬。例如,在今天的大多數(shù)計算機(jī)上 (使用 32 位來表示數(shù)據(jù)類型 int)续镇,計算表達(dá)式 200300400*500 會得出結(jié)果 -884901888美澳。這違背了整數(shù)運(yùn)算的特性,計算一組正數(shù)的乘積不應(yīng)產(chǎn)生一個負(fù)的結(jié)果。

另一方面制跟,整數(shù)的計算機(jī)運(yùn)算滿足人們所熟知的真正整數(shù)運(yùn)算的許多性質(zhì)舅桩。例如,利
用乘法的結(jié)合律和交換律雨膨,計算下面任何一個C表達(dá)式擂涛,都會得出結(jié)果一884901888:

(500 * 400) * (300 * 200)

((500 * 400) * 300) * 200

((200 * 500) * 300) * 400

400 * (200 * (300 * 500))

計算機(jī)可能沒有產(chǎn)生期望的結(jié)果,但是至少它是一致的聊记!

浮點運(yùn)算有完全不同的數(shù)學(xué)屬性撒妈。雖然溢出會產(chǎn)生特殊的值+∞,但是一組正數(shù)的乘積總是正的排监。由于表示的精度有限狰右,浮點運(yùn)算是不可結(jié)合的。例如舆床,在大多數(shù)機(jī)器上棋蚌,C表達(dá)式 (3.14+le20) - le20 求得的值會是 0.0,而 3.14 + (le20-le20) 求得的值會是3.14挨队。整數(shù)運(yùn)算和浮點數(shù)運(yùn)算會有不同的數(shù)學(xué)屬性是因為它們處理數(shù)字表示有限性的方式不同——整數(shù)的表示雖然只能編碼一個相對較小的數(shù)值范圍谷暮,但是這種表示是精確的;而浮點數(shù)雖然可以編碼一個較大的數(shù)值范圍盛垦,但是這種表示只是近似的湿弦。

通過研究數(shù)字的實際表示,我們能夠了解可以表示的值的范圍和不同算術(shù)運(yùn)算的屬性情臭。為了使編寫的程序能在全部數(shù)值范圍內(nèi)正確工作,而且具有可以跨越不同機(jī)器赌蔑、操作系統(tǒng)和編譯器組合的可移植性,了解這種屬性是非常重要的。后面我們會講到牙丽,大量計算機(jī)的安全漏洞都是由于計算機(jī)算術(shù)運(yùn)算的微妙細(xì)節(jié)引發(fā)的膳算。在早期,當(dāng)人們碰巧觸發(fā)了程序漏洞趾浅,只會給人們帶來一些不便愕提,但是現(xiàn)在,有眾多的黑客企圖利用他們能找到的任何漏洞皿哨,不經(jīng)過授權(quán)就進(jìn)入他人的系統(tǒng)浅侨。這就要求程序員有更多的責(zé)任和義務(wù),去了解他們的程序如何工作证膨,以及如何被迫產(chǎn)生不良的行為如输。

計算機(jī)用幾種不同的二進(jìn)制表示形式來編碼數(shù)值。隨著第3章進(jìn)人機(jī)器級編程,你需要熟悉這些表示方式不见。在本章中澳化,我們描述這些編碼,并且教你如何推出數(shù)字的表示稳吮。通過直接操作數(shù)字的位級表示缎谷,我們得到了幾種進(jìn)行算術(shù)運(yùn)算的方式。理解這些技術(shù)對于理解編譯器產(chǎn)生的機(jī)器級代碼是很重要的灶似,編譯器會試圖優(yōu)化算術(shù)表達(dá)式求值的性能列林。

我們對這部分內(nèi)容的處理是基于一組核心的數(shù)學(xué)原理的。從編碼的基本定義開始喻奥,然后得出一些屬性席纽,例如可表示的數(shù)字的范圍、它們的位級表示以及算術(shù)運(yùn)算的屬性撞蚕。我們相信從這樣一個抽象的觀點來分析這些內(nèi)容润梯,對你來說是很重要的,因為程序員需要對計算機(jī)運(yùn)算與更為人熟悉的整數(shù)和實數(shù)運(yùn)算之間的關(guān)系有清晰的理解甥厦。

旁注 怎樣閱讀本章

本章我們研究在計算機(jī)上如何表示數(shù)字和其他形式數(shù)據(jù)的基本屬性纺铭,以及計算機(jī)對這些數(shù)據(jù)執(zhí)行操作的屬性。這就要求我們深入研究數(shù)學(xué)語言刀疙,編寫公式和方程式舶赔,以及展示重要屬性的推導(dǎo)。

為了幫助你閱讀谦秧,這部分內(nèi)容安排如下:首先給出以數(shù)學(xué)形式表示的屬性竟纳,作為原理。然后疚鲤,用例子和非形式化的討論來解釋這個原理锥累。我們建議你反復(fù)閱讀原理描述和它的示例與討論,直到你對該屬性的說明內(nèi)容及其重要性有了牢固的直覺集歇。對于更加復(fù)雜的屬性桶略,還會提供推導(dǎo),其結(jié)構(gòu)看上去將會像一個數(shù)學(xué)證明诲宇。雖然最終你應(yīng)該嘗試?yán)斫膺@些推導(dǎo)际歼,但在第一次閱讀時你可以跳過它們。

我們也鼓勵你在閱讀正文的過程中完成練習(xí)題姑蓝,這會促使你主動學(xué)習(xí)鹅心,幫助你理論聯(lián)系實際。有了這些例題和練習(xí)題作為背景知識纺荧,再返回推導(dǎo)巴帮,你將發(fā)現(xiàn)理解起來會容易許多溯泣。同時,請放心榕茧,掌握好高中代數(shù)知識的人都具備理解這些內(nèi)容所需要的數(shù)學(xué)技能垃沦。

C++ 編程語言建立在 C 語言基礎(chǔ)之上,它們使用完全相同的數(shù)字表示和運(yùn)算用押。本章中關(guān)于 C 的所有內(nèi)容對 C++ 都有效肢簿。另一方面,Java 語言創(chuàng)造了一套新的數(shù)字表示和運(yùn)算標(biāo)準(zhǔn)蜻拨。C 標(biāo)準(zhǔn)的設(shè)計允許多種實現(xiàn)方式池充,而Java標(biāo)準(zhǔn)在數(shù)據(jù)的格式和編碼上是非常精確具體的。本章中多處著重介紹了Java支持的表示和運(yùn)算缎讼。

旁注 C編程語言的演變

前面提到過收夸,C 編程語言是貝爾實驗室的 Dennis Ritchie 最早開發(fā)出來的,目的是和 Unix 操作系統(tǒng)一起使用(Unix也是貝爾實驗室開發(fā)的)血崭。在那個時候卧惜,大多數(shù)系統(tǒng)程序,例如操作系統(tǒng)夹纫,為了訪問不同數(shù)據(jù)類型的低級表示咽瓷,都必須大量地使用匯編代碼。比如說舰讹,像malloc庫函數(shù)提供的內(nèi)存分配功能茅姜,用當(dāng)時的其他高級語言是無法編寫的。

Brian Kernighan和 Dennis Ritchie 的著作的第1版記錄了最初貝爾實驗室的 C 語言版本月匣。隨著時間的推移钻洒,經(jīng)過多個標(biāo)準(zhǔn)化組織的努力,C 語言也在不斷地演變锄开。1989 年素标,美國國家標(biāo)準(zhǔn)學(xué)會下的一個工作組推出了 ANSIC 標(biāo)準(zhǔn),對最初的貝爾實驗室的 C 語言做了重大修改院刁。ANSIC 與貝爾實驗室的 C 有了很大的不同糯钙,尤其是函數(shù)聲明的方式粪狼。Brian Kernighan 和 Dennis Ritchie 在著作的第 2 版中描述了 ANSIC,這本書至今仍被公認(rèn)為關(guān)于 C 語言最好的參考手冊之一退腥。

國際標(biāo)準(zhǔn)化組織接替了對 C 語言進(jìn)行標(biāo)準(zhǔn)化的任務(wù),在 1990 年推出了一個幾乎和ANSIC 一樣的版本再榄,稱為 “ISO C90”狡刘。該組織在 1999 年又對 C 語言做了更新,推出 “ISO C99”困鸥。在這一版本中嗅蔬,引入了一些新的數(shù)據(jù)類型剑按,對使用不符合英語語言字符的文本字符串提供了支持。更新的版本2011年得到批準(zhǔn)澜术,稱為 “ISO C11”艺蝴,其中再次添加了更多的數(shù)據(jù)類型和特性。最近增加的大多數(shù)內(nèi)容都可以向后兼容鸟废,這意味著根據(jù)早期標(biāo)準(zhǔn)(至少可以回溯到ISO C90)編寫的程序按新標(biāo)準(zhǔn)編譯時會有同樣的行為猜敢。

GNU 編譯器套裝(GNU Compiler Collection,GCC) 可以基于不同的命令行選項盒延,依照多個不同版本的C語言規(guī)則來編譯程序缩擂,如圖2-1所示。比如添寺,根據(jù) ISO Cl1來編譯程序prog.c胯盯,我們就使用命令行

linux> gcc-std=cllprog.c

圖2-1 向GCC指定不同的C語言版本

編譯選項 -ansi 和 -std=c89 的用法是一樣的 —— 會根據(jù)ANSI或者 ISO C90 標(biāo)準(zhǔn)來編譯程序。(C90有時也稱為 “C89”计露,這是因為它的標(biāo)準(zhǔn)化工作是從1989年開始的博脑。)編譯選項 -std=c99 會讓編譯器按照 ISO C99 的規(guī)則進(jìn)行編譯。

本書中薄坏,沒有指定任何編譯選項時趋厉,程序會按照基于 ISO C90 的 C 語言版本進(jìn)行編譯,但是也包括一些 C99胶坠、C11 的特性君账,一些 C++ 的特性,還有一些是與 GCC 相關(guān)的特性沈善。GNU 項目正在開發(fā)一個結(jié)合了 ISO C11 和其他一些特性的版本乡数,可以通過命令行選項 -std=gnul1 來指定。(目前闻牡,這個實現(xiàn)還未完成净赴。)今后,這個版本會成為默認(rèn)的版本罩润。

2.1 信息存儲

大多數(shù)計算機(jī)使用8位的塊玖翅,或者字節(jié) (byte),作為最小的可尋址的內(nèi)存單位割以,而不是訪問內(nèi)存中單獨的位金度。機(jī)器級程序?qū)?nèi)存視為一個非常大的字節(jié)數(shù)組,稱為虛擬內(nèi)存 (virtual memory)严沥。內(nèi)存的每個字節(jié)都由一個唯一的數(shù)字來標(biāo)識猜极,稱為它的地址 (address),所有可能地址的集合就稱為虛擬地址空間(virtual address space)消玄。顧名思義跟伏,這個虛擬地址空間只是一個展現(xiàn)給機(jī)器級程序的概念性映像丢胚。實際的實現(xiàn)(見第9章)是將動態(tài)隨機(jī)訪問存儲器(DRAM)、閃存受扳、磁盤存儲器携龟、特殊硬件和操作系統(tǒng)軟件結(jié)合起來,為程序提供一個看上去統(tǒng)一的字節(jié)數(shù)組勘高。

在接下來的幾章中骨宠,我們將講述編譯器和運(yùn)行時系統(tǒng)是如何將存儲器空間劃分為更可管理的單元,來存放不同的程序?qū)ο?(program object)相满,即程序數(shù)據(jù)层亿、指令和控制信息×⒚溃可以用各種機(jī)制來分配和管理程序不同部分的存儲匿又。這種管理完全是在虛擬地址空間里完成的。例如建蹄,C 語言中一個指針的值(無論它指向一個整數(shù)碌更、一個結(jié)構(gòu)或是某個其他程序?qū)ο?都是某個存儲塊的第一個字節(jié)的虛擬地址。C 編譯器還把每個指針和類型信息聯(lián)系起來洞慎,這樣就可以根據(jù)指針值的類型痛单,生成不同的機(jī)器級代碼來訪問存儲在指針?biāo)赶蛭恢锰幍闹怠1M管 C 編譯器維護(hù)著這個類型信息劲腿,但是它生成的實際機(jī)器級程序并不包含關(guān)于數(shù)據(jù)類型的信息旭绒。每個程序?qū)ο罂梢院唵蔚匾暈橐粋€字節(jié)塊,而程序本身就是一個字節(jié)序列焦人。

給C語言初學(xué)者 C語言中指針的作用

指針是 C 語言的一個重要特性挥吵。它提供了引用數(shù)據(jù)結(jié)構(gòu)(包括數(shù)組)的元素的機(jī)制。與變量類似花椭,指針也有兩個方面:值和類型忽匈。它的值表示某個對象的位置,而它的類型表示那個位置上所存儲對象的類型(比如整數(shù)或者浮點數(shù))矿辽。

真正理解指針需要查看它們在機(jī)器級上的表示以及實現(xiàn)丹允。這將是第3章的重點之一,3.10.1節(jié)將對其進(jìn)行深入介紹袋倔。

2. 1. 1 十六進(jìn)制表示法

一個字節(jié)由8位組成雕蔽。在二進(jìn)制表示法中,它的值域是 00000000_2?11111111_2奕污。如果看成十進(jìn)制整數(shù)萎羔,它的值域就是0_{10}?255_{10}液走。兩種符號表示法對于描述位模式來說都不是非常方便碳默。二進(jìn)制表示法太冗長贾陷,而十進(jìn)制表示法與位模式的互相轉(zhuǎn)化很麻煩。替代的方法是嘱根,以16為基數(shù)髓废,或者叫做十六進(jìn)制(hexadecimal)數(shù),來表示位模式该抒。十六進(jìn)制(簡寫為“hex”)使用數(shù)字 ‘0’?‘9’ 以及字符 ‘A’?‘F’ 來表示16個可能的值慌洪。圖2-2展示了16個十六進(jìn)制數(shù)字對應(yīng)的十進(jìn)制值和二進(jìn)制值。用十六進(jìn)制書寫凑保,一個字節(jié)的值域為00_{16}?FF_{16}冈爹。

在C語言中,以 0x 或 0X 開頭的數(shù)字常量被認(rèn)為是十六進(jìn)制的值欧引。字符 ‘A’?‘F’ 既可以是大寫频伤,也可以是小寫。例如芝此,我們可以將數(shù)字FA1D37B_{16}寫作 0xFA1D37B憋肖,或者 0xfa1d37b,甚至是大小寫混合婚苹,比如岸更,0xFa1D37b。在本書中膊升,我們將使用C表示法來表品十六進(jìn)制值怎炊。

編寫機(jī)器級程序的一個常見任務(wù)就是在位模式的十進(jìn)制、二進(jìn)制和十六進(jìn)制表示之間人工轉(zhuǎn)換廓译。二進(jìn)制和十六進(jìn)制之間的轉(zhuǎn)換比較簡單直接结胀,因為可以一次執(zhí)行一個十六進(jìn)制數(shù)字的轉(zhuǎn)換。數(shù)字的轉(zhuǎn)換可以參考如圖2-2所示的表责循。一個簡單的竅門是糟港,記住十六進(jìn)制數(shù)字 A、C 和 F 相應(yīng)的十進(jìn)制值院仿。而對于把十六進(jìn)制值 B秸抚、D 和 E 轉(zhuǎn)換成十進(jìn)制值,則可以通過計算它們與前三個值的相對關(guān)系來完成歹垫。

比如剥汤,假設(shè)給你一個數(shù)字 0xl73A4C∨挪遥可以通過展開每個十六進(jìn)制數(shù)字吭敢,將它轉(zhuǎn)換為二進(jìn)制格式,如下所示:

十六進(jìn)制 1 7 3 A 4 C
二進(jìn)制 0001 0111 0011 1010 0100 1100

這樣就得到了二進(jìn)制表示000101110011101001001100暮芭。
反過來鹿驼,如果給定一個二進(jìn)制數(shù)字1111001010110110110011,可以通過首先把它分為每4位一組來轉(zhuǎn)換為十六進(jìn)制欲低。不過要注意,如果位總數(shù)不是4的倍數(shù)畜晰,最左邊的一組可以少于4位砾莱,前面用0補(bǔ)足。然后將每個4位組轉(zhuǎn)換為相應(yīng)的十六進(jìn)制數(shù)字:

二進(jìn)制 11 1100 1010 1101 1011 0011
十六進(jìn)制 3 C A D B 3
  • ? 練習(xí)題2. 1 完成下面的數(shù)字轉(zhuǎn)換:

    A. 將0x39A7F8轉(zhuǎn)換為二進(jìn)制凄鼻。

    B. 將二進(jìn)制1100100101111011轉(zhuǎn)換為十六進(jìn)制腊瑟。

    C. 將0xD5E4C轉(zhuǎn)換為二進(jìn)制。

    D. 將二進(jìn)制1001101110011110110101轉(zhuǎn)換為十六進(jìn)制块蚌。

當(dāng)值 χ 是 2 的非負(fù)整數(shù) n 次冪時闰非,也就是χ=2^n 我們可以很容易地將 χ 寫成十六進(jìn)制形式,只要記住 χ 的二進(jìn)制表示就是1后面跟 n 個 0峭范。十六進(jìn)制數(shù)字 0 代表4個二進(jìn)制 0河胎。所以,當(dāng) n 表示成 i + 4j 的形式虎敦,其中 0≤i≤3游岳,我們可以把 χ 寫成開頭的十六進(jìn)制數(shù)字為 1(i=0)、2(i=1)其徙、4(i=2)或者8(i=3)胚迫,后面跟隨著 j 個十六進(jìn)制的0。比如唾那,χ=2048=2^{11}访锻,我們有 n=11=3 + 4 * 2,從而得到十六進(jìn)制表示 0x800闹获。

  • ? 練習(xí)題2.2 填寫下表中的空白項期犬,給出2的不同次冪的二進(jìn)制和十六進(jìn)制表示:
n 2^n(十進(jìn)制) 2^n(十六進(jìn)制)
9 512 0x200
19
16384
0x10000
17
32
0x80

十進(jìn)制和十六進(jìn)制表示之間的轉(zhuǎn)換需要使用乘法或者除法來處理一般情況。將一個十進(jìn)制數(shù)字 χ 轉(zhuǎn)換為十六進(jìn)制避诽,可以反復(fù)地用 16 除 χ龟虎,得到一個商 q 和一個余數(shù) r,也就是 χ = q * 16 + r沙庐。然后鲤妥,我們用十六進(jìn)制數(shù)字表示的 r 作為最低位數(shù)字,并且通過對 q 反復(fù)
進(jìn)行這個過程得到剩下的數(shù)字拱雏。例如棉安,考慮十進(jìn)制 314 156的轉(zhuǎn)換:

314156 = 19634 - 16 +12 (C)
19 634= 1227 \cdot 16+2 (2)
1227= 76 \cdot 16 +11 (B)
76= 4 - 16 + 12 (C)
4= 0 \cdot 16 +4 (4)

從這里,我們能讀出十六進(jìn)制表示為 0x4CB2C铸抑。

反過來贡耽,將一個十六進(jìn)制數(shù)字轉(zhuǎn)換為十進(jìn)制數(shù)字,我們可以用相應(yīng)的16的冪乘以每個十六進(jìn)制數(shù)字。比如蒲赂,給定數(shù)字 0x7AF,我們計算它對應(yīng)的十進(jìn)制值為 7 \cdot 16^2 + 10 \cdot 16 +15 = 7 \cdot 256 + 10 \cdot 16 +15 = 1792+160+15 =1967

  • ? 練習(xí)題2.3 一個字節(jié)可以用兩個十六進(jìn)制數(shù)字來表示阱冶。填寫下表中缺失的項,給出不同字節(jié)模式的十進(jìn)制凳宙、二進(jìn)制和十六進(jìn)制值:
十進(jìn)制 二進(jìn)制 十六進(jìn)制
0 0000 0000 0x00
167
62
188
0011 0111
1000 1000
1111 0011
0x52
OxAC
0xE7

旁注 十進(jìn)制和十六進(jìn)制間的轉(zhuǎn)換
較大數(shù)值的十進(jìn)制和十六進(jìn)制之間的轉(zhuǎn)換,最好是讓計算機(jī)或者計算器來完成职祷。有大量的工具可以完成這個工作氏涩。一個簡單的方法就是利用任何標(biāo)準(zhǔn)的搜索引擎,比如查詢:

把0xabcd轉(zhuǎn)換為十進(jìn)制數(shù)

把123用十六進(jìn)制表示有梆。

  • ? 練習(xí)題2.4 不將數(shù)字轉(zhuǎn)換為十進(jìn)制或者二進(jìn)制是尖,試著解答下面的算術(shù)題,答案要用十六進(jìn)制表示泥耀。提示:只要將執(zhí)行十進(jìn)制加法和減法所使用的方法改成以16為基數(shù)饺汹。

    A. 0x503c+0x8=

    B. 0x503c-0x40=

    C. 0x503c+64=

    D. 0x50ea-0x503c=

2.1.2 字?jǐn)?shù)據(jù)大小

每臺計算機(jī)都有一個字長 (word size),指明指針數(shù)據(jù)的標(biāo)稱大小 (nominal size)。因為虛擬地址是以這樣的一個字來編碼的痰催,所以字長決定的最重要的系統(tǒng)參數(shù)就是虛擬地址空間的最大大小兜辞。也就是說,對于一個字長為 w 位的機(jī)器而言夸溶,虛擬地址的范圍為 0 \backsim 2^W-1,程序最多訪問 2^w 個字節(jié)逸吵。

最近這些年,出現(xiàn)了大規(guī)模的從32位字長機(jī)器到64位字長機(jī)器的遷移缝裁。這種情況首先出現(xiàn)在為大型科學(xué)和數(shù)據(jù)庫應(yīng)用設(shè)計的高端機(jī)器上扫皱,之后是臺式機(jī)和筆記本電腦,最近則出現(xiàn)在智能手機(jī)的處理器上捷绑。32位字長限制虛擬地址空間為4千兆字節(jié)(寫作4GB),也就是說韩脑,剛剛超過 4 \times 10^9 字節(jié)。擴(kuò)展到 64 位字長使得虛擬地址空間為 16EB粹污,大約是 1.84 \times 10^{19} 字節(jié)段多。

大多數(shù)64位機(jī)器也可以運(yùn)行為32位機(jī)器編譯的程序,這是一種向后兼容壮吩。因此衩匣,舉例來說,當(dāng)程序prog.c用如下偽指令編譯后

linux> gcc -m32 prog.c

該程序就可以在32位或64位機(jī)器上正確運(yùn)行粥航。另一方面琅捏,若程序用下述偽指令編譯

linux> gcc -m64 prog.c

那就只能在64位機(jī)器上運(yùn)行。因此递雀,我們將程序稱為“32位程序”或“64位程序”時柄延,區(qū)別在于該程序是如何編譯的, 而不是其運(yùn)行的機(jī)器類型。

計算機(jī)和編譯器支持多種不同方式編碼的數(shù)字格式搜吧,如不同長度的整數(shù)和浮點數(shù)市俊。比如,許多機(jī)器都有處理單個字節(jié)的指令滤奈,也有處理表示為2字節(jié)摆昧、4字節(jié)或者8字節(jié)整數(shù)的指令,還有些指令支持表示為4字節(jié)和8字節(jié)的浮點數(shù)蜒程。

圖2-3 基本 C 數(shù)據(jù)類型的典型大小(以字節(jié)為單位)绅你。分配的字節(jié)數(shù)受程序是如何編譯的影響而變化。本圖給出的是32位和64位程序的典型值

C語言支持整數(shù)和浮點數(shù)的多種數(shù)據(jù)格式昭躺。圖2-3 展示了為 C 語言各種數(shù)據(jù)類型分配的字節(jié)數(shù)杠步。(我們在2.2節(jié)討論 C 標(biāo)準(zhǔn)保證的字節(jié)數(shù)和典型的字節(jié)數(shù)之間的關(guān)系董习。)有些數(shù)據(jù)類型的確切字節(jié)數(shù)依賴于程序是如何被編譯的饭耳。我們給出的是32位和64位程序的典型值遗增。整數(shù)或者為有符號的,即可以表示負(fù)數(shù)帝洪、零和正數(shù)似舵;或者為無符號的,即只能表示非負(fù)數(shù)葱峡。C 的數(shù)據(jù)類型 char 表示一個單獨的字節(jié)啄枕。盡管 “char” 是由于它被用來存儲文本串中的單個字符這一事實而得名,但它也能被用來存儲整數(shù)值族沃。數(shù)據(jù)類型 short频祝,int 和 long 可以提供各種數(shù)據(jù)大小。即使是為64位系統(tǒng)編譯脆淹,數(shù)據(jù)類型 int 通常也只有 4 個字節(jié)常空。數(shù)據(jù)類型 long —般在 32位 程序中為 4 字節(jié),在 64 位程序中則為 8 字節(jié)盖溺。

為了避免由于依賴“典型”大小和不同編譯器設(shè)置帶來的奇怪行為漓糙,ISO C99 引入了一類數(shù)據(jù)類型,其數(shù)據(jù)大小是固定的烘嘱,不隨編譯器和機(jī)器設(shè)置而變化昆禽。其中就有數(shù)據(jù)類型 int32_t 和 int64_t,它們分別為4個字節(jié)和8個字節(jié)蝇庭。使用確定大小的整數(shù)類型是程序
員準(zhǔn)確控制數(shù)據(jù)表示的最佳途徑醉鳖。

大部分?jǐn)?shù)據(jù)類型都編碼為有符號數(shù)值,除非有前綴關(guān)鍵字 unsigned 或?qū)Υ_定大小的
數(shù)據(jù)類型使用了特定的無符號聲明哮内。數(shù)據(jù)類型 char 是一個例外盗棵。盡管大多數(shù)編譯器和機(jī)器將它們視為有符號數(shù),但 C 標(biāo)準(zhǔn)不保證這一點。相反纹因,正如方括號指示的那樣喷屋,程序員應(yīng)該用有符號字符的聲明來保證其為一個字節(jié)的有符號數(shù)值。不過瞭恰,在很多情況下屯曹,程序行為對數(shù)據(jù)類型 char 是有符號的還是無符號的并不敏感。

對關(guān)鍵字的順序以及包括還是省略可選關(guān)鍵字來說惊畏,C 語言允許存在多種形式恶耽。比
如,下面所有的聲明都是一個意思:

unsigned long

unsigned long int

long unsigned

long unsigned int

我們將始終使用圖2-3給出的格式陕截。

圖2-3還展示了指針(例如一個被聲明為類型為“char * ”的變量)使用程序的全字長驳棱。大多數(shù)機(jī)器還支持兩種不同的浮點數(shù)格式:單精度(在 C 中聲明為float)和雙精度(在 C 中聲明為 double)批什。這些格式分別使用4字節(jié)和8字節(jié)农曲。

給C語言初學(xué)者 聲明指針
對于任何數(shù)據(jù)類型 T,聲明
T *p;
表明 p 是一個指針變量驻债,指向一個類型為 T 的對象乳规。例如,
char *p;
就將一個指針聲明為指向一個char類型的對象合呐。

程序員應(yīng)該力圖使他們的程序在不同的機(jī)器和編譯器上可移植暮的。可移植性的一個方面就是使程序?qū)Σ煌瑪?shù)據(jù)類型的確切大小不敏感淌实。C 語言標(biāo)準(zhǔn)對不同數(shù)據(jù)類型的數(shù)字范圍設(shè)置了下界(這點在后面還將講到)冻辩,但是卻沒有上界。因為從 1980 年左右到 2010 年左右拆祈,32 位機(jī)器和 32 位程序是主流的組合恨闪,許多程序的編寫都假設(shè)為圖2-3 中 32 位程序的字節(jié)分配。隨著 64 位機(jī)器的日益普及放坏,在將這些程序移植到新機(jī)器上時咙咽,許多隱藏的對字長的依賴性就會顯現(xiàn)出來,成為錯誤淤年。比如钧敞,許多程序員假設(shè)一個聲明為 int 類型的程序?qū)ο竽鼙挥脕泶鎯σ粋€指針。這在大多數(shù) 32 位的機(jī)器上能正常工作麸粮,但是在一臺 64 位的機(jī)器上卻會導(dǎo)致問題溉苛。

2.1.3 尋址和字節(jié)順序

對于跨越多字節(jié)的程序?qū)ο螅覀儽仨毥蓚€規(guī)則:這個對象的地址是什么弄诲,以及在內(nèi)存中如何排列這些字節(jié)炊昆。在幾乎所有的機(jī)器上,多字節(jié)對象都被存儲為連續(xù)的字節(jié)序列,對象的地址為所使用字節(jié)中最小的地址凤巨。例如视乐,假設(shè)一個類型為 int 的變量 x 的地址為 0x100,也就是說,地址表達(dá)式 &x 的值為 0xl00敢茁。那么佑淀,(假設(shè)數(shù)據(jù)類型 int 為 32 位表示)x 的 4 個字節(jié)將被存儲在內(nèi)存的 0x100、0x101彰檬、0x102 和 0x103 位置伸刃。

排列表示一個對象的字節(jié)有兩個通用的規(guī)則》瓯叮考慮一個 w 位的整數(shù)捧颅,其位表示為 [x_{\omega-1}, x_{\omega-2},\cdots,x_1,x_0],其中 x_{\omega-1} 是最高有效位较雕,而 x_0 最低有效位碉哑。假設(shè) \omega 是 8 的倍數(shù),這些位就能被分組成為字節(jié)亮蒋,其中最高有效字節(jié)包含位 [x_{\omega-1},x_{\omega-2},\cdots,x_{\omega-8}]扣典,而最低有效字節(jié)包含位 [x_7,x_6,\cdots,x_0],其他字節(jié)包含中間的位慎玖。某些機(jī)器選擇在內(nèi)存中按照從最低有效字節(jié)到最高有效字節(jié)的順序存儲對象贮尖,而另一些機(jī)器則按照從最高有效字節(jié)到最低有效字節(jié)的順序存儲。前一種規(guī)則——最低有效字節(jié)在最前面的方式趁怔,稱為小端法(little endian)湿硝。后一種規(guī)則——最高有效字節(jié)在最前面的方式,稱為大端法(big endian)润努。

假設(shè)變量 x 的類型為 int关斜,位于地址0x100處,它的十六進(jìn)制值為 0x01234567任连。地
址范圍 0x100 ~ 0x103的字節(jié)順序依賴于機(jī)器的類型:

image

注意蚤吹,在字 0x01234567 中,高位字節(jié)的十六進(jìn)制值為 0x01随抠,而低位字節(jié)值為 0x67裁着。

大多數(shù)Intel兼容機(jī)都只用小端模式。另一方面拱她,IBM 和 Oracle(從其2010年收購
Sun Microsystems開始)的大多數(shù)機(jī)器則是按大端模式操作二驰。注意我們說的是“大多數(shù)”。這些規(guī)則并沒有嚴(yán)格按照企業(yè)界限來劃分秉沼。比如桶雀,IBM 和 Oracle 制造的個人計算機(jī)使用的是 Intel 兼容的處理器矿酵,因此使用小端法。許多比較新的微處理器是雙端法(bi-endian)矗积,也就是說可以把它們配置成作為大端或者小端的機(jī)器運(yùn)行全肮。然而,實際情況是:一旦選擇了特定操作系統(tǒng)棘捣,那么字節(jié)順序也就固定下來辜腺。比如,用于許多移動電話的ARM微處理器乍恐,其硬件可以按小端或大端兩種模式操作评疗,但是這些芯片上最常見的兩種操作系統(tǒng)——Android(來自Google)和 IOS(來自Apple)卻只能運(yùn)行于小端模式。

令人吃驚的是茵烈,在哪種字節(jié)順序是合適的這個問題上百匆,人們表現(xiàn)得非常情緒化。實際上呜投,術(shù)語 “l(fā)ittle endian(小端)” 和 “big endian(大端)” 出自Jonathan Swift的《格利佛游記》(Gulliver's Travels)一書加匈,其中交戰(zhàn)的兩個派別無法就應(yīng)該從哪一端(小端還是大端)打開一個半熟的雞蛋達(dá)成一致。就像雞蛋的問題一樣宙彪,選擇何種字節(jié)順序沒有技術(shù)上的理由矩动,因此爭論淪為關(guān)于社會政治論題的爭論有巧。只要選擇了一種規(guī)則并且始終如一地堅持释漆,對于哪種字節(jié)排序的選擇都是任意的。

旁注 “端”的起源

以下是 Jonathan Swift 在 1726 年關(guān)于大小端之爭歷史的描述:
“……我下面要告訴你的是篮迎,Lilliput 和 Blefuscu 這兩大強(qiáng)國在過去36個月里一直在苦戰(zhàn)男图。戰(zhàn)爭開始是由于以下的原因:我們大家都認(rèn)為,吃雞蛋前甜橱,原始的方法是打破雞蛋較大的一端逊笆,可是當(dāng)今皇帝的祖父小時候吃雞蛋,一次按古法打雞蛋時碰巧將一個手指弄破了岂傲,因此他的父親难裆,當(dāng)時的皇帝,就下了一道敕令镊掖,命令全體臣民吃雞蛋時打破雞蛋較小的一端乃戈,違令者重罰。老百姓們對這項命令極為反感亩进。歷史告訴我們症虑,由此曾發(fā)生過六次叛亂,其中一個皇帝送了命归薛,另一個丟了王位谍憔。這些叛亂大多都是由Blefuscu的國王大臣們煽動起來的匪蝙。叛亂平息后,流亡的人總是逃到那個帝國去尋救避難习贫。據(jù)估計逛球,先后幾次有11000人情愿受死也不肯去打破雞蛋較小的一端。關(guān)于這一爭端苫昌,曾出版過幾百本大部著作需忿,不過大端派的書一直是受禁的,法律也規(guī)定該派的任何人不得做官蜡歹∥堇澹”(此段譯文摘自網(wǎng)上蔣劍鋒譯的《格利佛游記》第一卷第4章。)

在他那個時代月而,Swift是在諷刺英國 (Lilliput) 和法國 (Blefuscu) 之間持續(xù)的沖突汗洒。Danny Cohen, 一位網(wǎng)絡(luò)協(xié)議的早期開創(chuàng)者,第一次使用這兩個術(shù)語來指代字節(jié)順序父款,后來這個術(shù)語被廣泛接納了溢谤。

對于大多數(shù)應(yīng)用程序員來說,其機(jī)器所使用的字節(jié)順序是完全不可見的憨攒。無論為哪種類型的機(jī)器所編譯的程序都會得到同樣的結(jié)果世杀。不過有時候,字節(jié)順序會成為問題肝集。首先是在不同類型的機(jī)器之間通過網(wǎng)絡(luò)傳送二進(jìn)制數(shù)據(jù)時瞻坝,一個常見的問題是當(dāng)小端法機(jī)器產(chǎn)生的數(shù)據(jù)被發(fā)送到大端法機(jī)器或者反過來時,接收程序會發(fā)現(xiàn)杏瞻,字里的字節(jié)成了反序的所刀。為了避免這類問題,網(wǎng)絡(luò)應(yīng)用程序的代碼編寫必須遵守已建立的關(guān)于字節(jié)順序的規(guī)則捞挥,以確保發(fā)送方機(jī)器將它的內(nèi)部表示轉(zhuǎn)換成網(wǎng)絡(luò)標(biāo)準(zhǔn)浮创,而接收方機(jī)器則將網(wǎng)絡(luò)標(biāo)準(zhǔn)轉(zhuǎn)換為它的內(nèi)部表示。我們將在第11章中看到這種轉(zhuǎn)換的例子砌函。

第二種情況是斩披,當(dāng)閱讀表示整數(shù)數(shù)據(jù)的字節(jié)序列時字節(jié)順序也很重要。這通常發(fā)生在檢查機(jī)器級程序時讹俊。作為一個示例垦沉,從某個文件中摘出了下面這行代碼,該文件給出了一個針對 Intel x86-64處理器的機(jī)器級代碼的文本表示:

 4004d3: 01 05 43 0b 20 00 add %eax,0x200b43(%rip)

這一行是由反匯編器(disassembler)生成的劣像,反匯編器是一種確定可執(zhí)行程序文件所表示的指令序列的工具乡话。我們將在第3章中學(xué)習(xí)有關(guān)這些工具的更多知識,以及怎樣解釋像這樣的行耳奕。而現(xiàn)在绑青,我們只是注意這行表述的意思是:十六進(jìn)制字節(jié)串 01 05 43 0b 20 00 是一條指令的字節(jié)級表示诬像,這條指令是把一個字長的數(shù)據(jù)加到一個值上,該值的存儲地址由 0x200b43 加上當(dāng)前程序計數(shù)器的值得到闸婴,當(dāng)前程序計數(shù)器的值即為下一條將要執(zhí)行指令的地址坏挠。如果取出這個序列的最后4個字節(jié):43 0b 20 00,并且按照相反的順序?qū)懗鲂罢В覀兊玫?00 20 0b 43降狠。去掉開頭的 0,得到值0x200b43,這就是右邊的數(shù)值庇楞。當(dāng)閱讀像此類小端法機(jī)器生成的機(jī)器級程序表示時榜配,經(jīng)常會將字節(jié)按照相反的順序顯示。書寫字節(jié)序列的自然方式是最低位字節(jié)在左邊吕晌,而最高位字節(jié)在右邊蛋褥,這正好和通常書寫數(shù)字時最高有效位在左邊,最低有效位在右邊的方式相反睛驳。

字節(jié)順序變得重要的第三種情況是當(dāng)編寫規(guī)避正常的類型系統(tǒng)的程序時烙心。在 C 語言中,可以通過使用強(qiáng)制類型轉(zhuǎn)換 (cast)聯(lián)合 (union) 來允許以一種數(shù)據(jù)類型引用一個對象乏沸,而這種數(shù)據(jù)類型與創(chuàng)建這個對象時定義的數(shù)據(jù)類型不同淫茵。大多數(shù)應(yīng)用編程都強(qiáng)烈不推薦這種編碼技巧,但是它們對系統(tǒng)級編程來說是非常有用蹬跃,甚至是必需的匙瘪。

圖2-4 展示了一段 C 代碼,它使用強(qiáng)制類型轉(zhuǎn)換來訪問和打印不同程序?qū)ο蟮淖止?jié)表示炬转。我們用 typedef 將數(shù)據(jù)類型 byte_pointer 定義為一個指向類型為 “unsignedchar” 的對象的指針辆苔。這樣一個字節(jié)指針引用一個字節(jié)序列算灸,其中每個字節(jié)都被認(rèn)為是一個非負(fù)整數(shù)扼劈。第一個例程 show_bytes 的輸入是一個字節(jié)序列的地址,它用一個字節(jié)指針以及一個字節(jié)數(shù)來指示菲驴。該字節(jié)數(shù)指定為數(shù)據(jù)類型 size_t荐吵,表示數(shù)據(jù)結(jié)構(gòu)大小的首選數(shù)據(jù)類型。show_bytes 打印出每個以十六進(jìn)制表示的字節(jié)赊瞬。C 格式化指令 “%.2x” 表明整數(shù)必須用至少兩個數(shù)字的十六進(jìn)制格式輸出先煎。

#include <stdio.h>

typedef unsigned char *byte_pointer;

void show_bytes(byte_pointer start, size_t len) {
    size_t i;
    for (i = 0; i < len; i++)
        printf(" %.2x", start[i]);
    printf("\n");
}

void show_int(int x) {
    show_bytes((bypte_pointer) &x, sizeof(int));
}

void show_float(float x) {
    show_bytes((byte_pointer) &x, sizeof(float));
}

void show_pointer(void *x) {
    show_bytes((byte_pointer) &x, sizeof(void *));
}

圖2-4 打印程序?qū)ο蟮淖止?jié)表示。這段代碼使用強(qiáng)制類型轉(zhuǎn)換來規(guī)避類型系統(tǒng)巧涧。很容易定義針對其他數(shù)據(jù)類型的類似函數(shù)

過程 show_int薯蝎、show_float 和 show_pointer 展示了如何使用程序 show_bytes 來分別輸出類型為int、float和 void * 的 C 程序?qū)ο蟮淖止?jié)表示谤绳≌季猓可以觀察到它們僅僅傳遞給 show_bytes 一個指向它們參數(shù) x 的指針 &x袒哥,且這個指針被強(qiáng)制類型轉(zhuǎn)換為 "unsigned char * ”。這種強(qiáng)制類型轉(zhuǎn)換告訴編譯器消略,程序應(yīng)該把這個指針看成指向一個字節(jié)序列堡称,而不是指向一個原始數(shù)據(jù)類型的對象。然后艺演,這個指針會被看成是對象使用的最低字節(jié)地址却紧。

這些過程使用C語言的運(yùn)算符 sizeof 來確定對象使用的字節(jié)數(shù)。一般來說胎撤,表達(dá)式 sizeof(T) 返回存儲一個類型為 T 的對象所需要的字節(jié)數(shù)晓殊。使用 sizeof 而不是一個固定的值,是向編寫在不同機(jī)器類型上可移植的代碼邁進(jìn)了一步伤提。

在幾種不同的機(jī)器上運(yùn)行如圖2-5 所示的代碼挺物,得到如圖2-6 所示的結(jié)果。我們使用
了以下幾種機(jī)器:

Linux 32:運(yùn)行 Linux 的 Intel IA32 處理器飘弧。

Windows:運(yùn)行 Windows 的 Intel IA32 處理器识藤。

Sun:運(yùn)行Solaris的 Sun Microsystems SPARC 處理器。(這些機(jī)器現(xiàn)在由Oracle生產(chǎn)次伶。)

Linux 64:運(yùn)行 Linux 的 Intel x86-64 處理器痴昧。

-------------------------------- code/data/show-bytes.c
void test_show_bytes(int val) {
    int ival = val;
    float fval = (float) ival;
    int *pval = &ival;
    show_int(ival);
    show_float(fval);
    show_pointer(pval);
}
-------------------------------- code/data/show-bytes.c

圖2-5 字節(jié)表示的示例,這段代碼打印示例數(shù)據(jù)對象的字節(jié)表示

圖2-6 不同數(shù)據(jù)值的字節(jié)表示冠王。除了字節(jié)順序以外赶撰,int和float的結(jié)果是一樣的。指針值與機(jī)器相關(guān)

參數(shù)12345的十六進(jìn)制表示為 0x00003039柱彻。對于int類型的數(shù)據(jù)豪娜,除了字節(jié)順序以外,我們在所有機(jī)器上都得到相同的結(jié)果哟楷。特別地瘤载,我們可以看到在 Linux32、Windows 和 Linux64上卖擅,最低有效字節(jié)值0x39最先輸出鸣奔,這說明它們是小端法機(jī)器;而在Sun 上最后輸出惩阶,這說明Sun是大端法機(jī)器挎狸。同樣地,float 數(shù)據(jù)的字節(jié)断楷,除了字節(jié)順序以外锨匆,也都是相同的。另一方面冬筒,指針值卻是完全不同的恐锣。不同的機(jī)器/操作系統(tǒng)配置使用不同的存儲分配規(guī)則紊遵。一個值得注意的特性是 Linux32、Windows 和 Sun的機(jī)器使用 4 字節(jié)地址侥蒙,而 Linux64 使用8字節(jié)地址暗膜。

給C語言初學(xué)者 使用 typedef 來命名數(shù)據(jù)類型

C 語言中的 typedef 聲明提供了一種給數(shù)據(jù)類型命名的方式。這能夠極大地改善代
碼的可讀性鞭衩,因為深度嵌套的類型聲明很難讀懂学搜。

typedef的語法與聲明變量的語法十分相像,除了它使用的是類型名论衍,而不是變量名瑞佩。因此,圖2-4 中 byte_pointer 的聲明和將一個變量聲明為類型 “unsigned char * ”有相同的形式坯台。

例如炬丸,聲明:

typedef int *int_pointer;

int_pointer ip;

將類型 “int_pointer” 定義為一個指向int的指針,并且聲明了一個這種類型的變量 ip蜒蕾。我們還可以將這個變量直接聲明為:

int *ip;

給 C 語言初學(xué)者 使用 printf格式化輸出

printf函數(shù)(還有它的同類 fprintf 和 sprintf )提供了一種打印信息的方式稠炬,這種方式對格式化細(xì)節(jié)有相當(dāng)大的控制能力。第一個參數(shù)是格式串(format string),而其余的參數(shù)都是要打印的值咪啡。在格式串里首启,每個以 “%” 開始的字符序列都表示如何格式化下一個參數(shù)。典型的示例包括:‘%d’ 是輸出一個十進(jìn)制整數(shù)撤摸,‘%f’ 是輸出一個浮點數(shù)毅桃,而 ‘%c’ 是輸出一個字符,其編碼由參數(shù)給出准夷。

指定確定大小數(shù)據(jù)類型的格式钥飞,如 int32_t,要更復(fù)雜一些衫嵌,相關(guān)內(nèi)容參見 2.2.3 節(jié)的旁注读宙。

可以觀察到,盡管浮點型和整型數(shù)據(jù)都是對數(shù)值12345編碼渐扮,但是它們有截然不同的
字節(jié)模式:整型為 0x00003039论悴,而浮點數(shù)為 0x4640E400。一般而言墓律,這兩種格式使用不同的編碼方法。如果我們將這些十六進(jìn)制模式擴(kuò)展為二進(jìn)制形式幔亥,并且適當(dāng)?shù)貙⑺鼈円莆怀芊恚蜁l(fā)現(xiàn)一個有13個相匹配的位的序列,用一串星號標(biāo)識出來:

\begin{matrix} 0 & 0 & 0 & 0 & 3 & 0 & 3 & 9 & & & \\ 0000&0000&0000&0000&0011&0000&0011&1001& & & \\ & & & & * &****&****&****& & & \\ & & & 4 & 6 & 4 & 0 & E & 4 & 0 & 0 \\ & & 01&0001&1001&0000&0011&1001&0000&0000&00 \\ \end{matrix}

這并不是巧合帕棉。當(dāng)我們研究浮點數(shù)格式時针肥,還將再回到這個例子饼记。

給 C 語言初學(xué)者 指針和數(shù)組

在函數(shù) show_bytes(圖2-4)中,我們看到指針和數(shù)組之間緊密的聯(lián)系慰枕,這將在 3.8 節(jié)中詳細(xì)描述具则。這個函數(shù)有一個類型為 byte_pointer(被定義為一個指向 unsigned char 的指針)的參數(shù) start,但是我們在第 8 行上看到數(shù)組引用start[i]具帮。在 C 語言中博肋,我們能夠用數(shù)組表示法來引用指針,同時我們也能用指針表示法來引用數(shù)組元素蜂厅。在這個例子中匪凡,引用 start[i] 表示我們想要讀取以 start 指向的位置為起始的第i個位置處的字節(jié)。

給 C 語言初學(xué)者 指針的創(chuàng)建和間接引用
在圖2-4的第 13掘猿、17和 21 行病游,我們看到對 C 和 C++ 中兩種獨有操作的使用。C 的“取地址”運(yùn)算符 & 創(chuàng)建一個指針稠通。在這三行中衬衬,表達(dá)式 &x 創(chuàng)建了一個指向保存變量 x 的位置的指針。這個指針的類型取決于 x 的類型改橘,因此這三個指針的類型分別為int佣耐、float 和 void**。(數(shù)據(jù)類型 void * 是一種特殊類型的指針唧龄,沒有相關(guān)聯(lián)的類型信息兼砖。)

強(qiáng)制類型轉(zhuǎn)換運(yùn)算符可以將一種數(shù)據(jù)類型轉(zhuǎn)換為另一種。因此既棺,強(qiáng)制類型轉(zhuǎn)換(byte_pointer) &x 表明無論指針 &x 以前是什么類型讽挟,它現(xiàn)在就是一個指向數(shù)據(jù)類型為 unsigned char 的指針。這里給出的這些強(qiáng)制類型轉(zhuǎn)換不會改變真實的指針丸冕,它們只是告訴編譯器以新的數(shù)據(jù)類型來看待被指向的數(shù)據(jù)耽梅。

旁注 生成一張ASCII表

可以通過執(zhí)行命令 man ascii 來得到一張ASCII字符碼的表。

  • ? 練習(xí)題2.5 思考下面對 show_bytes 的三次調(diào)用:

    int val = 0x87654321;

    byte_pointer valp = (byte_pointer) &val;

    show_bytes(valp, 1); /* A. /

    show_bytes(valp, 2); /
    B. /

    show_bytes(valp, 3); /
    C. * /

    指出在小端法機(jī)器和大端法機(jī)器上胖烛,每次調(diào)用的輸出值完沪。

    A. 小端法: 大端法:

    B. 小端法: 大端法:

    C. 小端法: 大端法:

  • ? 練習(xí)題2.6 使用show_int 和 show_float栅盲,我們確定整數(shù) 3510593 的十六進(jìn)制表示為 0x00359141,而浮點數(shù) 3510593.0 的十六進(jìn)制表示為 0x4A564504。

    A. 寫出這兩個十六進(jìn)制值的二進(jìn)制表示又厉。

    B. 移動這兩個二進(jìn)制串的相對位置,使得它們相匹配的位數(shù)最多瘟判。有多少位相匹配呢抡句?

    C. 串中的什么部分不相匹配?

2.1.4 表示字符串

C 語言中字符串被編碼為一個以 null(其值為0)字符結(jié)尾的字符數(shù)組。每個字符都由某個標(biāo)準(zhǔn)編碼來表示利朵,最常見的是ASCII字符碼律想。因此,如果我們以參數(shù) “12345” 和 6 (包括終止符)來運(yùn)行例程 show_bytes绍弟,我們得到結(jié)果 31 32 33 34 35 00技即。請注意,十進(jìn)制數(shù)字 x 的 ASCII 碼正好是 0x3x樟遣,而終止字節(jié)的十六進(jìn)制表示為 0x00而叼。在使用ASCII碼作為字符碼的任何系統(tǒng)上都將得到相同的結(jié)果,與字節(jié)順序和字大小規(guī)則無關(guān)年碘。因而澈歉,文本數(shù)據(jù)比二進(jìn)制數(shù)據(jù)具有更強(qiáng)的平臺獨立性。

  • ? 練習(xí)題 2.7 下面對 show_bytes 的調(diào)用將輸出什么結(jié)果屿衅?

    const char *s = "abcdef";
    show_bytes((byte_pointer) s, strlen(s));

旁注 文字編碼的Unicode標(biāo)準(zhǔn)

ASCII 字符集適合于編碼英語文檔埃难,但是在表達(dá)一些特殊字符方面并沒有太多辦法,例如法語的 “?”涤久。它完全不適合編碼希臘語涡尘、俄語和中文等語言的文檔。這些年响迂,提出了很多方法來對不同語言的文字進(jìn)行編碼考抄。Unicode 聯(lián)合會 (Unicode Consortium) 修訂了最全面且廣泛接受的文字編碼標(biāo)準(zhǔn)。當(dāng)前的 Unicode 標(biāo)準(zhǔn)(7.0版)的字庫包括將近 100000 個字符蔗彤,支持廣泛的語言種類川梅,包括古埃及和巴比倫的語言。為了保持信用然遏,Unicode 技術(shù)委員會否決了為 Klingon(即電視連續(xù)劇《星際迷航》中的虛構(gòu)文明)編寫語言標(biāo)準(zhǔn)的提議贫途。

基本編碼,稱為 Unicode 的“統(tǒng)一字符集”待侵,使用32位來表示字符丢早。這好像要求文本串中每個字符要占用 4 個字節(jié)。不過秧倾,可以有一些替代編碼怨酝,常見的字符只需要 1 個或 2 個字節(jié),而不太常用的字符需要多一些的字節(jié)數(shù)那先。特別地农猬,UTF-8 表示將每個字符編碼為一個字節(jié)序列,這樣標(biāo)準(zhǔn) ASCII 字符還是使用和它們在 ASCII 中一樣的單字節(jié)編碼胃榕,這也就意味著所有的 ASCII 字節(jié)序列用 ASCII 碼表示和用 UTF-8 表示是一樣的盛险。

Java 編程語言使用 Unicode 來表示字符串瞄摊。對于 C 語言也有支持 Unicode 的程序庫勋又。

2.1.5 表示代碼

考慮下面的 C 函數(shù):

int sum(int x, int y) {
    return x + y;
}

當(dāng)我們在示例機(jī)器上編譯時苦掘,生成如下字節(jié)表示的機(jī)器代碼:
\begin{matrix} Linux &32&55&89&e5&8b&45&0c&03&45&08&c9&c3 \\ Windows &55&89&e5&8b&45&0c&03&45&08&5d&c3 \\ Sun &81&c3 &e0 &08 &90 &02 &00 &09 \\ Linux &64 &55 &48 &89 &e5 &89 &7d &fc &89 &75 &f8 &03 &45 &fc &c9 &c3 \\ \end{matrix}

我們發(fā)現(xiàn)指令編碼是不同的。不同的機(jī)器類型使用不同的且不兼容的指令和編碼方式楔壤。即使是完全一樣的進(jìn)程鹤啡,運(yùn)行在不同的操作系統(tǒng)上也會有不同的編碼規(guī)則,因此二進(jìn)制代碼是不兼容的蹲嚣。二進(jìn)制代碼很少能在不同機(jī)器和操作系統(tǒng)組合之間移植递瑰。

計算機(jī)系統(tǒng)的一個基本概念就是,從機(jī)器的角度來看隙畜,程序僅僅只是字節(jié)序列抖部。機(jī)器沒有關(guān)于原始源程序的任何信息,除了可能有些用來幫助調(diào)試的輔助表以外议惰。在第3章學(xué)習(xí)機(jī)器級編程時慎颗,我們將更清楚地看到這一點。

2.1.6布爾代數(shù)簡介

二進(jìn)制值是計算機(jī)編碼言询、存儲和操作信息的核心俯萎,所以圍繞數(shù)值 0 和 1 的研究已經(jīng)演化出了豐富的數(shù)學(xué)知識體系。這起源于 1850 年前后喬治?布爾(George Boole运杭,1815-1864) 的工作夫啊,因此也稱為布爾代數(shù)(Booleanalgebra)。布爾注意到通過將邏輯值 TRUE(真)和 FALSE(假)編碼為二進(jìn)制值 1 和 0辆憔,能夠設(shè)計出一種代數(shù)撇眯,以研究邏輯推理的基本原則。

圖2-7 布爾代數(shù)的運(yùn)算虱咧。二進(jìn)制值 1 和 0 表示邏輯值 TRUE 或者 FALSE熊榛,而運(yùn)算符~、&彤钟、| 和^分別表示邏輯運(yùn)算 NOT来候、AND、OR 和 EXCLUSIVE-OR

最簡單的布爾代數(shù)是在二元集合 {0,1} 基礎(chǔ)上的定義逸雹。圖2-7 定義了這種布爾代數(shù)中的幾種運(yùn)算营搅。我們用來表示這些運(yùn)算的符號與 C 語言位級運(yùn)算使用的符號是相匹配的,這些將在后面討論到梆砸。布爾運(yùn)算 ~ 對應(yīng)于邏輯運(yùn)算 NOT转质,在命題邏輯中用符號 ﹁ 表示。也就是說帖世,當(dāng) P 不是真的時候休蟹,我們就說 ﹁P 是真的,反之亦然。相應(yīng)地赂弓,當(dāng) P 等于 0 時绑榴,~P 等于 1,反之亦然盈魁。布爾運(yùn)算 & 對應(yīng)于邏輯運(yùn)算 AND翔怎,在命題邏輯中用符號 ∧ 表示。當(dāng) P 和 Q 都為真時杨耙,我們說 P ∧ Q 為真赤套。相應(yīng)地,只有當(dāng) p=1 且 q=1 時珊膜,p&q 才等于 1容握。布爾運(yùn)算 | 對應(yīng)于邏輯運(yùn)算 OR,在命題邏輯中用符號 ∨ 表示车柠。當(dāng) P 或者 Q 為真時剔氏,我們說 P ∨ Q 成立。相應(yīng)地堪遂,當(dāng) p=1 或者 q=1 時介蛉,p|q 等于1。布爾運(yùn)算 ^ 對應(yīng)于邏輯運(yùn)算異或溶褪,在命題邏輯中用符號 十 表示币旧。當(dāng) P 或者 Q 為真但不同時為真時,我們說 P十Q 成立猿妈。相應(yīng)地吹菱,當(dāng) p=1 且 q=0,或者 p=0 且 q=1 時,p^q 等于1彭则。

后來創(chuàng)立信息論領(lǐng)域的 Claude Shannon(1916—2001)首先建立了布爾代數(shù)和數(shù)字邏輯之間的聯(lián)系鳍刷。他在 1937 年的碩士論文中表明了布爾代數(shù)可以用來設(shè)計和分析機(jī)電繼電器網(wǎng)絡(luò)。盡管那時計算機(jī)技術(shù)已經(jīng)取得了相當(dāng)?shù)陌l(fā)展俯抖,但是布爾代數(shù)仍然在數(shù)字系統(tǒng)的設(shè)計和分析中扮演著重要的角色输瓜。

我們可以將上述4個布爾運(yùn)算擴(kuò)展到位向量的運(yùn)算,位向量就是固定長度為 w芬萍、由 0 和 1 組成的串尤揣。位向量的運(yùn)算可以定義成參數(shù)的每個對應(yīng)元素之間的運(yùn)算。假設(shè) a 和 b 分別表示位向量 [a_{w-1},a_{w-2},\cdots,a_0][b_{w-1},b_{w-2},\cdots,b_0]柬祠。我們將 a \\& b 也定義為一個長度為 w 的位向量北戏,其中第i個元素等于a_i\\&b_i, 0 \leqslant i<w÷祝可以用類似的方式將運(yùn)算 |嗜愈、^ 和 ~ 擴(kuò)展到位向量上旧蛾。

舉個例子,假設(shè) w=4蠕嫁,參數(shù) a=[0110]锨天,b=[1100]。那么4種運(yùn)算 a&b拌阴、a|6绍绘、a^b 和 ~b 分別得到以下結(jié)果:

\begin{array}{cccc} \begin{array}{cc} & 0110 \\ \& & 1100 \\ \hline & 0100 \\ \end{array} & \begin{array}{cc} & 0110 \\ \mid & 1100 \\ \hline & 1110 \\ \end{array} & \begin{array}{cc} & 0110 \\ \hat\ & 1100 \\ \hline & 1010 \\ \end{array} & \begin{array}{cc} & 0110 \\ \backsim & 1100 \\ \hline & 1110 \\ \end{array} \end{array}

  • ? 練習(xí)題2.8 填寫下表奶镶,給出位向量的布爾運(yùn)算的求值的求值結(jié)果迟赃。
運(yùn)算 結(jié)果
a [01101001]
b [01010101]
~a ——
~b ——
a&b ——
a | b ——
a^b ——

網(wǎng)絡(luò)旁注 DATA:BOOL 關(guān)于布爾代數(shù)和布爾環(huán)的更多內(nèi)容

對于任意整數(shù) w>0,長度為 w 的位向量上的布爾運(yùn)算 |厂镇、& 和 ~ 形成了一個布爾代數(shù)纤壁。最簡單的情況是 w=l 時,只有 2 個元素捺信;但是對于更普遍的情況酌媒,有 2^W 個長度為 w 的位向量。布爾代數(shù)和整數(shù)算術(shù)運(yùn)算有很多相似之處迄靠。例如秒咨,乘法對加法的分配律,寫為 a?(b+c)=(a?b)+(a?c)掌挚,而布爾運(yùn)算 &對 | 的分配律雨席,寫為 a&(b|c)=(a&b|(a&c)。此外吠式,布爾運(yùn)算 | 對 & 也有分配律陡厘,寫為 a|(b&c)=(a|b)&(a|c),但是對于整數(shù)我們不能說 a+(b?c)=(a+b)?(a+c)特占。

當(dāng)考慮長度為 w 的位向量上的 ^糙置、& 和 ~ 運(yùn)算時,會得到一種不同的數(shù)學(xué)形式是目,我們稱為布爾環(huán)(Boolean ring)谤饭。布爾環(huán)與整數(shù)運(yùn)算有很多相同的屬性。例如懊纳,整數(shù)運(yùn)算的一個屬性是每個值 X 都有一個加法逆元(additive inverse)—x揉抵,使得 x+(—x)=0。布爾環(huán)也有類似的屬性长踊,這里的“加法”運(yùn)算是^功舀,不過這時每個元素的加法逆元是它自己本身。也就是說身弊,對于任何值 a 來說辟汰,a^a=0,這里我們用 0 來表示全 0 的位向量列敲。可以看到對單個位來說這是成立的帖汞,即 00=11=0戴而,將這個擴(kuò)展到位向量也是成立的。當(dāng)我們重新排列組合順序翩蘸,這個屬性也仍然成立所意,因此有(ab)a=b。這個屬性會引起一些很有趣的結(jié)果和聰明的技巧催首,在練習(xí)題2.10中我們會有所探討扶踊。

位向量一個很有用的應(yīng)用就是表示有限集合。我們可以用位向量[a_{w-1},\cdots,a_1,a_0]郎任。編碼任何子集 A \subseteq {0,1,\cdots,w-1}秧耗,其中a_i=1當(dāng)且僅當(dāng) i\in A。例如(記住我們是把 a_{w-1} 寫在左邊舶治,而將 a_0 寫在右邊)分井,位向量a=[01101001] 表示集合 A=\lbrace 0,3,5,6 \rbrace,而 b = [01010101]表示集合 B= \lbrace 0,2,4,6 \rbrace霉猛。使用這種編碼集合的方法尺锚,布爾運(yùn)算 | 和 & 分別對應(yīng)于集合的并和交,而~對應(yīng)于于集合的補(bǔ)惜浅。還是用前面那個例子瘫辩,運(yùn)算 a&b 得到位向量 [01000001],而 A\cap B={0,6}赡矢。

在大量實際應(yīng)用中杭朱,我們都能看到用位向量來對集合編碼。例如吹散,在第8章弧械,我們會看到有很多不同的信號會中斷程序執(zhí)行。我們能夠通過指定一個位向量掩碼空民,有選擇地使能或是屏蔽一些信號刃唐,其中某一位位置上為 1 時,表明信號 i 是有效的(使能)界轩,而 0 表明該信號是被屏蔽的画饥。因而,這個掩碼表示的就是設(shè)置為有效信號的集合浊猾。

  • ? 練習(xí)題2.9 通過混合三種不同顏色的光(紅色抖甘、綠色和藍(lán)色),計算機(jī)可以在視頻屏幕或者液晶顯示器上產(chǎn)生彩色的畫面葫慎。設(shè)想一種簡單的方法衔彻,使用三種不同顏色的光薇宠,每種光都能打開或關(guān)閉,投射到玻璃屏幕上艰额,如圖所示:

    image

    那么基于光源R(紅)澄港、G(綠)、B(藍(lán))的關(guān)閉(0)或打開(1)柄沮,我們就能夠創(chuàng)建8種不同的顏色:
    image

    這些顏色中的每一種都能用一個長度為3的位向量來表示回梧,我們可以對它們進(jìn)行布爾運(yùn)算。
    • A.一種顏色的補(bǔ)是通過關(guān)掉打開的光源祖搓,且打開關(guān)閉的光源而形成的狱意。那么上面列出的 8 種顏色每一種的補(bǔ)是什么?
    • B. 描述下列顏色應(yīng)用布爾運(yùn)算的結(jié)果:

      藍(lán)色 | 綠色 =

      黃色 & 藍(lán)綠色 =

      紅色 ^ 紅紫色 =

2.1.7 C 語言中的位級運(yùn)算

C語言的一個很有用的特性就是它支持按位布爾運(yùn)算棕硫。事實上髓涯,我們在布爾運(yùn)算中使
用的那些符號就是 C 語言所使用的:| 就是 OR(或),&就是 AND(與)哈扮,~ 就是 NOT(取反),而 ^ 就是 EXCLUSIVE-OR(異或)蚓再。這些運(yùn)算能運(yùn)用到任何“整型”的數(shù)據(jù)類型上滑肉,包括圖2-3 所示內(nèi)容。以下是一些對 char 數(shù)據(jù)類型表達(dá)式求值的例子:

C的表達(dá)式 二進(jìn)制表達(dá)式 二進(jìn)制結(jié)果 十六進(jìn)制結(jié)果
~0x41 ~[0100 0001] [1011 1110] OxBE
~0x00 ~[0000 0000] [1111 1111] OxFF
0x69&0x55 [0110 1001]&[0101 0101] [0100 0001] 0x41
0x69 | 0x55 [0110 1001] | [0101 0101] [0111 1101] 0x7D

正如示例說明的那樣摘仅,確定一個位級表達(dá)式的結(jié)果最好的方法靶庙,就是將十六進(jìn)制的參數(shù)擴(kuò)展成二進(jìn)制表示并執(zhí)行二進(jìn)制運(yùn)算,然后再轉(zhuǎn)換回十六進(jìn)制娃属。

  • ? 練習(xí)題2.10 對于任一位向量a六荒,有 a^a=0。應(yīng)用這一屬性矾端,考慮下面的程序:

    void inplace_swap(int *x, int *y) {
        *y = *x ^ *y;  /* Step 1 */
        *x = *x ^ *y;  /* Step 2 */
        *y = *x ^ *y;  /* Step 3 */
    }
    

    正如程序名字所暗示的那樣掏击,我們認(rèn)為這個過程的效果是交換指針變量x和y所指向的存儲位置處存放的值。注意秩铆,與通常的交換兩個數(shù)值的技術(shù)不一樣砚亭,當(dāng)移動一個值時,我們不需要第三個位置來臨時存儲另一個值殴玛。這種交換方式并沒有性能上的優(yōu)勢捅膘,它僅僅是一個智力游戲。

    以指針 x 和 y 指向的位置存儲的值分別是 a 和 b 作為開始滚粟,填寫下表寻仗,給出在程序的每一步之后,存儲在這兩個位置中的值凡壤。利用 ^ 的屬性證明達(dá)到了所希望的效果署尤∈咭В回想一下,每個元素就是它自身的加法逆元(a^a=0)沐寺。

步驟 *x *y
初始 a b
第1步
第2步
第3步
  • ? 練習(xí)題2.11 在練習(xí)題2.10中的inplace_swap函數(shù)的基礎(chǔ)上林艘,你決定寫一段代碼,實現(xiàn)將一個數(shù)組中的元素頭尾兩端依次對調(diào)混坞。你寫出下面這個函數(shù):

      void reverse_array(int a[], int cnt) {
      int first, last;
      for (first = 0, last = cnt-1;
          first <= last;
          first++,last--)
          inplace_swap(&a[first] , &a[last]);
      }
    

    當(dāng)你對一個包含元素 1狐援、2、3和 4 的數(shù)組使用這個函數(shù)時究孕,正如預(yù)期的那樣啥酱,現(xiàn)在數(shù)組的元素變成了 4、3厨诸、2 和 1镶殷。不過,當(dāng)你對一個包含元素 1微酬、2绘趋、3、4 和 5 的數(shù)組使用這個函數(shù)時颗管,你會很驚奇地看到得到數(shù)字的元素為 5陷遮、4、0垦江、2 和 1帽馋。實際上,你會發(fā)現(xiàn)這段代碼對所有偶數(shù)長度的數(shù)組都能正確地工作比吭,但是當(dāng)數(shù)組的長度為奇數(shù)時绽族,它就會把中間的元素設(shè)置成0。

    • A. 對于一個長度為奇數(shù)的數(shù)組衩藤,長度 cnt=2k+l吧慢,函數(shù)reverse_array 最后一次循環(huán)中,變量first和last的值分別是什么慷彤?
    • B. 為什么這時調(diào)用函數(shù) inplace_swap 會將數(shù)組元素設(shè)置為0?
    • C. 對 reverse_array 的代碼做哪些簡單改動就能消除這個問題娄蔼?

位級運(yùn)算的一個常見用法就是實現(xiàn)掩碼運(yùn)算,這里掩碼是一個位模式底哗,表示從一個字中選出的位的集合岁诉。讓我們來看一個例子,掩碼 0xFF(最低的8位為1)表示一個字的低位字節(jié)跋选。位級運(yùn)算 x&0xFF 生成一個由 x 的最低有效字節(jié)組成的值涕癣,而其他的字節(jié)就被置為 0。比如,對于 x=0x89ABCDEF坠韩,其表達(dá)式將得到 0x000000EF距潘。表達(dá)式 ~0 將生成一個全 1 的掩碼,不管機(jī)器的字大小是多少只搁。盡管對于一個32位機(jī)器來說音比,同樣的掩碼可以寫成 0xFFFFFFFF,但是這樣的代碼不是可移植的。

  • ? 練習(xí)題2.12 對于下面的值氢惋,寫出變量 x 的 C 語言表達(dá)式洞翩。你的代碼應(yīng)該對任何字長 w \geqslant 8 都能工作。我們給出了當(dāng) x=0x87654321 以及 w=32 時表達(dá)式求值的結(jié)果焰望,僅供參考骚亿。

    • A. x 的最低有效字節(jié),其他位均置為0熊赖。[0x00000021]来屠。
    • B. 除了 x 的最低有效字節(jié)外,其他的位都取補(bǔ)震鹉,最低有效字節(jié)保持不變俱笛。[0x789ABC21]。
    • C. x 的最低有效字節(jié)設(shè)置成全 1足陨,其他字節(jié)都保持不變嫂粟。[0X876543FF]。
  • ? 練習(xí)題2.13 從20世紀(jì)70年代末到80年代末墨缘,Digital Equipment 的 VAX 計算機(jī)是一種非常流行的機(jī)型。它沒有布爾運(yùn)算 AND 和 OR 指令零抬,只有 bis(位設(shè)置)和 bic(位清除)這兩種指令镊讼。兩種指令的輸入都是一個數(shù)據(jù)字 x 和一個掩碼字 m。它們生成一個結(jié)果 z平夜,z 是由根據(jù)掩碼 m 的位來修改 x 的位得到的蝶棋。使用bis指令,這種修改就是在 m 為 1 的每個位置上忽妒,將 z 對應(yīng)的位設(shè)置為 1玩裙。使用 bic 指令,這種修改就是在 m 為 1 的每個位置段直,將 z 對應(yīng)的位設(shè)置為 0吃溅。

    為了看清楚這些運(yùn)算與 C 語言位級運(yùn)算的關(guān)系,假設(shè)我們有兩個函數(shù) bis 和 bic 來實現(xiàn)位設(shè)置和位清除操作鸯檬。只想用這兩個函數(shù)决侈,而不使用任何其他 C 語言運(yùn)算,來實現(xiàn)按位 | 和 ^ 運(yùn)算喧务。填寫下列代碼中缺失的代碼赖歌。提示:寫出 bis 和 bic 運(yùn)算的 C 語言表達(dá)式枉圃。

/* Declarations of functions implementing operations bis and bic */
int bis(int x, int m);
int bic(int x, int m);

/* Compute x|y using only calls to functions bis and bic */
int bool_or(int x, int y) {
    int result = __________;
    return result;
}
/* Compute x^y using only calls to functions bis and bic */
int bool_xor(int x, int y) {
    int result = __________;
    return result;
}

2.1.8 C 語言中的邏輯運(yùn)算

C 語言還提供了一組邏輯運(yùn)算符 ||、&& 和 庐冯!孽亲,分別對應(yīng)于命題邏輯中的 OR、AND 和 NOT 運(yùn)算展父。邏輯運(yùn)算很容易和位級運(yùn)算相混淆返劲,但是它們的功能是完全不同的。邏輯運(yùn)算認(rèn)為所有非零的參數(shù)都表示 TRUE犯祠,而參數(shù) 0 表示 FALSE旭等。它們返回 1 或者 0,分別表示結(jié)果為 TRUE 或者為 FALSE衡载。以下是一些表達(dá)式求值的示例搔耕。

表達(dá)式 結(jié)果
!0x41 0x00
!0x00 0x01
!!0x41 0x01
0x68&&0x55 0x01
0x69 || 0x55 0x01

可以觀察到,按位運(yùn)算只有在特殊情況下痰娱,也就是參數(shù)被限制為 0 或者 1 時弃榨,才和與其對應(yīng)的邏輯運(yùn)算有相同的行為。

邏輯運(yùn)算符 && 和 || 與它們對應(yīng)的位級運(yùn)算 & 和 | 之間第二個重要的區(qū)別是梨睁,如果對第一個參數(shù)求值就能確定表達(dá)式的結(jié)果鲸睛,那么邏輯運(yùn)算符就不會對第二個參數(shù)求值。因此坡贺,例如官辈,表達(dá)式 a&&5/a 將不會造成被零除,而表達(dá)式 p&&*p++ 也不會導(dǎo)致間接引用空指針遍坟。

  • ? 練習(xí)題2.14 假設(shè) x 和 y 的字節(jié)值分別為 0x66 和 0x39拳亿。填寫下表,指明各個 C 表達(dá)式的字節(jié)值愿伴。
表達(dá)式 表達(dá)式
x & y x && y
x | y x || y
~x | ~y !x || !y
x & !y x && ~y
  • ? 練習(xí)題2.15 只使用位級和邏輯運(yùn)算肺魁,編寫一個 C 表達(dá)式,它等價于 x==y隔节。換句話說鹅经,當(dāng) x 和 y 相等時它將返回1,否則就返回0怎诫。

2.1.9 C 語言中的移位運(yùn)算

C 語言還提供了一組移位運(yùn)算瘾晃,向左或者向右移動位模式。對于一個位表示為 [x_{w-1},x_{w-2},\cdots,x_0] 的操作數(shù)x刽虹,C 表達(dá)式 x<<k 會生成一個值酗捌,其位表示為 [x_{w-k-1},x_{w-k-2},\cdots,x_0,0,\cdots,0]。也就是說,x向左移動 k 位胖缤,丟棄最高的 k 位尚镰,并在右端補(bǔ) k 個 0。移位量應(yīng)該是一個 0~w—1 之間的值哪廓。移位運(yùn)算是從左至右可結(jié)合的狗唉,所以 x<<j<<k 等價于 (x<<j)<<k

有一個相應(yīng)的右移運(yùn)算 x>>k涡真,但是它的行為有點微妙分俯。一般而言,機(jī)器支持兩種形
式的右移:邏輯右移算術(shù)右移哆料。邏輯右移在左端補(bǔ) k 個 0缸剪,得到的結(jié)果是[0,\cdots,0,x_{w-1},x_{w-2},\cdots,x_k]。算術(shù)右移是在左端補(bǔ) k 個最高有效位的值东亦,得到的結(jié)果是 [x_{w-1},\cdots,x_{w-1},x_{w-1},x_{w-2},\cdots,x_k]杏节。這種做法看上去可能有點奇特,但是我們會發(fā)現(xiàn)它對有符號整數(shù)數(shù)據(jù)的運(yùn)算非常有用典阵。

讓我們來看一個例子奋渔,下面的表給出了對一個 8 位參數(shù) x 的兩個不同的值做不同的移位操作得到的結(jié)果:

操作
參數(shù)x [01100011] [10010101]
x << 4 [00110000] [01010000]
x >> 4 (邏輯右移) [00000110] [00001001]
x >> 4 (算術(shù)右移) [00000110] [11111001]

斜體的數(shù)字表示的是最右端(左移)或最左端(右移)填充的值载萌⊥芗ィ可以看到除了一個條目之外,其他的都包含填充 0棵磷。唯一的例外是算術(shù)右移[10010101]的情況歹啼。因為操作數(shù)的最高位是1玄渗,填充的值就是1。

C 語言標(biāo)準(zhǔn)并沒有明確定義對于有符號數(shù)應(yīng)該使用哪種類型的右移——算術(shù)右移或者邏輯
右移都可以狸眼。不幸地捻爷,這就意味著任何假設(shè)一種或者另一種右移形式的代碼都可能會遇到可移植性問題。然而份企,實際上,幾乎所有的編譯器/機(jī)器組合都對有符號數(shù)使用算術(shù)右移巡莹,且許多程序員也都假設(shè)機(jī)器會使用這種右移司志。另一方面,對于無符號數(shù)降宅,右移必須是邏輯的骂远。

與 C 相比,Java 對于如何進(jìn)行右移有明確的定義腰根。表達(dá)是 x>>k 會將 x 算術(shù)右移 k 個位置激才,而 x>>>k 會對x做邏輯右移。

旁注 移動K位,這里 K 很大

對于一個由 w 位組成的數(shù)據(jù)類型瘸恼,如果要移動 k \geqslant w 會得到什么結(jié)果呢劣挫?例如,計算下面的表達(dá)式會得到什么結(jié)果东帅,假設(shè)數(shù)據(jù)類型 int為 w=32
\begin{matrix} int & lval = 0xFEDCBA98 << 32; \\ int & aval = 0xFEDCBA98 >> 36; \\ unsigned & uval = 0xFEDCBA98u >> 40; \\ \end{matrix}

C 語言標(biāo)準(zhǔn)很小心地規(guī)避了說明在這種情況下該如何做压固。在許多機(jī)器上,當(dāng)移動一個 w 位的值時靠闭,移位指令只考慮位移量的低 \log_2 w 位帐我,因此實際上位移量就是通過計算 k \, mod \, w 得到的。例如愧膀,當(dāng)w=32時拦键,上面三個移位運(yùn)算分別是移動0、4和8位檩淋,得到結(jié)果:
\begin{matrix} lval & 0xFEDCBA98 \\ aval & 0xFFEDCBA9 \\ uval & OxOOFEDCBA \\ \end{matrix}

不過這種行為對于C程序來說是沒有保證的芬为,所以應(yīng)該保持位移量小于待移位值的位數(shù)。
另一方面狼钮,Java特別要求位移數(shù)量應(yīng)該按照我們前面所講的求模的方法來計算碳柱。

旁注 與移位運(yùn)算有關(guān)的操作符優(yōu)先級問題

常常有人會寫這樣的表達(dá)式 1<<2+3<<4,本意是 (1<<2)+(3<<4)熬芜。但是在 C 語言中莲镣,前面的表達(dá)式等價于 1<<(2+3)<<4,這是由于加法(和減法)的優(yōu)先級比移位運(yùn)算要高涎拉。然后瑞侮,按照從左至右結(jié)合性規(guī)則,括號應(yīng)該是這樣打的 (1<<(2+3))<<4鼓拧,得到的結(jié)果是 512 ,而不是期望的 52半火。

在 C 表達(dá)式中搞錯優(yōu)先級是一種常見的程序錯誤原因,而且常常很難檢查出來季俩。所以當(dāng)你拿不準(zhǔn)的時候钮糖,請加上括號!

  • ? 練習(xí)題2.16 填寫下表酌住,展示不同移位運(yùn)算對單字節(jié)數(shù)的影響店归。思考移位運(yùn)算的最好方式是使用二進(jìn)制表示。將最初的值轉(zhuǎn)換為二進(jìn)制酪我,執(zhí)行移位運(yùn)算消痛,然后再轉(zhuǎn)換回十六進(jìn)制。每個答案都應(yīng)該是8個二進(jìn)制數(shù)字或者2個十六進(jìn)制數(shù)字都哭。
image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末秩伞,一起剝皮案震驚了整個濱河市逞带,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纱新,老刑警劉巖展氓,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怒炸,居然都是意外死亡带饱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門阅羹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來勺疼,“玉大人,你說我怎么就攤上這事捏鱼≈绰” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵导梆,是天一觀的道長轨淌。 經(jīng)常有香客問我,道長看尼,這世上最難降的妖魔是什么递鹉? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮藏斩,結(jié)果婚禮上躏结,老公的妹妹穿的比我還像新娘。我一直安慰自己狰域,他們只是感情好媳拴,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著兆览,像睡著了一般屈溉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上抬探,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天子巾,我揣著相機(jī)與錄音,去河邊找鬼小压。 笑死砰左,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的场航。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼廉羔,長吁一口氣:“原來是場噩夢啊……” “哼溉痢!你這毒婦竟也來了僻造?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤孩饼,失蹤者是張志新(化名)和其女友劉穎髓削,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镀娶,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡立膛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了梯码。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宝泵。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖轩娶,靈堂內(nèi)的尸體忽然破棺而出儿奶,到底是詐尸還是另有隱情,我是刑警寧澤鳄抒,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布闯捎,位于F島的核電站,受9級特大地震影響许溅,放射性物質(zhì)發(fā)生泄漏瓤鼻。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一贤重、第九天 我趴在偏房一處隱蔽的房頂上張望茬祷。 院中可真熱鬧,春花似錦游桩、人聲如沸牲迫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盹憎。三九已至,卻和暖如春铐刘,著一層夾襖步出監(jiān)牢的瞬間陪每,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工镰吵, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留檩禾,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓疤祭,卻偏偏與公主長得像盼产,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子勺馆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 夜鶯2517閱讀 127,712評論 1 9
  • 版本:ios 1.2.1 亮點: 1.app角標(biāo)可以實時更新天氣溫度或選擇空氣質(zhì)量戏售,建議處女座就不要選了侨核,不然老想...
    我就是沉沉閱讀 6,878評論 1 6
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭灌灾,有人歡樂有人憂愁搓译,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,523評論 28 53
  • 兔子雖然是枚小碩 但學(xué)校的碩士四人寢不夠 就被分到了博士樓里 兩人一間 在學(xué)校的最西邊 靠山 兔子的室友身體不好 ...
    待業(yè)的兔子閱讀 2,586評論 2 9