第一部分
我們對計算機(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
編譯選項 -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)制表示法中,它的值域是 奕污。如果看成十進(jìn)制整數(shù)萎羔,它的值域就是液走。兩種符號表示法對于描述位模式來說都不是非常方便碳默。二進(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é)的值域為冈爹。
在C語言中,以 0x 或 0X 開頭的數(shù)字常量被認(rèn)為是十六進(jìn)制的值欧引。字符 ‘A’?‘F’ 既可以是大寫频伤,也可以是小寫。例如芝此,我們可以將數(shù)字寫作 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 次冪時闰非,也就是 我們可以很容易地將 χ 寫成十六進(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。比如唾那,访锻,我們有 n=11=3 + 4 * 2,從而得到十六進(jìn)制表示 0x800闹获。
- ?
練習(xí)題2.2
填寫下表中的空白項期犬,給出2的不同次冪的二進(jìn)制和十六進(jìn)制表示:
n | (十進(jì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)換:
從這里,我們能讀出十六進(jìn)制表示為 0x4CB2C铸抑。
反過來贡耽,將一個十六進(jìn)制數(shù)字轉(zhuǎn)換為十進(jìn)制數(shù)字,我們可以用相應(yīng)的16的冪乘以每個十六進(jìn)制數(shù)字。比如蒲赂,給定數(shù)字 0x7AF,我們計算它對應(yīng)的十進(jìn)制值為
- ?
練習(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ī)器而言夸溶,虛擬地址的范圍為 ,程序最多訪問 個字節(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),也就是說韩脑,剛剛超過 字節(jié)。擴(kuò)展到 64 位字長使得虛擬地址空間為 16EB粹污,大約是 字節(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ù)蜒程。
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ù)類型 ,聲明
;
表明 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ù)捧颅,其位表示為 ,其中 是最高有效位较雕,而 最低有效位碉哑。假設(shè) 是 8 的倍數(shù),這些位就能被分組成為字節(jié)亮蒋,其中最高有效字節(jié)包含位 扣典,而最低有效字節(jié)包含位 ,其他字節(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ī)器的類型:
注意蚤吹,在字 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é)表示
參數(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)識出來:
這并不是巧合帕棉。當(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ī)器代碼:
我們發(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ù)撇眯,以研究邏輯推理的基本原則。
最簡單的布爾代數(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 分別表示位向量 和 柬祠。我們將 也定義為一個長度為 w 的位向量北戏,其中第個元素等于÷祝可以用類似的方式將運(yùn)算 |嗜愈、^ 和 ~ 擴(kuò)展到位向量上旧蛾。
舉個例子,假設(shè) w=4蠕嫁,參數(shù) a=[0110]锨天,b=[1100]。那么4種運(yùn)算 a&b拌阴、a|6绍绘、a^b 和 ~b 分別得到以下結(jié)果:
- ?
練習(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 個元素捺信;但是對于更普遍的情況酌媒,有 個長度為 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)算的一個屬性是每個值 都有一個加法逆元(additive inverse)—揉抵,使得 。布爾環(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)用就是表示有限集合。我們可以用位向量郎任。編碼任何子集 秧耗,其中當(dāng)且僅當(dāng) 。例如(記住我們是把 寫在左邊舶治,而將 寫在右邊)分井,位向量 表示集合 ,而 b = [01010101]表示集合 霉猛。使用這種編碼集合的方法尺锚,布爾運(yùn)算 | 和 & 分別對應(yīng)于集合的并和交,而~對應(yīng)于于集合的補(bǔ)惜浅。還是用前面那個例子瘫辩,運(yùn)算 a&b 得到位向量 ,而 赡矢。
在大量實際應(yīng)用中杭朱,我們都能看到用位向量來對集合編碼。例如吹散,在第8章弧械,我們會看到有很多不同的信號
會中斷程序執(zhí)行。我們能夠通過指定一個位向量掩碼空民,有選擇地使能或是屏蔽一些信號刃唐,其中某一位位置上為 1 時,表明信號 是有效的(使能)界轩,而 0 表明該信號是被屏蔽的画饥。因而,這個掩碼表示的就是設(shè)置為有效信號的集合浊猾。
- ?
練習(xí)題2.9
通過混合三種不同顏色的光(紅色抖甘、綠色和藍(lán)色),計算機(jī)可以在視頻屏幕或者液晶顯示器上產(chǎn)生彩色的畫面葫慎。設(shè)想一種簡單的方法衔彻,使用三種不同顏色的光薇宠,每種光都能打開或關(guān)閉,投射到玻璃屏幕上艰额,如圖所示:
那么基于光源R(紅)澄港、G(綠)、B(藍(lán))的關(guān)閉(0)或打開(1)柄沮,我們就能夠創(chuàng)建8種不同的顏色:
這些顏色中的每一種都能用一個長度為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 的代碼做哪些簡單改動就能消除這個問題娄蔼?
- A. 對于一個長度為奇數(shù)的數(shù)組衩藤,長度 cnt=2k+l吧慢,函數(shù)reverse_array 最后一次循環(huán)中,變量first和last的值分別是什么慷彤?
位級運(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)該對任何字長 都能工作。我們給出了當(dāng) x=0x87654321 以及 時表達(dá)式求值的結(jié)果焰望,僅供參考骚亿。- A. x 的最低有效字節(jié),其他位均置為0熊赖。[0x00000021]来屠。
- B. 除了 x 的最低有效字節(jié)外,其他的位都取補(bǔ)震鹉,最低有效字節(jié)保持不變俱笛。[0x789ABC21]。
- C. x 的最低有效字節(jié)設(shè)置成全 1足陨,其他字節(jié)都保持不變嫂粟。[0X876543FF]。
- A. x 的最低有效字節(jié),其他位均置為0熊赖。[0x00000021]来屠。
?
練習(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)算瘾晃,向左或者向右移動位模式。對于一個位表示為 的操作數(shù)刽虹,C 表達(dá)式 會生成一個值酗捌,其位表示為 。也就是說,x向左移動 位胖缤,丟棄最高的 位尚镰,并在右端補(bǔ) 個 0。移位量應(yīng)該是一個 0~w—1 之間的值哪廓。移位運(yùn)算是從左至右可結(jié)合的狗唉,所以 等價于 。
有一個相應(yīng)的右移運(yùn)算 涡真,但是它的行為有點微妙分俯。一般而言,機(jī)器支持兩種形
式的右移:邏輯右移
和算術(shù)右移
哆料。邏輯右移在左端補(bǔ) 個 0缸剪,得到的結(jié)果是。算術(shù)右移是在左端補(bǔ) 個最高有效位的值东亦,得到的結(jié)果是 杏节。這種做法看上去可能有點奇特,但是我們會發(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 算術(shù)右移 k 個位置激才,而 會對x做邏輯右移。
旁注 移動K位,這里 K 很大
對于一個由 位組成的數(shù)據(jù)類型瘸恼,如果要移動 會得到什么結(jié)果呢劣挫?例如,計算下面的表達(dá)式會得到什么結(jié)果东帅,假設(shè)數(shù)據(jù)類型 int為 :
C 語言標(biāo)準(zhǔn)很小心地規(guī)避了說明在這種情況下該如何做压固。在許多機(jī)器上,當(dāng)移動一個 位的值時靠闭,移位指令只考慮位移量的低 位帐我,因此實際上位移量就是通過計算 得到的。例如愧膀,當(dāng)時拦键,上面三個移位運(yùn)算分別是移動0、4和8位檩淋,得到結(jié)果:
不過這種行為對于C程序來說是沒有保證的芬为,所以應(yīng)該保持位移量小于待移位值的位數(shù)。
另一方面狼钮,Java特別要求位移數(shù)量應(yīng)該按照我們前面所講的求模的方法來計算碳柱。
旁注 與移位運(yùn)算有關(guān)的操作符優(yōu)先級問題
常常有人會寫這樣的表達(dá)式 ,本意是 熬芜。但是在 C 語言中莲镣,前面的表達(dá)式等價于 ,這是由于加法(和減法)的優(yōu)先級比移位運(yùn)算要高涎拉。然后瑞侮,按照從左至右結(jié)合性規(guī)則,括號應(yīng)該是這樣打的 鼓拧,得到的結(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ù)字都哭。