深入理解計算機系統_第二章_信息的表示和處理

深入,並且廣泛
				-沉默犀牛

文章導讀

這一章介紹了計算機中信息(即二值信號)的表示和處理。

  1. 信息存儲
    1.1 十六進制表示法:介紹了十六進制的產生和十六進制、十進制、二進制之間的相互轉換規律
    1.2 字數據大小:介紹了在不同機器和編譯器中,數據類型數據在內存中所佔的大小
    1.3 尋址和字節順序 : 介紹了尋址方法和字節順序(小端法、大端法),float與int的二進制表示有聯繫
    1.4 表示字符串:介紹了字符串的編碼方式,以ASCII爲例,說明了文本數據比二進制數據移植性好
    1.5 表示代碼:介紹了在不同機器上產生的機器指令是不同的
    1.6 布爾代數簡介:介紹了布爾代數及四種運算(~、&、|、^)
    1.7 C語言中的位級運算:介紹了布爾運算在C語言中的運用
    1.8 C語言中的邏輯運算:介紹了C語言中的邏輯運算及與位級運算的不同
    1.9 C語言中的移位運算:介紹了左移和右移(邏輯右移和算術右移)
  2. 整數表示
    2.1 整數數據類型:介紹了常見的整數數據類型
    2.2 無符號數的編碼
    2.3 補碼編碼
    2.4 有符號數和無符號數之間的轉換:同樣的位模式應用不同的解釋方法
    2.5 C語言中的有符號數與無符號數:着重提及了有符號數與無符號數混雜時的潛在危險
    2.6 擴展一個數字的位表示:介紹了無符號數的零擴展和補碼數的符號擴展
    2.7 截斷數字:介紹了無符號數的截斷和補碼數的截斷
  3. 整數運算
    3.1 無符號加法:介紹了無符號數加法和溢出
    3.2 補碼加法:介紹了補碼數的加法和溢出
    3.3 補碼的非 : 非是指加法逆元
    3.4 無符號乘法
    3.5 補碼乘法:簡單方法是轉換爲10禁止計算後轉換爲二進制後截斷
    3.6 乘以常數:把常數轉爲 2的任意次冪的相互加減
    3.7 除以2的冪:分爲無符號和有符號(補碼)除法
    3.8 關於整數運算的最後思考:思考有限字長對於結果的影響
  4. 浮點數
    4.1 二進制小數
    4.2 IEEE浮點表示:分爲規格化、非規格化和特殊值
    4.3 數字示例
    4.4 舍入:分爲向偶數舍入、向零舍入、向下舍入、向上舍入
    4.5 浮點運算:加法可交換不可結合,乘法不具有結合性,不具有加法分配性

信息的表示和處理

現在計算機存儲和處理的信息以二值信號表示。對於有10個手指的人類來說,使用十進制是很自然的事情,但是當構造存儲和處理信息的機器是,二進制工作得更好。二值信號能夠很容易得被表示、存儲和傳輸,例如,可以表示爲穿孔卡片上有洞或無洞、導線上的高電壓或低電壓,或者順時針或者逆時針的磁場。對二值信號進行存儲和執行計算的電子電路非常簡單和可靠,製造商能夠在一個單獨的硅片上集成數百萬甚至數十億個這樣的電路。

[說不定這個宇宙中有一個星球,在那上面的生物長了3個手指,那他們一定就是3進制的吧哈哈哈哈哈]
[想起來一句名言,世界上有十種人,一種懂二進制,一種不懂二進制,哈哈哈哈哈哈哈哈 -2019/5/6]

孤立地講,單個的位不是非常有用。然而,當把位組合在一起,再加上某種解釋,即賦予不同的可能位模式以含義,我們就能夠表示任何有限集合的元素。比如,使用一個二進制數字系統,我們能夠用位組來編碼非負數。通過使用標準的字符碼,我們能夠對文檔中的字母和符號進行編碼。

我們研究三種最重要的數字表示。無符號(unsigned)編碼基於傳統的二進制表示法,表示大於或者等於零的數字。補碼(two‘s-complement)編碼是表示有符號整數的最常見的方式,有符號整數就是可以爲正或者爲負的數字。浮點數(floating-point)編碼是表示實數的科學計數法的以2爲基數的版本。計算機用這些不同的表示方法實現算術運算,例如加法和乘法,類似於對應的整數和實數運算。

計算機的表示法是用有限數量的位來對一個數字編碼,因此,當結果太大以至於不能表示時,某些運算就會溢出(overflow)。溢出會導致某些令人喫驚的後果。例如,在今天的大多數計算機上(使用32位來表示數據類型int),計算表達式200300400*500會得出-884901888。這違背了整數運算的特性,計算一組正數的乘積不應產生一個負的結果。

另一方面,整數的計算機運算滿足人們所熟知的真正整數運算的許多特性。例如,利用乘法的結合律和交換律。雖然計算機可能沒有產生期望的結果(人類期望),但是至少它是一致的!

浮點運算有完全不同的數學屬性。雖然溢出會產生特殊的值正無窮,但是一組正數的乘積總是正的。由於表示的精度有限,浮點運算是不可結合的。例如,在大多數機器上,C表達式(3.14 + 1e20)- 1e20求得的值會是0.0,而3.14 +(1e20 - 1e20)求得的值會是3.14。整數運算和浮點運算會有不同的數學屬性是因爲它們處理數字表示有限的方式不同——整數的表示雖然只能編碼一個相對較小的數字範圍,但是這種表示是精確的;而浮點數雖然可以編碼較大的數值範圍,但是這種表示只是近似的。

通過研究數字的實際表示,我們能夠了解可以表示的值的範圍和不同的算術運算屬性。大量的計算機的安全漏洞都是由於計算機算術運算的微妙細節引發的。這導致了衆多的黑客企圖利用他們能找到的任何漏洞,不經過授權就進入他人的系統。

[學習微機的時候就覺得數字的存儲和運算很麻煩,終於還是逃不脫啊。。。]
[今天再看也還是覺得有點煩啊。。。耐住性子啊。。。 -2019/5/6]

信息存儲

大多數計算機使用8位的塊,或者字節(byte),作爲最小的可尋址的內存單位,而不是訪問內存中單獨的位[把8個位想成一個班級,校領導能找的最小單位是班級]。機器級程序將內存視爲一個非常大的字節數組,稱爲虛擬內存。內存的每個字節都由一個唯一的數字來標識,稱爲它的地址,所有可能地址的集合就稱爲虛擬地址空間。顧名思義,這個虛擬地址空間只是一個展現給機器級程序的概念性映像。實際的實現是將動態隨機訪問存儲器、閃存、磁盤存儲器、特殊硬件和操作系統軟件結合起來,爲程序提供一個看上去統一的字節數組。

在接下來的幾章中,我們將講述編譯器和運行時系統是如何將存儲空間劃分爲更可管理的單元,來存放不同的程序對象(program object),即程序數據、指令和控制信息。可以用各種機制來分配和管理程序不同部分的存儲。這種管理完全是在虛擬地址空間裏完成的。例如,C語言中一個指針的值(無論它指向一個整數、一個結構或是某個其他程序對象)都是某個存儲塊的第一個字節的虛擬地址。C編譯器還把每個指針和類型信息聯繫起來,這樣就可以根據指針值的類型,生成不同的機器級代碼來訪問存儲在指針所指向位置處的值。儘管C編譯器維護着這個類型信息,但是它生成的實際機器級程序並不包含關於數據類型的信息。每個程序對象可以簡單地視爲一個字節塊,而程序本身就是一個字節序列。
[這樣就更好的理解函數指針了,反正函數也只不過是一個字節序列,那大可有一個指針指向它嘛 -2019/5/6]

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

十六進制表示法

一個字節由8位組成。在二進制表示法中,它的值域是00000000 ~ 11111111。如果看成十進制數,它的值域就是0 ~ 255。兩種符號表示法對於描述位模式來說都不是非常方便。二進制表示法太冗長,而十進制表示法與位模式的互相轉化很麻煩。替代的方法是,以16爲基數,或者叫做十六進制(hexadecimal)數,來表示位模式。十六進制(簡寫hex)使用數字“0” ~ “9” 以及字符“A”~“F”來表示16個可能的值[馬路上的紅燈超時間超過100秒後就會用A ~ F來表示]。用十六進制書寫,一個字節的值域爲00~FF。

在C語言中,以0x或0X開頭的數字常量被認爲是十六進制的值。字符A~F既可以是大寫,也可以是小寫,也可以是大小寫混合。

記住十六進制與十進制轉化的竅門就是,記住十六進制A、C、E相應的十進制數,這樣對於B、D、F的十進制數只要加1即可。

比如,給你一個數字0x173A4C。可以通過展開每個十六進制數字,將它轉換爲二進制格式,如下圖所示:
在這裏插入圖片描述
反過來,如果給你一個二進制數,可以通過首先把它分爲每4位一組來轉化爲十六進制。不過要注意,如果位總數不是4的倍數,最左邊的一組要前面補0,然後再將每個4位組轉換爲相應的十六進制數字:
在這裏插入圖片描述
[這個內容是比較簡單的,進制轉換是程序員的基本功吧,不過才知道原來十六進制的提出是爲了簡便的。]

十進制和十六進制表示之間的轉換需要使用乘法或者除法來處理一般情況,例如,十進制爲314156,則如下圖:在這裏插入圖片描述
所以十六進制表示爲 0x4CB2C。
如果十六進制爲0x7AF,則十進制數爲7×16的平方 + 10×16 + 15 = 1967

字數據大小

每臺計算機都有一個字長(word size),指明指針數據的標稱大小(nominal size)。因爲虛擬地址是以這樣的一個字來編碼的,所以字長決定的最重要的系統參數就是虛擬地址的最大大小。也就是說,對於一個字長爲w位的機器而言,虛擬地址的範圍爲0~(2的w地方 )- 1,程序最多訪問(2的w次方)個字節。

[看來我們的系統分爲32位64位的差別就在於此,所以同樣的機器既可以裝32位的系統,也可以裝64位的系統,因爲這個位數決定了虛擬地址的大小,跟實際你的機器的物理內存大小沒關係。但是大多數軟件也分了32位版本64位版本,而32位64位的虛擬地址大小不同,所以如果裝錯了會出現藍屏等現象,我想一定是64位的軟件裝在了32位的系統上,請求了超過4GB(2的32次方)-1)這個大小的地址,就藍屏了。]

最近這些年,出現了大規模的從32位字長機器到64位字長機器的遷移。32位字長限制虛擬地址空間爲4GB,擴展到64位字長使得虛擬地址空間爲16EB。

大多數64位機器也可以運行(爲32位機器編譯)的程序,這是一種後向兼容。我們將程序稱爲“32位程序”或者“64位程序”時,區別在於該程序是如何編譯的,而不是其運行的機器類型。

計算機和編譯器支持多種不同方式編碼的數字格式,如不同長度的整數和浮點數。比如,許多機器都有處理單個字節的指令,也有處理表示爲2字節、4字節或者8字節整數的指令,還有些指令支持表示爲4字節和8字節的浮點數。

C語言支持整數和浮點數的多種數據格式。有些數據類型的確切字節數依賴於程序是如何被編譯。
在這裏插入圖片描述
爲了避免由於依賴“典型”大小和不同編譯器設置帶來的奇怪行爲,ISO C99引入了一類數據類型,其數據大小是固定的,不隨編譯器和機器設置而變化。其中就有數據類型int32_tint64_t,它們分別是4個字節和8個字節。使用確定大小的整數類型是程序員準確控制數據表示的最佳途徑。

[怪不得我在很多程序中看到int32_tint64_t這兩種定義方法,原來是爲了固定數據大小。]

程序員應該力圖使它們的程序在不同的機器和編譯器上可移植。可移植性的一個方面就是使程序對不同數據類型確切大小不敏感。如果不關注這個問題,就有可能出現錯誤,比如,許多程序員假設一個聲明爲int類型的程序對象能被用來存儲一個指針。這在大多數32位的機器上能正常工作,但是在一臺64位的機器上卻會導致問題。

[這一點我很疑惑,int類型在32位機器爲4個字節,作爲指針的話最大地址爲4GB,但是64位機器的尋址地址最大爲16EB,是大於4GB的,爲啥會導致問題呢??????]
[這有啥疑惑的。。。正因爲int類型在32位機器和64位機器都是4個字節,所以int類型指針只能存最大爲4GB的指針,而64位機器需要存儲16EB的指針,超過了int類型指針的表示範圍,所以會有問題啊。 -2019/5/6]

尋址和字節順序

對於跨越多字節的程序對象,我們必須建立兩個規則:這個對象的地址是什麼,以及在內存中如何排列這些字節。在幾乎所有的機器上,多字節對象都被存儲爲連續的字節序列,對象的地址爲所使用字節中最小的地址。例如,假設一個類型爲int的變量x的地址爲0x100,也就是說,地址表達式&x的值爲0x100。那麼(假設數據類型int爲32位表示)x的4個字節將被存儲在內存的0x100、0x101、0x102、0x103位置。

排列表示一個對象的字節有兩個通用的規則。即小端法(little endian)大端法(big endian)小端法是指機器選擇在內存中按照從最低有效字節到最高有效字節的順序存儲對象;大端法是指機器選擇在內存中按照從最高有效字節到最低有效字節的順去存儲對象。

假設變量x的類型爲int,位於地址0x100處,它的十六進制值爲0x01234567。
在這裏插入圖片描述
注意,在字0x01234567中,高位字節的十六進制值是0x01,而低位字節是0x67。在哪種字節順序是合適的這個問題上,人們表現得非常情緒化。但其實只要選擇了一種規則並且始終如一地堅持,對哪種字節排序的選擇都是任意的。

對於大多數應用程序員來說,其機器所使用的字節順序是完全不可見的。無論爲哪種類型的機器所編譯的程序都會得到同樣的結果。不過有的時候,字節順序會成爲問題。

  1. 在不同類型的機器之間通過網絡傳送二進制數據時,小端法機器產生的數據發送到大端法機器時(或者反過來),字裏的字節成了反序的。爲了避免這類問題,網絡應用程序的代碼必須遵守已經建立的關於字節順序的規則,以確保發送方機器將它內部表示轉換成網絡標準,而接收方機器則將網絡標準轉換爲它的內部表示。
  2. 閱讀表示整數數據的字節序列時字節順序也很重要。這通常發生在檢查機器級程序時。就是在小端法機器生成的機器級程序表示中,書寫字節序列的方式是最低位字節在左邊,最高位在右邊,這正好與通常人類書寫數字時最高有效位在左邊,最低有效位在右邊的方式相反。
  3. 編寫規避正常的類型系統的程序時字節順序也會很重要,在C語言中,可以通過使用強制轉換類型(cast)聯合(union)來允許以一種數據類型引用一個對象,而這種數據類型與創建這個對象時定義的數據類型不同。

這裏展示一段C代碼,及其測試程序和結果:
在這裏插入圖片描述
在這裏插入圖片描述在這裏插入圖片描述
參數12345的十六進制表示爲0x00003039。

  1. 對於int類型的數據,除了字節順序以外,我們在所有機器上得到了相同的結果。(我們可以看到在Linux 32、windows和Linux 64上,最低有效字節0x39最先輸出,這說明它們是小端法機器;而在Sun上最後輸出,這說明Sun是大端法機器。)
  2. float數據的字節,除了字節順序以外,也都是相同的。
  3. 指針值卻是完全不同的。不同的機器/OS配置使用不同的存儲分配規則,Linux 32、Windows和Sun的機器使用4字節地址,而Linux 64使用8字節地址。

可以觀察到,儘管float和int數據都是對數值12345編碼,但是它們有截然不同的字節模型:int爲0x00003039,而float爲0x4640E400。如果我們將這些十六進制模式擴展爲二進制形式,並且適當地將它們移位,就會發現一個有13個相匹配的位的序列:
在這裏插入圖片描述
這不是巧合,以後研究浮點數格式的時候,還將再回到這個例子。

表示字符串

C語言中字符串被編碼爲一個以null字符結尾的字符數據。每個字符都由某個標準編碼來表示,最常見的是ASCII字符碼。因此,如果我們以參數“12345”和6來運行上面的show_bytes,我們得到結果31 32 33 34 35 00。注意,十進制數字x的ASCII碼正好是0x3x,而終止字節的十六進制表示爲0x00。在使用ASCII碼作爲字符碼的任何系統上都將得到相同的結果,與字節順序和字大小規則無關。因而,文本數據比二進制數據具有更強的平臺獨立性。

表示代碼

考慮下面的C函數:
在這裏插入圖片描述
當我們在示例機器上編譯時,生成如下字節表示的機器代碼:
在這裏插入圖片描述
我們發現指令編碼是不同的。不同的機器類型使用不同的且不兼容的指令和編碼方式。即使是完全一樣的進程,運行在不同的操作系統上也會有不同的編碼規則,因此二進制代碼是不兼容的。二進制代碼很少能在不同機器和操作系統組合之間移植。

計算機系統的一個基本概念就是,從機器的角度來看,程序僅僅只是字節序列。機器沒有關於原始源程序的任何信息,除了可能用來幫助調試的輔助表以外。

[怪不得在轉移代碼的時候,是轉移.c文件到對方電腦上去編譯,而不是直接轉移.o文件,原來是我自己電腦上的.o文件到對方電腦不能用的。]

布爾代數簡介

二進制值是計算機編碼、存儲和操作信息的核心,所以圍繞數值0和1的研究已經演化出了豐富的數學知識體系。
在這裏插入圖片描述
最後圖表中的^代表了異或。

將上述4個布爾運算擴展到位向量的運算,位向量就是固定長度爲w、由0和1組成的串。假設a = [0110] ,b = [1100],則四中運算:
在這裏插入圖片描述
布爾運算的運算法則有符合分配律:
&對|的分配律 :a & ( b | c ) = ( a & b ) | ( a & c )
|對&的分配律 :a | ( b & c ) = ( a | b ) & ( a | c )

此外布爾運算還有一個有趣的屬性:
a ^ a = 0
0 ^ 0 = 1 ^ 1 = 0
( a ^ b ) ^ a = b

即a是a的加法逆元
位向量一個很有用的應用就是表示有限集合。比如,a = [ 011010001 ]表示集合A = { 0 , 3 , 5 , 6,7},而 b = [ 01010101 ]表示集合B = { 0 , 2 , 4 , 6 }。

在大量實際應用中,我們都能看到用位向量來對集合編碼。例如,我們會看到很多不同的信號會中斷程序執行。我們能夠通過指定一個位向量掩碼,有選擇地使能或屏蔽一些信號,其中某一位位置上爲1時,表示信號i是有效的,而0表明該信號是被屏蔽的。因此,這個掩碼錶示的就是設置爲有效信號的集合。
[剛看完了input subsystem中,爲即將初始化的input_dev設置支持的事件類型,就是用設置evbit的位向量,之後在對應的handler函數處理時,也會首先檢查這個evbit向量的有效位。 -2019/5/6]

C語言中的位級運算

C語言的一個很有用的特性就是它支持按位布爾運算。以上的四種布爾運算(| 、 & 、 ~、 ^ )可以運用到任何“整形”的數據類型上。下圖是對char數據類型表達式求值的例子:
在這裏插入圖片描述
確定一個位級表達式的結果最好的方法,就是將十六進制的參數擴展成二進制表示並執行二進制運算,然後再轉換回十六進制。

上面說過位級運算的一個常見用法就是實現掩碼運算,這裏的掩碼是一個位模式,表示從一個字中選出的位的集合。舉個例子,掩碼0xFF(最低的8位爲1)表示一個字的低位字節。位級運算( x & 0xFF )生成一個由 x 的最低有效字節組成的值,而其他的字節就被置爲0。比如, x = 0x89ABCDEF,其表達式將得到0x000000EF。

C語言中的邏輯運算

C語言還提供了一組邏輯運算符|| 、 &&、 !,分別對應於命題邏輯中的OR、AND和NOT運算。邏輯運算很容易和位級運算相混淆,但是它們的功能是完全不同的。邏輯運算認爲所有非零的參數都表示TRUE,而參數0表示FALSE。
[在C語言的if語句中,條件不是0,即使是負值,都表示TRUE,一開始理解了好久。 -2019/5/6]
在這裏插入圖片描述
看得出來,按位運算只有在特殊情況下,也就是參數被限制爲0或者1時,才和與其對應的邏輯運算有相同的行爲。

C語言中的移位運算

C語言還提供了移位運算,向左或者向右移動位模式。

左移運算:x << k ,向左移動 k 位,丟棄最高的 k 位,並在右端補 k 個0。
右移運算:x >> k ,向右移動 k 位,但是它的行爲有點微妙。一般而言,機器支持兩種形式的右移邏輯右移算術右移邏輯右移在左邊補 k 個0,算術右移是在左端補 k 個最高有效位的值(雖然看上起很奇特,但是它對有符號整數數據的運算非常有用)。
以下是例子:
在這裏插入圖片描述
C語言標準並沒有明確定義對於有符號數應該使用哪類右移——算術右移邏輯右移都可以。不幸地,這就意味着任何假設一種或者另一種右移形式的代碼都可能遇到可移植性的問題。然而實際上,幾乎所有的編譯器/機器組合都對有符號數使用算術右移,且許多程序員也都假設機器會使用算術右移。對於無符號數,右移必須是邏輯右移

還需要注意的就是,必須要保證位移量 k 小於待移位值的位數。
[這裏加一個經常用來給寄存器賦值的移位操作:
假設我想給a寄存器的[0:1]位和[4:5]位寫入0x00
首先把0x00取反爲0x3,a = ~( ( 0x3 << 0 ) | ( 0x3 << 4) ) -2019/5/6 ]

整數表示

在本節中,我們描述用位來編碼整數的兩種不同的方式:一種只能表示非負數,而另一種能夠表示負數、零和整數。
下表展示引入一些數學術語,這些術語會在描述的過程中介紹。
在這裏插入圖片描述

整型數據類型

C語言支持多種整型數據類型——表示有限範圍的整數。這裏給出來的唯一一個與機器相關的取值範圍是大小指示符long的。大多數64位機器使用8個字節的表示,比32位機器使用的4個字節的表示的取值範圍大很多。
在這裏插入圖片描述
在這裏插入圖片描述
以上兩個圖標有一個值得注意的特點:取值範圍不對稱——負數的範圍比整數的範圍大 1 。

C語言標準定義了每種數據類型必須能夠表示的最小的取值範圍。如下圖,它們的取值範圍與上面所示的典型實現一樣或者小一些。特別的,除了固定大小的數據類型是例外,我們看到它們只要求正數和負數的取值範圍是對稱的。此外,數據類型int可以用兩個字節的數字來實現,這幾乎回退到了16位機器的時代。long的大小可以用4個字節的數字來實現。
在這裏插入圖片描述

無符號的編碼

無符號數即位向量的每一個位都成爲數字值的一部分,我們用一個函數B2U(Binary to Unsigned)來表示:
在這裏插入圖片描述
下面示例幾種情況下的B2U給出的從位向量到整數的映射:
在這裏插入圖片描述
無符號數的二進制表示有一個很重要的屬性,就是每個介於0 ~ (2的w次方 )- 1 的數都有唯一一個w位的值編碼,比如十進制數11作爲無符號數,只有一個4爲表示,即[ 1011 ]。
在這裏插入圖片描述
數學術語雙射是指一個函數 f 有兩面:它將數值 x 映射爲數值 y ,即 y = f ( x ) ,但是它也可以反向操作,即 x = f-1 ( y )。
[這不就是一個函數可不可導嘛 -2019/5/7]

補碼編碼

許多情況下,我們還希望表示負數值。最常見的有符號數的計算機表示方式就是補碼(two's-complement)形式。這個定義中,將字的最高有效位解釋爲負權(negative Weight)。我們用函數B2T(Binary to Two’s - complement)來表示。
在這裏插入圖片描述
最高有效位也成爲符號位,它的權重-(2的w-1次方),是無符號表示中權重的負數。符號位被設置爲 1 時,表示值爲負,反之爲非負。在這裏插入圖片描述
B2T也是一個雙射函數
在這裏插入圖片描述
在這裏插入圖片描述
關於這些數字,有幾個點值得注意:

  1. 補碼範圍是不對稱的,Tmin絕對值 = Tmax絕對值 + 1,也就是說 Tmin沒有與之對應的正數。之所以有這樣的不對稱性,是因爲一半的位模式(符號位 = 1)表示負數,而另一半(符號位 = 0)表示非負數。因爲0是非負數,也就意味着能表示的正數比負數少一個。
  2. 最大的無符號數值剛好比補碼的最大值的兩倍大一點,Umax = 2 * Tmax + 1。補碼錶示中所有表示負數的位模式在無符號表示中都變成了正數。
  3. 注意 -1 和 Umax 有同樣的位表示——一個全1的串
  4. 數值 0 在兩種表示方式中都是全0的串

C語言標準並沒有要求用補碼形式來表示有符號整數,但是幾乎所有的機器都是這麼做的。所以如果我們希望代碼有最大可移植性,除了C語言標準定義的數據類型的數值範圍之外,不能再假設任何可表示的數值範圍,也不能假設有符號數會使用何種特殊的表示方式。

爲了更好地理解補碼,考慮下面的代碼:
在這裏插入圖片描述
當在大端法機器上運行時,這段代碼的輸出爲 30 39 cf c7,指明 x 的十六進制表示爲0x3039,mx 的十六進制表示爲0xcfc7,分別展開爲位模式,x = [ 0011 0000 0011 1001] ,mx = [ 1100 1111 1100 0111 ],對這兩個位模式生成的值爲 12345 和 -12345。
在這裏插入圖片描述

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

C語言允許在不同的數字數據類型之間做強制類型轉換。例如,假設變量 x 聲明爲 int,u 聲明爲 unsigned。表達式(unsigned)x 會將 x 的值轉化爲一個無符號數值,而(int)u 將 u 的值轉換爲一個有符號整數。
那麼強制類型轉換會得到什麼結果呢?對於大多數C語言的實現來說,對於這個問題的回答是從位級角度來看的,而不是數的角度,即轉換前後的二進制位模式不會變,只是解釋方式改變了。[事情本身是沒有變的,只是你把它當做的東西變了,結果就變了,我本來是去開個門,你以爲我是開門通通氣,我們就接着聊;你以爲我是開門要你走,我們就散場。 -2019/5/7]

比如如下的代碼:
在這裏插入圖片描述
在一臺採用補碼的機器上,上述代碼會產生如下輸出:
v = -12345, uv = 53191
在上面的表格中看到了,-12345 的16爲補碼 和 53191的無符號表示的位模式是一樣的。

第二個例子:
在這裏插入圖片描述
在一臺採用補碼的機器上,上述代碼會產生如下輸出:
u = 4294967295, tu = -1

對於32位字長來說,無符號形式的4294967295(Umax)和補碼形式的 -1 的位模式是完全一樣的。將unsigned強制類型轉換爲 int ,底層的位表示保持不變。

從這個例子來看,T2U(- 12345 ) = 53191,U2T( 53191 ) = -12345。也就是說,十六進制表示寫作 0xCFC7 的16位位模式既是是-12345 的補碼錶示,又是 53191 的無符號表示。同時,12345 + 53191 = 65535 = 2的16次方。這個屬性可以推廣到給定位模式的兩個數值(補碼和無符號數)之間的關係。類似的,T2U(-1) = 4294967295,並且U2T(4294967295) = -1。同時,1 + 4294967295 = 4294967296 = 2的32次方。

也就是說,無符號表示中的Umax有着和補碼錶示的 -1 相同的位模式。這兩個數的關係:1 + Umax = 2的w次方
在這裏插入圖片描述
[這些答案都是我親自算過的,有不懂的可以直接留言問我。]
[誰不會啊還問的找你。。。 -2019/5/7]

C語言中的有符號數與無符號數

C語言中無符號數和有符號數之間的轉換,大多數系統遵循的原則是底層的位表示保持不變。

顯示的強制類型轉換就會導致轉換髮生:
在這裏插入圖片描述
另外,當一種類型的表達式被賦值給另外一種類型的變量時,轉換是隱式發生的:
在這裏插入圖片描述
當用printf輸出數值時,分別用 %d、 %u、 %x以有符號十進制、無符號十進制和十六進制格式輸出一個數字。考慮下面的代碼:
在這裏插入圖片描述
當在一個32位機器上運行時,它的輸出如下:
x = 4294967295 = -1
u = 2147483648 = -2147483648

當執行一個運算時,如果它的一個運算數是有符號的,另一個是無符號的,那麼C語言會隱式地將有符號數強制類型轉換爲無符號數,並假設這兩個數都是非負的,來執行這個運算。

這對於像 < 和 > 這樣的關係運算符來說,它會導致非直觀的結果。
在這裏插入圖片描述
這裏假設數據類型 int 表示爲32位補碼。考慮比較式 -1 < 0u。因爲第二個運算數是無符號的,第一個運算數就會被隱式的轉換爲無符號數,因此表達式就變爲了 4294967295u < 0u,這個答案顯然爲 0 。
在這裏插入圖片描述

擴展一個數字的位表示

一個常見的運算是在不同字長的整數之間轉換,同時又保持數值不變。當然,當目標數據類型太小以至於不能表示想要的值時,這根本就是不可能的。然而,從一個較小的數據類型轉換到一個較大的類型,應該總是可能的。

要將一個無符號數轉換爲一個更大的數據類型,我們只要簡單地在表示的開頭添加0。這種運算被稱爲零擴展(zero extension)

要將一個補碼數字轉換爲一個更大的數據類型,可以執行一個符號擴展(sign extension),在表示中添加最高有效位的值。

考慮下面的代碼:
在這裏插入圖片描述
在採用補碼錶示的32位大端法機器上運行這段代碼時,打印如下:
在這裏插入圖片描述
可以看到,儘管-12345 的補碼錶示和 53191 的無符號表示在16位字長時是相同的,但是在32位字長時卻是不同的。特別地,-12345 的十六進制表示爲 0xFFFFCFC7,而 53191 的十六進制表示爲 0x0000CFC7。前者使用的是符號擴展——最開頭加了16位,都是最高有效位 1,表示爲十六進制就是0xFFFF。後者開頭使用16個0來擴展,表示爲十六進制就是0x0000。

截斷數字

假設我們不用額外的位來擴展一個數值,而是減少一個數字的位數。例如下面的情況:
在這裏插入圖片描述
當我們把 x 強制類型轉換爲 short 時,我們就將32位的int截斷爲了16位的short int。這個16位的位模式就是 -12345 的補碼錶示。當我們把它強制類型轉換回int時,符號擴展把高16位設置爲 1,從而生成 -12345 的32位補碼錶示。

截斷一個數字可能會改變它的值——溢出的一種形式。對於一個無符號數,我們可以很容易得出其數值結果。

1.截斷無符號數
在這裏插入圖片描述
mod 是求模數的運算(求餘數),舉個例子:
B2U ( [ 10101 ] ) = 21
B2U ( [ 101 ] ) = 5
21 mod 8(2的3次方) = 5

B2U ( [ 11100 ] ) = 28
B2U ( [ 00 ] ) = 0
28 mod 4(2的2次方) = 0
[一定要注意是截斷爲k位,k = 3,截斷爲3位,而且是從低到高的3位]

  1. 截斷補碼數值(將最高位轉換爲符號位):
    在這裏插入圖片描述
    [就是截斷後依然將最高位視爲符號位,1爲負、0位正,怎麼可能不看成符號位呢,一個有符號數截斷以後還能弄成無符號數嗎?一壺水燒開了ph變成6了,還能變成酸水了? -2019/5/7]
    在這裏插入圖片描述
    [自己編程一定不會在變量賦值的時候造成截斷,給自己找麻煩。]

關於有符號數與無符號數的建議

像上一節看到的那樣,有符號數到無符號數的隱式強制類型轉換導致了某些非直觀的行爲。而這些非直觀的特性經常導致程序錯誤,並且這種包含隱式強制類型轉換的細微差別的錯誤很難被發現。

用以下兩個例子來展現這種細微的錯誤:
在這裏插入圖片描述
在這裏插入圖片描述
[這裏可能有的疑問是 無符號0 - 1 = Umax,[0000] - [0001] = [1111] = Umax,所以這個條件就成了必然滿足的條件了。]

我看到了很多無符號數運算的細微特性,尤其是有符號數到無符號數的隱式轉換,會導致錯誤或者漏洞的方式。避免這類錯誤的一種方法就是絕不使用無符號數。

當我們想要把字僅僅看作是位的集合而沒有任何數字意義時,無符號數值是非常有用的。例如,往一個字中放入描述各種布爾條件的標記(flag)時,就是這樣。地址自然地就是無符號的,所以系統程序員發現無符號類型是很有幫助的。當實現模運算和多精度運算的數學包時,數字是由字的數組來表示的,無符號值也會非常有用。

整數運算

剛入門的程序員非常驚奇地發現,兩個正數相加會得出一個負數,而比較表達式 x < y 和比較表達式 x - y < 0會產生不同的結果。這些屬性是由於計算機運行的有限性造成的。理解計算機運算的細微之處能夠幫助程序員編寫更可靠的代碼。

無符號加法

如果兩個加數取值範圍是0-15(四位),但是和的取值範圍爲0-30(五位),這樣的情況成爲字節膨脹,這意味着,想要完整的表示算術運算的結果,我們不能對字長做任何限制。但是常見的編程語言支持固定精度的運算,因此像“加法”和“乘法”這樣的運算不同於它們在整數上的相應運算。

x = 9 和 y = 12 的位表示分別是 [1001] 和 [1100] ,它們的和是21, 5位表示爲 [10101] ,但是如果丟棄了最高位,我們得到了 [0101],是十進制的5。

說一個算術運算溢出,是指完整的整數結果不能放到數據類型的字節限制中去。下圖中,當 x + y < 16 時,沒有溢出,對應與圖中標記爲“正常”的斜面。當 x + y >= 16 時,加法溢出,對應與圖中標記爲“溢出”的斜面。

在這裏插入圖片描述
當執行C程序時,不會將溢出作爲錯誤而發信號,不過有時候,我們可能希望判定是否發生了溢出。
在這裏插入圖片描述
9 + 12 = 5 。 由於 5 < 9 ,我們可以看出發生了溢出。
在這裏插入圖片描述

補碼加法

在這裏插入圖片描述
在這裏插入圖片描述
下圖描述了字長爲4的補碼加法。運算數的範圍爲-8 ~ 7之間。當 x + y < -8時,補碼加法就會負溢出,導致和增加了16。當 -8 <= x + y <= 8時,補碼加法就是x + y。當 x + y >= 8時,補碼加法就會正溢出,導致和減少了16。
在這裏插入圖片描述
溢出檢查:
在這裏插入圖片描述

補碼的非

在這裏插入圖片描述
這裏最好把無符號的非 和補碼的非 對比起來看:
無符號的非:
在這裏插入圖片描述

[無符號數的取反在上面說過了,這裏不再贅述]

補碼的非:
在這裏插入圖片描述
[因爲是補碼,所以從十六進制轉換爲十進制時要把位模式理解爲補碼,D的位模式是 [1101] 要注意這是補碼,所以轉爲十進制爲 -3 ,補碼數取非遵循上面的公式,-3 取非爲 3,-5 取非爲 5 ,-8(Tmin)取非爲 -8 ]

補碼非的位級表示:
計算一個位級表示的值的補碼非有幾種聰明的方法。這些計數很有用(例如當你在調試程序的時候遇到值0xfffffffa),同時它們也能夠讓你更多瞭解補碼錶示的本質。

執行位級補碼非的第一種方法是對每一位取反,再對結果加1。在C語言中,對於任意整數值x,計算表達式-x 和 ~x+1得到的結果完全相同。

示例如下:
在這裏插入圖片描述
[要注意這裏說的結果完全相同,是說轉化爲十進制的結果相同,不是位模式相同。-4的位模式是[1100] 取反後爲 [0011] 加1後爲 [0100] 轉換爲十進制爲 4 ]

[之前總不明白原碼反碼補碼,以爲有的編碼就是原碼,有的編碼就是補碼,現在才知道,編碼(即位模式)就是編碼,如果你把它當做了原碼,那讀出來的十進制數就是無符號數,如果把它當做補碼,那讀出來的十進制數就是有符號數。比如位模式[1101],作爲原碼,則讀出8 + 4 + 1 = 13,作爲補碼,則讀出 -8 + 4 + 1 = -3。強制類型轉換也只是對同一個編碼進行不同的解釋 -2019/5/13]

無符號乘法

在這裏插入圖片描述
x 和 y 都是w位的無符號數,不管x * y 的結果需要用多少位來表示,C語言都將結果截斷爲w位。而將一個無符號數截斷爲w位等價於計算該值對(2的w次方)的模。

補碼乘法

在這裏插入圖片描述
補碼數(即有符號數)的乘積最後還是補碼數,對這個補碼數截斷爲w位相當於計算該值對2的w次方求模,再把結果轉換爲補碼。
[比如下圖中的 -3 * 3 = -9 ,-9 mod 8 = -1 ,-1 的補碼錶示就是[111]。]
在這裏插入圖片描述
在之前XDR庫的代碼的安全漏洞,就是因爲乘法溢出導致的,如果在32位的機器上,兩個乘數結果的位模式超過了32位,截斷爲32位以後就出問題了。

乘以常數

以往,在大多數機器上,整數乘法指令相當慢,需要10個或者更多的時鐘週期,然而其他整數運算(加法、減法、位級計算額移位)只需要1個時鐘週期。即使在Intel Core i7上,其整數乘法也需要3個時鐘週期。因此,編譯器使用了一項重要的優化,試着用移位和加法運算的組合來代替乘以常數因子的乘法。

首先考慮乘以2的冪的情況:
在這裏插入圖片描述
比如,4位。11的位模式爲[1011]。k = 2時將其左移得到6位位模式[101100],即可編碼爲無符號數11 * 4 = 44。

[左移一個數值等價於執行一個與2的冪相乘的無符號乘法。而且無論是無符號運算還是補碼運算,乘以2的冪都可能導致溢出。即使溢出的時候,我們通過移位得到的結果也是一樣的。]

由於整數乘法比移位和加法的代價大得多,許多C語言編譯器試圖以移位、加法和減法的組合來消除很多整數乘以常數的情況。比如,x * 14 = x * (8 + 4 + 2) = ( x << 3 ) + ( x << 2 ) + ( x << 1 ),將乘法替換爲三個位移和兩個加法

[x * 14 是可以這樣,那x * -14呢?其實,x * -14 = ( x << 1 ) - ( x << 4 ) -2019/5/13]

歸納一下:對於某個常數K的表達式 x * K 生成代碼。編譯器會將K的二進制表示爲一組0和1交替的序列,比如14可以寫爲[(0…0)(111)(0)]。考慮一組從位置n到位置m的連續的1(n >= m),(對於14來說 n = 3, m = 1。)我們可以用下面兩種方法來計算這些位對乘積的影響:
在這裏插入圖片描述
那麼怎麼選擇A還是B呢?原則是 n = m,選擇A;否則選擇B。其實就是爲了儘可能的減少移位和加減的次數。

除以2的冪

在大多數機器上,整數除法比整數乘法更慢——需要30個或者更多的時鐘週期。所以除以2的冪也是用移位來實現,只不過用的是右移。無符號和補碼分別使用邏輯右移和算術右移來實現

[還記得之前說過的嗎?左移只有一種,右移就分爲了邏輯右移和算術右移,在這裏就用到了,邏輯右移是左邊補0,算術右移左邊補符號位]
在這裏插入圖片描述
直接以下面的例子來看,很直觀:
在這裏插入圖片描述
左端移入的0以斜體表示。並且移位總是舍入到零(例如771.25 舍入到 771)
在這裏插入圖片描述
對於x >= 0 ,變量x的符號位爲0,所以效果和邏輯右移是一樣的。對於負數,會按照向下舍入(例如-771.25 會舍入到 -772)
在這裏插入圖片描述
我們可以通過在移位之前偏置(biasing)這個值,來修正這個不合適的舍入
[爲啥這是不合適的。。。我沒覺得不合適啊。。。]
在這裏插入圖片描述
在這裏插入圖片描述
現在我們可以看到,除以2的冪可以通過邏輯或者算術右移來實現。這也正是爲什麼大多數機器上提供這兩種類型的右移。不過,這種方法不能想乘法那樣推廣到任意常數。

[是啊,乘以3 可以用(乘以4 - 本身)來實現,但是除以3 顯然沒辦法做出類似的操作。]
[沒啥補充的,就是想告訴你我5/13複習了一遍這一章 -2019/5/13]

關於整數運算的最後思考

計算機執行的“整數”運算實際上是一種模運算形式。
表示數字的有限字長限制了可能的值的取值範圍,運算結果可能溢出。

補碼錶示提供了一種既能表示負數也能表示正數的靈活方法,同時使用了執行無符號算數相同的位級實現,包括加法、減法、乘法和除法,無論運算數是以無符號形式還是補碼形式表示的,都有完全一樣或者非常類似的位級行爲。

我們看到了C語言中的某些規定可能會產生意想不到的結果,而這些結果可能是難以察覺或理解的缺陷的源頭。特別看到了unsigned數據類型,雖然它概念上很簡單,但可能導致意想不到的行爲。這種數據類型還會以出乎意料的方式出現,比如書寫整數常數和調用庫函數時。

浮點數

浮點數對執行涉及非常大的數字、非常接近於0的數字,以及更普遍地作爲實數運算的近似值的計算,是很有用的。

直到20世紀80年代,每個計算機制造商都設計了自己的表示浮點數的規則,以及對浮點數執行運算的細節。另外,它們常常不會太多地關注運算的精確性,而把實現的速度和簡便性看得比數字精確性更重要。
現在所有的計算機都支持IEEE浮點的標準,這大大提高了科學應用程序在不同機器上的可移植性。

本節中,我們將看到IEEE浮點格式中數字是如何表示的。我們還會探討舍入(rounding)的問題,即當一個數字不能被準確地表示爲這種格式時,就必須向上或者向下舍入。然後,我們將探討加法、乘法和關係運算符的數學屬性。許多程序員認爲浮點數沒意思,往壞了說,深奧難懂。我們將看到,因爲IEEE格式是定義在一組小而一致的原則上的,所以它實際上是相當優雅和容易理解的。
[對,我自己覺得沒意思就是因爲覺得難 -2019/5/13]

二進制小數

理解浮點數的第一步是考慮含有小數值的二進制數字。首先,讓我們來看看更熟悉的十進制表示法。
在這裏插入圖片描述
數字權的定義與十進制小數點符號(‘.’)相關,這意味着小數點左邊的數字的權是10的正冪,得到整數值,而小數點右邊的數字的權是10的負冪,得到小數值。例如
在這裏插入圖片描述
類似的,這種表示方法表示的二進制數定義如下:
在這裏插入圖片描述
在這裏插入圖片描述
例如:
在這裏插入圖片描述在這裏插入圖片描述
[根據這個定義,很容易知道二進制小數是無法表示所有的十進制小數的,比如0.25 = 1/4 是1/2的2次冪,所以二進制小數就是0.01,但是如果是0.2 = 1/5 ,不是1/2的任何次冪,就無法表示了。]

注意,形如0.11…1的數表示的是剛好小於1的數。

假定我們僅考慮有限長度的編碼,那麼十進制表示法不能準確的表達像1/3 和 5/7 這樣的數。類似,小數的二進制表示法只能表示哪些能夠被寫成(x * 2的y次冪)的數,其他值只能被近似地表示。例如,數字1/5可以用十進制小數0.20精確表示。不過我們並不能把它準確的表示爲一個二進制小數,我們只能近似的表示它,增加二進制表示的長度可以提高表示精度:
在這裏插入圖片描述
浮點運算的不精確性能夠產生災難性的後果。1991.2.25,在第一次海灣戰爭期間,沙特阿拉伯的達摩地區設置的美國愛國者導彈,攔截伊拉克的飛毛腿導彈失敗。飛毛腿導彈擊中了美國一個兵營,造成28名士兵死亡。美國總審計局對失敗原因做了詳細的分析,並且確定底層的原因在於一個數字計算不精確。
在這裏插入圖片描述
在這裏插入圖片描述

IEEE浮點表示

前一節中談到的定點表示法不能很有效地表示非常大的數字,例如,表達式5 * 2的100次冪是用101後面跟隨100個零的位模式來表示。
在這裏插入圖片描述
下圖給出將這三個字段裝進字中兩種最常見的格式。在單精度浮點格式(C語言中的float)中,s、exp和frac字段分別爲1位,k = 8位和n = 23位,得到一個32位的表示。在雙進度浮點格式(C語言中的double)中,s、exp和frac字段分別爲1位、k = 11位和n = 52位,得到一個64位的表示。
在這裏插入圖片描述
給定位表示,根據exp的值,被編碼的值可以分爲三種不同的情況(最後一種情況有兩個變種)。
在這裏插入圖片描述
[在看以下的3個case的描述時,你可能會覺得很懵,我建議你像我一樣,往下看下去再回頭來看,第一遍只需要知道這三類值的表示範圍,只要知道這就是“三種值”跟“浮點數”的關係類似“正數,零,負數”三個定義跟“數字”的關係一樣就好。然後去看8位浮點數格式的表格,再反過來看這三種情況的描述就會覺得豁然開朗了。]

case 1:規格化的值
這是最普遍的情況。當階碼的位模式既不全爲0,也不全爲1,都屬於這種情況。這種情況中,階碼字段被解釋爲以偏置(biased)形式表示的有符號整數。也就是說,階碼的值是 E = e - Bias,其中e是無符號數,而Bias是一個等於(2的k-1次方 - 1)的偏置值。
小數字段frac被解釋爲描述小數值f,其中0 <= f < 1(二進制小數點在最高有效位的左邊)。尾數定義爲M = 1 + f。有時,這種方式也叫做 隱含的以1開頭的(implied leading 1)表示。
既然我們總是能夠調整階碼E,使得尾數M在範圍 1 <= M < 2,之中,那麼這種表示方法是一種輕鬆獲得一個額外精度位的技巧。既然第一位總是1,那麼就不需要顯式地表示它。

這種情況表示的範圍,對於單精度是-126 ~ +127,而對於雙精度是 -1022 ~ +1023。

case 2:非規格化的值
當階碼域全爲0時,所表示的數是非規格化數。這種情況下,階碼值是E = 1 - Bias,而尾數的值是M = f,也就是小數字段的值,不包含隱含的開頭的1。

非規格化數有兩個用途。首先,它們提供了一種表示數值0的方法,+0.0的浮點表示的位模式爲全0:符號位是0,階碼字段全爲0,小數域也全爲0,這就得到M = f = 0。當符號位爲1,而其他域全爲0時,我們得到值-0.0。根據IEEE浮點格式,值+0.0和-0.0在某些方面被認爲是不同的,而在其他方面是相同的。
非格式化數的另外一個功能是表示那些非常接近於0.0的數。它們提供了一種屬性,稱爲逐漸溢出(gradual underflow),其中,可能的數值分佈均勻地接近於0.0。

case 3:特殊值
階碼全爲1的時候出現的。
當小數域全爲0時,得到的值表示無窮,s = 0時是+無窮,s = 1時是- 無窮。當我們把兩個非常大的數相乘,或者除以零時,無窮能夠表示溢出的結果。當小數域非零時,結果值被稱爲“NaN”,即“不是一個數(Not a Number)”。一些運算結果不能是實數或無窮,就會返回這樣的NaN值。在某些應用中,表示未初始化的數據時,它們也很有用處。
[對不起,我fo了,沒耐心了,下次接着看。 -2019/5/13 TODO:接着看]

數字示例

下圖展示了一組數值,它們可以用假定的6位格式來表示,有k = 3的階碼位和n = 2的尾數位。偏置量是[2的(3-1)次方 - 1 = 3]。a部分顯示了所有可能的值(除了NaN)。兩個無窮值在兩個末端。最大數量值的規格化數是±14。非規格化數聚集在0的附近。圖中b部分中,我們只展示了介於-1.0到+1.0之間的數值。兩個零是特殊的非規格化數。可以看到,那些可表示的數並不是均勻分佈的——越靠近原點處它們越稠密。
在這裏插入圖片描述
[這段描述中我不明白爲啥做最大數量值是 +14 / -14]
在這裏插入圖片描述
這個圖展示了8位浮點數格式的示例,其中有k = 4的階碼位和 n = 3的小數位。偏置量是7。
圖被分爲三個區域,來描述三類數字。不同的列給出了階碼字段是如何編碼階碼E的,小數字段是如何編碼尾數M的,以及它們一起是如何形成要表示的值
V = 2的e次方 * M的。
從0自身開始,最靠近0的是非規格化數。這種格式的非規格化數的E = 1 - 7 = -6,得到權是 1/64。小數f的值的範圍是0,1/8,2/8,…,7/8,從而得到數V的範圍是0 ~ 1/64 * 7/8 = 7/512。
接下來的是規格化數, E = 1 - 7 = -6,並且小數取值範圍也爲0,1/8,2/8,…,7/8。然而,尾數在範圍 1 + 0 = 1 和 1 + 7/8 = 15/8之間,得出數V在範圍8/512 和 15/512之間。
這種表示具有一個有趣的屬性,假如我們把上圖中的值的位模式解釋爲無符號數,它們就是按升序排列的,就像它們表示的浮點數一樣。這不是偶然的——IEEE格式如此設計就是爲了浮點數能夠使用整數排序函數來進行排序。當處理負數時,有一個小的難點,因爲它們有開頭的1,而且它們是按照降序出現的,但是不需要浮點運算來進行比較也能解決這個問題。

小練習:
這裏我們來聯繫一下浮點數的表示方式
在這裏插入圖片描述
在這裏插入圖片描述
聯繫把一些整數值轉換成浮點形式對理解浮點表示很有用。
例如,12345的無符號位模式是[ 11 0000 0011 1001]。通過將二進制小數點左移13位,得到這個數的一個規格化表示,得到 12345 = 1.1000000111001 * 2的13次方。
爲了用IEEE單精度形式來編碼,我們丟棄開頭的1,並且在末尾加10個0,來構造小數字段。得到 [1000000111001 0000000000]。爲了構造階碼字段,我們用13加上偏置量127,得到140,其二進制表示爲 [1000 1100]。加上符號位0,得到二進制浮點表示 : [0 10001100 10000001110010000000000]。

[這裏爲什麼要加10個0呢?因爲IEEE單精度形式編碼,要求浮點數一共是32位,1位符號,8爲階碼,23位小數位,12345的無符號位模式14位,去掉前面的1需要在家10位才能到23位。]

整數值13245(0x3039)和單精度浮點值12345.0(0x4640E400)在位級表示上有下列關係:
在這裏插入圖片描述

可以看到,相關的區域對應於整數的低位,剛好在等於1的最高有效位之前停止(這個位就是隱含的開頭的位1),和浮點表示的小數部分的高位是相匹配的。

再做一個小練習:
3510593的十六進制表示爲 0x00359141,推導出單精度浮點數3510593.0的十六進制表示:0x4A564504

舍入

因爲表示方法限制了浮點數的範圍和精度,所以浮點運算只能近似地表示實數運算。因此,對於值x,我們一般想用這一種系統的方法,能夠找到“最接近的”匹配值x’,它可以用期望的浮點形式表示出來,這就是舍入(rounding)運算的任務。

一個關鍵問題是在兩個可能值的中間確定舍入方向。例如,如果我有1.50美元,想把它舍入到最接近的美元數,應該是1美元還是2美元呢?一種可選擇的方法是維持實際數字的下界和上界。例如,我們可以確定可表示的值-x和+x,使得-x <= x <= +x。IEEE浮點格式定義了四種不同的舍入方式。默認的方法是找到最接近的匹配,而其他三種可用於計算上界和下界。

下圖表示了四種舍入方式,默認使用的是向偶數舍入向偶數舍入試圖找到一個最接近的匹配值。它採用的方法是:它將數字向上或者向下舍入,使得結果的最低有效數字是偶數。

在這裏插入圖片描述

[1.5 ,2.5, -1.5 這種數字可以用浮點數表示啊,爲啥還要舍入呢? 我覺得這裏舉例的數字只是爲了說明各個舍入的效果而已吧]

向偶數舍入看上去是一個很隨意的目標——有什麼理由偏向取偶數呢?爲什麼不始終把位於兩個可表示的值中間的值都向上舍入呢?使用這種方法的一個問題就是很容易假想到這樣的情景:這種方法舍入一組數值,會在計算這些值的平均數中引入統計偏差。我們採用這種方式舍入得到的一組數的平均值將比這些數本身的平均值略高一些。相反,如果我們總是把兩個可表示值中間的數字向下舍入,那麼舍入後的一組數的平均值將比這些數本身的平均值略低一些。向偶數舍入在大多數實現情況中避免了這種統計偏差。在50%的時間裏,它將向上舍入,而在50%的時間裏,它將向下舍入

在我們不想舍入到整數時可以使用向偶數舍入。我們只考慮最低有效數字是奇數還是偶數。例如,假設我們想將十進制數舍入到最接近的百分位。不管用哪種舍入方式,我們都將把1.2349999舍入到1.23,而將1.2450000都舍入到1.24,因爲4是偶數。

相似地,向偶數舍入法能夠運用在二進制小數上。我們將最低有效位的值0認爲是偶數,值1認爲是奇數。一般來說,只有對形如XX…X.YY…Y100…的二進制位模式的數,這種舍入方式纔有效,其中X和Y表示任意位置,最右邊的Y是要被舍入的位置。只有這種位模式表示在兩個可能的結果正中間的值。例如,考慮攝入到最近的四分之一的問題(也就是二進制小數點右邊2位)。我們將10.00011(23/32)向下舍入到10.00(2),10.00110(23/16)向上舍入到10.01(2*1/4),因爲這些值不是兩個可能值的正中間值。我們將10.11100(2 * 7/8)向上舍入到11.00(3),而10.10100(2 * 5/8)向下舍入成10.10(2 * 1/2),因爲這些值是兩個可能值的中間值,並且我們傾向於使最低有效位爲0。

以下舍入到最接近的二分之一(二進制小數點右邊1位):
在這裏插入圖片描述

在這裏插入圖片描述
在這裏插入圖片描述

[看過A答案後,發現舍入位置右邊如果有兩個1,那肯定是在舍入位置進1,如果舍入位置右邊有一個1,那麼就是剛纔說的處於兩個可能值的中間值,以讓最低有效位爲0的原則進行舍入。]

在這裏插入圖片描述

在這裏插入圖片描述

浮點運算

IEEE標準指定了一個簡單的規則,來確定諸如加法和乘法這樣的算術運算的結果。在實際中,浮點單元的設計者使用了一些聰明的小技巧,使得計算只要精確到能夠保證得到一個正確的舍入結果。當參數中有一個是特殊值(如-0,負無窮,NaN)時,IEEE標準定義了一些使之更合理的規則。例如,定義 1 / -0 將產生負無窮,而定義1 / +0 會產生正無窮。

IEEE標準中指定浮點運算行爲方法的一個優勢在於,它可以獨立於任何具體的硬件或者軟件實現。因此,我們可以檢查它的抽象數學屬性,而不必考慮它實際上是如何實現的。

前面我們看到了整數(包括無符號和補碼)加法形成了阿貝爾羣。實數上的加法也形成了阿貝爾羣,是可以交換的,但是不可以結合。例如,(3.14 + 1e10 ) - 1e10 求值得到 0.0 ——因爲舍入,值3.14 會丟失。另一方面,表達式 3.14 + (1e10 - 1e10)得出值爲 3.14 。作爲阿貝爾羣,大多數值在浮點加法下都有逆元,也就是說 x + -x = 0 。無窮(因爲 正無窮 - 無窮 = NaN)和NaN 是例外情況,因爲對於任何x,都有Nan + x = NaN。

浮點加法不具有結合性,這是缺少的最重要的羣屬性。對於科學計算程序員和編譯器編寫者來說,這具有重要的含義。例如,假設一個編譯器給定了如下代碼片段:
x = a + b + c;
y = b + c + d;
編譯器可能試圖通過產生下列代碼來省去一個浮點加法:
t = b + c;
x = a + t;
y = t + d;
然而,對於x來說,這個計算可能會產生於原始值不同的值,因爲它使用了加法運算的不同的結合方式。在大多數應用中,這種差異小得無關緊要。不幸的是,編譯器無法知道在效率和忠實於原始程序的確切行爲之間,使用者願意做出什麼樣的選擇。結果是,編譯器傾向於保守,避免任何對功能產生影響的優化,即使是很輕微的影響。

另一方面,浮點加法滿足了單調性屬性:如果 a >= b ,那麼對於任何 a、b以及 x的值,除了NaN,都有 x + a >= x + b 。無符號或補碼加法不具有這個實數(和整數)加法的屬性。

浮點乘法也遵循通常乘法所具有的許多屬性。我們定義 x * y 爲Round(x * y)。這個運算在乘法中是封閉的(雖然可能產生無窮大或NaN),它是可交換的,而且它的乘法單元爲1.0。另一方面,由於可能發生溢出,或者由於舍入而失去精度,它不具有可結核性。例如,單精度浮點情況下,表達式(1e20 * 1e20)1e-20求值爲正無窮,而1e20 * (1e201e-20)將得出1e20。另外,浮點乘法在加法上不具備分配性。例如,單精度浮點情況下,表達式 1e20 * (1e20 - 1e-20)求值爲 0 ,而1e20 * 1e20 - 1e20 * 1e20 會得出NaN。

另一方面,對於任何 a、b 和 c,並且a、b 和 c 都不等於NaN,浮點乘法滿足下列單調性:
a >= b 且 c >= 0 得出 a * c >= b * c
a >= b 且 c <= 0 得出 a * c <= b * c
此外,我們還可以保證,只要 a 不等於 NaN ,就有 a * a >= 0 。像我們先前所看到的,無符號或補碼的乘法沒有這些單調性屬性。
對於科學計算程序員和編譯器編寫者來說,缺乏結合性和分配性是很嚴重的問題。即使爲了在三維空間中確定兩條線是否交叉而寫代碼這樣看上去很簡單的任務,也可能成爲一個很大的挑戰。

C語言中的浮點數

所有的C語言版本提供了兩種不同的浮點數據類型:floatdouble 。在支持IEEE浮點格式的機器上,這些數據類型就對應於單精度和雙精度浮點。另外,這類機器使用 向偶數舍入 的舍入方式。不幸的是,因爲C語言標準不要求機器使用IEEE浮點,所以沒有標準的方法來改變舍入方式或者得到諸如-0正無窮負無窮或者NaN之類的特殊值。大多數系統提供include(‘.h’)文件和讀取這些特徵的過程庫,但是細節隨系統不同而不同。例如,當程序文件中出現下列句子時,GUN編譯器GCC會定義程序常數INFINTY(表示正無窮NAN(表示NaN)
#define_GUN_SOURCE 1
#include <math.h>

當在intfloatdouble格式之間進行強制類型轉換時,程序改變數值和位模式的原則如下(假設 int 是 32 位的):

  • int 轉換成 float ,數字不會溢出,但是有可能被舍入。
  • intfloat 轉換成 double ,因爲 double 有更大的範圍(也就是可表示值的範圍),也有更高的精度(也就是有效位數),所以能夠保留精確的數值。
  • double 轉換成 float ,因爲範圍要小一些,所以值可能溢出成 正無窮 或者 負無窮。另外,由於精確度較小,它還可能被舍入。
  • float 或者 double 轉換成 int,值將會向零舍入。例如,1.999將被轉換成1,而 -1.999 將被轉換成 -1。進一步來說,值可能會溢出。C語言標準沒有對這種情況指定固定的結果。與 Intel 兼容的微處理器指定位模式[10 … 00](字長爲w時的TMinw)整數不確定(integer indefinite)值。一個浮點數到整數的轉換,如果不能爲該浮點數找到一個合理的整數近似值,就會產生這樣一個值。因此,表達式(int) + 1e10 會得到 -21483648,即從一個正值變成了一個負值。

小結

計算機將信息編碼爲位(比特),通常組織成字節序列。有不同的編碼方式表示整數、實數和字符串。不同的計算機模型在編碼數字和多字節數據中的字節順序時使用不同的約定。
C語言的設計可以包容多種不同字長和數字編碼的實現。64位字長的機器逐漸普及。由於64位機器也可運行那些爲32位機器編譯的程序,我們的重點就放在區分32位和64位程序,而不是機器本身。64位程序的優勢是可以突破32位程序具有的4GB地址限制。
大多數機器對整數使用補碼編碼,而對浮點數使用IEEE標準754編碼,在位級上理解這些編碼,並且理解算數運算的數學特性,對於想使編寫的程序能在全部數值範圍上正確運算的程序員來說,是很重要的。
在相同長度的無符號和有符號整數之間進行強制類型轉換時,大多數C語言實現遵循的原則是底層的位模式不變。在補碼機器上,對於一個w位的值,這種行爲是由函數T2U 和 U2T 來描述的。C語言隱式的強制類型轉換會出現許多程序員無法預計的結果,常常導致程序錯誤。
由於編碼的長度有限,與傳統整數和實數運算相比,計算機運算具有非常不同的屬性。當超出表示範圍時,有限長度能夠引起數值溢出。當浮點數非常接近於0.0,從而轉換成零時,也會下溢。
和大多數其他程序語言一樣,C語言實現的有限整數運算和真實的整數運算相比,有一些特殊的屬性。例如,由於溢出,表達式 x * x 能夠得出負數。但是,無符號數和補碼的運算都滿足整數運算的許多其他屬性,包括結合律、交換律和分配律。這就允許編譯器做很多的優化。例如,用(x << 3) - x 取代表達式 7 * x時,我們就利用了 結合律、交換律和分配律的屬性,還利用了位移 和 乘以2的冪之間的關係。
我們已經看到了幾種使用位級運算和算術運算組合的聰明方法。例如,使用補碼運算,~x + 1等價於 -x。另外一個例子,假設我們想要一個形如[0,…,0,1,…,1]的位模式,由 w - k 個0後面緊跟着 k 個1組成。這些位模式有助於掩碼運算。這種模式能夠通過C表達式(1 << k) - 1生成,利用的是這樣一個屬性,即我們想要的位模式的數值爲 2的k次方 - 1。例如,表達式(1 << 8) - 1 將產生位模式 0xFF。
浮點表示通過將數字編碼爲 x * 2的y次方 的形式來近似地表示實數。最常見的浮點表示方式是由IEEE標準754定義的。它提供了幾種不同的精度,最常見的是單精度(32位)和雙精度(64位)。IEEE浮點也能夠表示特殊值 正無窮、負無窮 和 NaN。
必須非常小心地使用浮點運算,因爲浮點運算只有有限的範圍和精度,而且並不遵守普遍的算術屬性,比如結合性。

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