《深刻理解計算機系統》讀書筆記 —— 第二章 信息的表示和處理

本章主要研究了計算機中無符號數,補碼,浮點數的編碼方式,經過研究數字的實際編碼方式,咱們可以瞭解計算機中不一樣類型的數據可表示的值的範圍,不一樣算術運算的屬性,能夠知道計算機是如何處理數據溢出的。瞭解計算機的編碼方式,對於咱們寫出能夠跨越不一樣機器,不一樣操做系統和編譯器組合的代碼具備重要的幫助。程序員

@面試

信息存儲

爲何會有二進制?二進制有什麼含義和優點?

  對於有10個手指的人類來講,使用十進制表示法是很天然的事情,可是當構造存儲和處理信息的機器時,二進制值工做得更好。二值信號可以很容易地被表示、存儲和傳輸。例如,能夠表示爲穿孔卡片上有洞或無洞、導線上的高電壓或低電壓,或者順時針或逆時針的磁場。對二值信號進行存儲和執行計算的電子電路很是簡單和可靠,製造商可以在一個單獨的硅片上集成數百萬甚至數十億個這樣的電路。孤立地講,單個的位不是很是有用。然而,當把位組合在一塊兒,再加上某種解釋,即賦予不一樣的可能位模式以含意,咱們就可以表示任何有限集合的元素。好比,使用一個二進制數字系統,咱們可以用位組來編碼非負數。經過使用標準的字符碼咱們可以對文檔中的字母和符號進行編碼編程

計算機的三種編碼方式

  無符號:無符號(unsigned)編碼基於傳統的二進制表示法,表示大於或者等於零的數字。數組

  補碼:補碼(two' s-complement)編碼是表示有符號整數的最多見的方式,有符號整數就是能夠爲正或者爲負的數字。網絡

  浮點數:浮點數( floating-point)編碼是表示實數的科學記數法的以2爲基數的版本。函數

整數&浮點數

  在計算機中,整數的運算符合運算符的交換律和結合律,溢出的結果會表示爲負數。整數的編碼範圍比較小,可是其結果表示是精確的。學習

  浮點數的運算是不可結合的,而且其溢出會產生特殊的值——正無窮。浮點數的編碼範圍大,可是其結果表示是近似的。優化

  形成上述不一樣的緣由主要是由於計算機對於整數和浮點數的編碼格式不一樣。ui

虛擬內存&虛擬地址空間

  大多數計算機使用8位的塊,或者字節(byte),做爲最小的可尋址的內存單位,而不是訪問內存中單獨的位。機器級程序將內存視爲一個很是大的字節數組,稱爲虛擬內存( virtual memory)。內存的每一個字節都由一個惟一的數字來標識,稱爲它的地址(address),全部可能地址的集合就稱爲虛擬地址空間( virtual address space)。編碼

  指針是由數據類型和指針值構成的,它的值表示某個對象的位置,而它的類型表示那個位置上所存儲對象的類型(好比整數或者浮點數)。C語言中任何一個類型的指針值對應的都是一個虛擬地址。C語言編譯器能夠根據不一樣類型的指針值生成不一樣的機器碼來訪問存儲在指針所指向位置處的值。可是它生成的實際機器級程序並不包含關於數據類型的信息

二進制&十進制&十六進制

二進制轉十六進制(分組轉換)

  四位二進制能夠表示一位十六進制。二進制和十六進制的互相轉換方法以下表所示。這裏就不展開講解了。

十六進制 1 7 3 A 4 C
二進制 0001 0111 0011 1010 0100 1100
十進制轉十六進制

Gamma公式展現 \(\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N\) 是經過 Euler integral

  設x爲2的非負整數n次冪時,也就是\(x = {2^n}\)。咱們能夠很容易地將x寫成十六進制形式,只要記住x的二進制表示就是1後面跟n個0(好比
\(1024 = {2^{10}}\),二進制爲10000000000)。十六進制數字0表明4個二進制0。因此,當n表示成i+4j的形式,其中0≤i≤3,咱們能夠把x寫成開頭的十六進制數字爲1(i=0)、2(i=1)、4(i=2)或者8(i=3),後面跟隨着j個十六進制的0。好比,\(2048 = {2^{11}}\),咱們有n=11=3+4*2,從而獲得十六進制表示爲0x800。下面再看幾個例子。

n \({2^{n}}\)(十進制) \({2^{n}}\)(十六進制)
9 512 0x200
19(3+4*4) 524288 0x80000
14(2+4*2) 16384 0x4000
16(0+4*4) 65536 0x10000
17(1+4*4) 131072 0x20000
5(1+4*1) 32 0x20
7(3+4*1) 128 0x80

  十進制轉十六進制還可使用另外一種方法:展轉相除法。反過來,十六進制轉十進制能夠用相應的16的冪乘以每一個十六進制數字。

虛擬地址的範圍

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

16位字長機器的地址範圍:0~65535(FFFF)

32位字長機器的地址範圍:0~4294967296(FFFFFFFF,4GB)

64位字長機器的地址範圍:0~18446744073709551616(1999999999999998,16EB)

32位編譯指令:gcc -m32 main.c

64位編譯指令:gcc -m64 main.c

C語言基本數據類型的典型大小(字節爲單位)

有符號 無符號 32位 64位
[signed] char unsigned char 1 1
short unsigned short 2 2
int unsigned int 4 4
long unsigned long 4 8
int32_t uint32_t 4 4
int64_t uint64_t 8 8
char* 4 8
float 4 4
double 8 8

  注意:基本C數據類型的典型大小分配的字節數是由編譯器如何編譯所決定的,並非由機器位數而決定的。本表給出的是32位和64位程序的典型值。

  爲了不因爲依賴「典型」大小和不一樣編譯器設置帶來的奇怪行爲,ISOC99引入了類數據類型,其數據大小是固定的,不隨編譯器和機器設置而變化。其中就有數據類型int32_t和int64_t,它們分別爲4個字節和8個字節。使用肯定大小的整數類型是程序員準確控制數據表示的最佳途徑。

  對關鍵字的順序以及包括仍是省略可選關鍵字來講,C語言容許存在多種形式。好比,下面全部的聲明都是一個意思:

unsigned long 
unsigned long int 
long unsigned 
long unsigned int

大端&小端

  大端:是指數據的高字節保存在內存的低地址中,而數據的低字節保存在內存的高地址中,這樣的存儲模式有點兒相似於把數據看成字符串順序處理:地址由小向大增長,而數據從高位往低位放。

  小端:是指數據的高字節保存在內存的高地址中,而數據的低字節保存在內存的低地址中,這種存儲模式將地址的高低和數據位權有效地結合起來,高地址部分權值高,低地址部分權值低,和咱們的邏輯方法一致。

  舉個例子,假設變量x的類型爲int,位於地址0x100處,它的十六進制值爲0x01234567。地址範圍0x100~0x103的字節順序依賴於機器的類型。

大端法

地址 0x100 0x101 0x102 0x103
數據 01 23 45 67

小端法

地址 0x100 0x101 0x102 0x103
數據 67 45 23 01

注意,在字0x01234567中,高位字節的十六進制值爲0x01,而低位字節值爲0x67。

記憶方式:

大端==高尾端,即尾端(67)放在高地址(0x103)。

小端==低尾端,即尾端(67)放在低地址(0x100)。

擴展:大小端有什麼意義?

1.不一樣設備的數據傳輸

  A設備爲小端模式,B設備爲大端模式。當經過網絡將A設備的數據傳輸到B設備時,就會出現問題。(B設備如何轉換A設備的數據將在後面章節講解)

2.閱讀反彙編代碼

  假設Intel x86-64(x86都屬於小端)生成某段程序的反彙編碼以下:

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

  這條指令是把一個字長的數據加到一個值上,該值的存儲地址由0x200b43加上當前程序計數器的值獲得,當前程序計數器的值即爲下一條將要執行指令的地址。

  咱們習慣的閱讀順序爲最低位在左邊,最高位在右邊,0x00200b43。而小端模式生成的反彙編碼最低位在右邊,最高位在左邊,01 05 43 0b 20 00.和咱們的閱讀順序正好相反。

3.編寫符合各類系統的通用程序

/*打印程序對象的字節表示。這段代碼使用強制類型轉換來規避類型系統。很容易定義針對其餘數據類型的相似函數*/
#include <stdio.h>
typedef unsigned char* byte_pointer;
/*傳遞給 show_bytes一個指向它們參數x的指針&x,且這個指針被強制類型轉換爲「unsigned char*」。這種強制類型轉換告訴編譯器,程序應該把這個指針當作指向一個字節序列,而不是指向一個原始數據類型的對象。*/
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 ((byte_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* x));
}
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);
}

  以上代碼打印示例數據對象的字節表示以下表:

機器 類型 字節(十六進制)
Linux32 12345 int 39 30 00 00
Windows 12345 int 39 30 00 00
Linux64 12345 int 39 30 00 00
Linux32 12345.0 float 00 e4 40 46
Windows 12345.0 float 00 e4 40 46
Linux64 12345.0 float 00 e4 40 46
Linux32 &ival int * e4 f9 ff bf
Windows &ival int * b4 cc 22 00
Linux64 &ival int * b8 11 e5 ff ff 7f 00 00

  注:Linux64爲x86-64處理器。

  除了字節順序之外,int和 float的結果是同樣的。指針值與機器類型相關。參數12345的十六進制表示爲0x00003039。對於int類型的數據,除了字節順序之外,咱們在全部機器上都獲得相同的結果。此外,指針值倒是徹底不一樣的。不一樣的機器/操做系統配置使用不一樣的存儲分配規則。( Linux3二、 Windows機器使用4字節地址,而 Linux64使用8字節地址)

  能夠觀察到,儘管浮點型和整型數據都是對數值12345編碼,可是它們有大相徑庭的字節模式:整型爲0x00003039,而浮點數爲0x4640E400。通常而言,這兩種格式使用不一樣的編碼方法。

位運算符&邏輯運算符

  位運算符:& | ~ ^。邏輯運算符:&& || !。特別要 ~ 和!的區別,看下面的例子。

表達式 結果
!0x41 0x00
!!0x41 0x01
!0x00 0x01
~0x41 0x3E
~0x00 0xff

  「!」邏輯非運算符,邏輯操做符通常將其操做數視爲條件表達式,返回結果爲Bool類型:「!true」表示條件爲真(true)。「!false 」表示條件爲假(false)。

  "~"位運算符,表明位的取反,對於整形變量,對每個二進制位進行取反,0變1,1變0。

^爲異或運算符,有一個重要的性質:a ^ a = 0,a ^ 0= a。即任何數和其自身異或結果爲0,和0異或結果仍爲原來的數。利用這個性質,咱們能夠找出數組中只出現一次/兩次/三次等的數字。如何找呢?

例1:假設給定一個數組 arr,除了某個元素只出現一次之外,其他每一個元素均出現兩次。找出那個只出現了一次的元素。

思路:其他元素出現了都是兩次,所以,將數組內的全部元素依次異或,最後的結果即爲只出現一次的元素。好比,arr = [0,0,1,1,8,8,12],0 ^ 0 ^ 1 ^ 1^ 8^ 8^ 12 = 12。 感興趣的能夠本身編程試下。

例2:給定一個整數數組 arr,其中剛好有兩個元素只出現一次,其他全部元素均出現兩次。 找出只出現一次的那兩個元素。

思路:首先能夠經過異或得到兩個出現一次的數字的異或值,該異或值中的爲1的bit位確定是來自這兩個數字之中的一個。而後能夠隨便選一個爲1的bit位,按照這個bit位,將全部該位爲1的數字分爲一組,全部該位爲0的數字分爲一組,這樣就成了查找兩個子數組中只出現了一次的數字。

例3:假設給定一個數組 arr,除了某個元素只出現一次之外,其他每一個元素均出現了三次。找出那個只出現了一次的元素。

思路:能夠本身考慮下。

  邏輯運算符&& 和||還有一個短路求值的性質。具體以下。

  若是對第一個參數求值就能肯定表達式的結果,那麼邏輯運算符就不會對第二個參數求值。經常使用的例子以下

int main()
{
    int a=3,b=3;
    (a=0)&&(b=5);
    printf("a=%d,b=%d\n",a,b); // a = 0 ,b = 3
    (a=1)||(b=5);
    printf("a=%d,b=%d",a,b); // a = 1 ,b = 3
}

  a=0爲假因此沒有對B進行操做

  a=1爲真,因此沒有對b進行操做

邏輯左移和算術左移

  邏輯左移(SHL)和算數左移(SAL),規則相同,右邊統一添0

  邏輯右移(SHR),左邊統一添0

  算數右移(SAR),左邊添加的數和符號有關 (正數補0,負數補1)

  好比一個有符號位的8位二進制數11001101,邏輯右移無論符號位,若是移一位就變成01100110。算術右移要管符號位,右移一位變成11100110。

  e.g:1010101010,其中[]位是添加的數字

  邏輯左移一位:010101010[0]

  算數左移一位:010101010[0]

  邏輯右移一位:[0]101010101

  算數右移一位:[1]101010101

移位符號(<<, >>, >>>)

  <<,有符號左移位,將運算數的二進制總體左移指定位數,低位用0補齊。

  >>,有符號右移位,將運算數的二進制總體右移指定位數,正數高位用0補齊,負數高位用1補齊(保持負數符號不變)。

擴展:當移動位數大於實際位數時該怎麼辦?

  對於一個由w位組成的數據類型,若是要移動k≥w位會獲得什麼結果呢?例如,計算下面的表達式會獲得什麼結果,假設數據類型int爲w=32。

int lval=OxFEDCBA98 << 32;
int aval=0xFEDCBA98 >> 36;
unsigned uval = OxFEDCBA98u >>40;

   C語言標準很當心地規避了說明在這種狀況下該如何作。在許多機器上,當移動一個w位的值時,移位指令只考慮位移量的低\([{\log _2}w]\)位,所以實際上位移量就是經過計算k mod w獲得的。例如,當w=32時,上面三個移位運算分別是移動0、4和8位,獲得結果:

lval OxFEDCBA98
aval OxFFEDCBA9
uvaL OXOOFEDCBA

  不過這種行爲對於C程序來講是沒有保證的,因此應該保持位移量小於待移位值的位數。

整數表示

  約定一些術語以下所示

符號 類型 含義
\([B2{T_w}]\) 函數 二進制轉補碼
\([B2{U_w}]\) 函數 二進制轉無符號數
\([U2{B_w}]\) 函數 無符號數轉二進制
\([U2{T_w}]\) 函數 無符號轉補碼
\([T2{B_w}]\) 函數 補碼轉二進制
\([T2{U_w}]\) 函數 補碼轉無符號數
\(T{Min_w}\) 常數 最小補碼值
\(T{Max_w}\) 常數 最大補碼值
\(U{Max_w}\) 常數 最大無符號數
\(+ _w^t\) 操做 補碼加法
\(+ _w^u\) 操做 無符號數加法
\(* _w^t\) 操做 補碼乘法
\(* _w^u\) 操做 無符號數乘法
\(- _w^t\) 操做 補碼取反
\(- _w^u\) 操做 無符號數取反

無符號數的編碼

  無符號數編碼的定義:

  對向量\(\vec x = [{x_{w - 1}},{x_{w - 2}}, \cdots ,{x_0}]\):\(B2{U_w}(\vec x) = \sum\limits_{i = 0}^{w - 1} {{x_i}{2^i}}\)

  其中,\(\vec x\)看做一個二進制表示的數,每一個位\({x_i}\)取值爲0或1。舉個例子以下所示。

\(B2{U_4}([0001]) = 0*{2^3} + 0*{2^2} + 0*{2^1} + 1*{2^0} = 0 + 0 + 0 + 1 = 1\)

\(B2{U_4}([1111]) = 1*{2^3} + 1*{2^2} + 1*{2^1} + 1*{2^0} = 8 + 4 + 2 + 1 = 15\)

  無符號數能表示的最大值爲:\(UMa{x_w} = \sum\limits_{i = 0}^{w - 1} {{2^w} - 1}\)

補碼的編碼

  補碼編碼的定義:

  對向量\(\vec x = [{x_{w - 1}},{x_{w - 2}}, \cdots ,{x_0}]\):\(B2{T_w}(\vec x) = - {x_{w - 1}}{2^{w - 1}} + \sum\limits_{i = 0}^{w - 2} {{x_i}{2^i}}\)

  最高有效位\({x_{w - 1}}\)也稱爲符號位,它的「權重」爲$ - {2^{w - 1}}$,是無符號表示中權重的負數。符號位被設置爲1時,表示值爲負,而當設置爲0時,值爲非負。舉個例子

\(B2{T_4}([0001]) = - 0*{2^3} + 0*{2^2} + 0*{2^1} + 1*{2^0} = 0 + 0 + 0 + 1 = 1\)

\(B2{T_4}([1111]) = - 1*{2^3} + 1*{2^2} + 1*{2^1} + 1*{2^0} = - 8 + 4 + 2 + 1 = - 1\)

  w位補碼所能表示的範圍:\(TMi{n_w} = - {2^{w - 1}}\),\(TMa{x_w} = {2^{w - 1}} - 1\)

關於補碼須要注意的地方

  第一,補碼的範圍是不對稱的\(\left| {TMin} \right| = \left| {TMax} \right| + 1\),也就是說,\(TMin\)沒有與之對應的正數。之因此會有這樣的不對稱性,是由於一半的位模式(符號位設置爲1的數)表示負數,而另外一半(符號位設置爲0的數)表示非負數。由於0是非負數,也就意味着能表示的整數比負數少一個。

  第二,最大的無符號數值恰好比補碼的最大值的兩倍大一點:\(UMa{x_w} = 2TMa{x_w} + 1\)補碼錶示中全部表示負數的位模式在無符號表示中都變成了正數

  注:補碼並非計算機表示負數的惟一方式,只是你們都採用了這種方式。計算機也能夠用其餘方式表示負數,好比原碼和反碼。有興趣能夠繼續深刻了解。

肯定數據類型的大小

  在不一樣位長的系統中,int,double,long等佔據的位數不一樣,其可表示的範圍的大小也不同,如何編寫具備通用性的程序呢?ISO C99標準在文件stdint.h中引入了整數類型類。這個文件定義了一組數據類型,他們的聲明形式位:intN_t,uintN_t(N取值通常爲8,16,32,64)。好比,uint16_t在任何操做系統中均可以表述一個16位的無符號變量。int32_t表示32位有符號變量。

  一樣的,這些數據類型的最大值和最小值由一組宏定義表示,好比INTN_MIN,INTN_MAX和UINTN_MAX。

  打印肯定類型的內容時,須要使用宏。

  好比,打印int32_t,uint64_t,能夠用以下方式:

printf("x=%"PRId32",y=%"PRIu64"\n",x,y);

  編譯爲64位程序時,宏PRId32展開成字符串「d」,宏PRIu64則展開成兩個字符串「l」「u」。當C預處理器遇到僅用空格(或其餘空白字符)分隔的一個字符串常量序列時,就把它們串聯起來。所以,上面的 printf調用就變成了:printf("x=%d.y=%lu\n",x,y);

  使用宏能保證:不論代碼是如何被編譯的,都能生成正確的格式字符串。

無符號數和補碼的相互轉化

  補碼轉換爲無符號數:

對於知足在這裏插入圖片描述

舉例以下:

x \(T2{U_4}(x)\) 解釋
-8 8 -8<0,-8+\({2^4}\)=16,\(T2{U_4}(-8)\)=16
-3 13 -3<0,-3+\({2^4}\)=13,\(T2{U_4}(-3)\)=16
-2 14 -2<0,-2+\({2^4}\)=14,\(T2{U_4}(-2)\)=16
-1 15 -1<0,-1+\({2^4}\)=15,\(T2{U_4}(-1)\)=16
0 0 \(T2{U_4}(0)\)=0
5 5 \(T2{U_4}(5)\)=5

  無符號數轉補碼:

  對於知足\(0 \le u \le UMa{x_w}\)的u有:在這裏插入圖片描述

  結合下面兩張圖理解下:

  從補碼到無符號數的轉換。函數T2U將負數轉換爲大的正數

image-20201023171812180

  從無符號數到補碼的轉換。函數U2T把大於\({2^{w - 1}} - 1\)的數字轉換爲負值

image-20201023172007748

有符號數與無符號數的轉換

  前面提到過,補碼並非計算機表示負數的惟一方式,可是幾乎全部的計算機都是使用補碼來表示負數。所以無符號數轉有符號數就是使用函數\(U2{T_w}\),而從有符號數轉無符號數就是應用函數\(T2{U_w}\)

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

  好比,假設數據類型int表示爲32位補碼,求表達式-1<0U的值。由於第二個運算數是無符號的,第一個運算數就會被隱式地轉換爲無符號數,所以表達式就等價於4294967295U<0U,因此表達式的值爲0。

擴展數字

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

  好比,將16位的無符號數12(0xC),擴展爲32位爲0x0000000C。

  要將一個補碼數字轉換爲一個更大的數據類型,能夠執行一個符號擴展( sign exten sion),即擴展符號位。

  好比,將16位的有符號數-25(0x8019,1000000000011001),擴展爲32位爲0xffff8019。

截斷數字

  無符號數截斷公式爲:\(B2{U_k}(x) = B2{U_w}(X)mod{2^k}\)等價於\(x' = x\bmod {2^k}\),\(x'\)爲其截斷k位的結果。

  好比,將9從int轉換爲short,即須要截斷16位,k=16。\(9\bmod {2^{16}} = 9\)。所以,9從int轉換爲short的結果爲9。

  有符號數的截斷公式爲:\(B2{T_k}(x) = U2{T_k}(B2{U_w}(X)mod{2^k})\)等價於\(x' = U2{T_k}(x{\kern 1pt} {\kern 1pt} {\kern 1pt} mod{2^k})\),\(x'\)爲其截斷k位的結果。

  好比,將53791從int轉換爲short,即須要截斷16位,k=16。\(53791\bmod {2^{16}} = 53791\)\(U2{T_{16}}(53791) = 53791 - 65536 = - 12345\)。所以,53791從int轉換爲short的結果爲-12345。

  無符號數截斷的幾個例子(將4位數值截斷爲3位)

原始值 截斷值
0 0
2 2
9 1
11 3
15 7

  有符號數截斷的幾個例子(將4位數值截斷爲3位)

原始值 截斷值
0 0
2 2
-7 1
-5 3
-1 -1

小結

  關於有符號數和無符號數的轉換,數字的擴展與截斷,常常發生於不一樣類型不一樣位長數字的轉換,這些操做通常都是由計算機自動完成的,可是咱們最好要知道計算機是如何完成轉換的,這對於咱們檢查BUG是特別有用的。這些內容咱們不必定要都記住,可是當發生錯誤時,咱們是要知道從哪裏檢查。

整數運算

無符號數加法

  對知足在這裏插入圖片描述
,正常狀況下,x+y的值保持不變,而溢出狀況則是該和減去\({2^{\rm{w}}}\)的結果。

好比,考慮一個4位數字表示(最大值爲15),x=9,y=12,和爲21,超出了範圍。那麼x+y的結果爲9+12-15=6。

補碼加法

對知足
在這裏插入圖片描述

  當\({{2^{w - 1}} \le x + y}\),產生正溢出,當\({w + y < - {2^{w - 1}}}\),產生負溢出。當\({ - {2^{w - 1}} \le x + y < {2^{w - 1}}}\),正常。具體參考下圖。

image-20201023201221606

  舉例以下表所示(以4位補碼加法爲例)

x y x+y \(x + _4^ty\) 狀況
-8[1000] -5[1011] -13[10011] 3[0011] 1
-8[1000] -8[1000] -16[10000] 0[0000] 1
-8[1000] 5[0101] -3[11101] -3[1101] 2
2[0010] 5[0101] 7[00111] 7[0111] 3
5[0101] 5[0101] 10[01010] -6[1010] 4

補碼的非

  對知足\(TMi{n_w} \le x \le TMa{x_w}\)的x,其補碼的非\(- _w^tx\)由下式給出

在這裏插入圖片描述

(吐槽下CSDN,使用typora寫好latex公式,粘貼過來報錯,原來CSDN的Markdown是用Katex渲染的。這不是增長工做量嗎?)

  也就是說,對w位的補碼加法來講,\({TMi{n_w}}\)是本身的加法的逆,而對其餘任何數值x都有-x做爲其加法的逆。

無符號數的乘法

  對知足\(0 \le x,y \le UMa{x_w}\)的x和y有:\(x*_w^uy = (x*y)mod{2^w}\)

補碼的乘法

  對知足\(TMi{n_w} \le {\rm{x}}{\rm{y}} \le TM{\rm{a}}{{\rm{x}}_{\rm{w}}}\)的x和y有:\(x*_w^ty = U2{T_w}((x*y)mod{2^w})\)

舉例,3位數字乘法的結果

模式 x y x * y 截斷的x * y
無符號 4 [100] 5 [101] 20 [010100] 4 [100]
補碼 -4 [100] -3 [101] 12 [001100] -4 [100]
無符號 2 [010] 7 [111] 14 [001110] 6 [110]
補碼 2 [010] -1 [111] -2 [111110] -2 [110]
無符號 6 [110] 6 [110] 36 [100100] 4 [100]
補碼 -2 [110] -2 [110] 4 [000100] -4 [100]

常數與符號數的乘法

  在大多數機器上,整數乘法指令至關慢,須要10個或者更多的時鐘週期,然而其餘整數運算(例如加法、減法、位級運算和移位)只須要1個時鐘週期。所以,編譯器使用了一項重要的優化,試着用移位和加法運算的組合來代替乘以常數因子的乘法。

  因爲整數乘法比移位和加法的代價要大得多,許多C語言編譯器試圖以移位、加法和減法的組合來消除不少整數乘以常數的狀況。例如,假設一個程序包含表達式x*14。利用\(14 = {2^3} + {2^2} + {2^1}\),編譯器會將乘法重寫爲(x<<3)+(x<<2)+(x<1),將一個乘法替換爲三個移位和兩個加法。不管x是無符號的仍是補碼,甚至當乘法會致使溢出時,兩個計算都會獲得同樣的結果。(根據整數運算的屬性能夠證實這一點。)更好的是,編譯器還能夠利用屬性\(14 = {2^4} - 1\),將乘法重寫爲(x<<4)-(x<<1),這時只須要兩個移位和一個減法。

  概括如下,對於某個常數的K的表達式x * K生成代碼。咱們能夠用下面兩種不一樣形式中的一種來計算這些位對乘積的影響:

  形式A:\((x < < n) + (x < < (n - 1)) + \cdots + (x < < m)\)

  形式B:\((x < < (n + 1)) - (x < < m)\)

  對於嵌入式開發中,咱們常用這種方式來操做寄存器了。在編程中,咱們要習慣使用移位運算來代替乘法運算,能夠大大提升代碼的效率。

常數與符號數的除法

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

無符號數的除法

  對無符號運算使用移位是很是簡單的,部分緣由是因爲無符號數的右移必定是邏輯右移。同時注意,移位老是舍入到零

  舉例以下,以12340的16位表示邏輯右移k位的結果。左端移入的零以粗體表示。

k >>k(二進制) 十進制 \(12340/{2^k}\)
0 0011000000110100 12340 12340.0
1 0001100000011010 6170 6170.0
4 0000001100000011 771 771.25
8 0000000000110000 48 48.203125
補碼的除法(向下舍入)

  對於除以2的冪的補碼運算來講,狀況要稍微複雜一些。首先,爲了保證負數仍然爲負,移位要執行的是算術右移

  對於x≥0,變量x的最高有效位爲0,因此效果與邏輯右移是同樣的。所以,對於非負數來講,算術右移k位與除以\({2^k}\)是同樣的。

  舉例以下所示,對-12340的16位表示進行算術右移k位。對於不須要舍入的狀況(k=1),結果是\(x/{2^k}\)。當須要進行舍入時,移位致使結果向下舍入。例如,右移4位將會把-771.25向下舍入爲-772。咱們須要調整策略來處理負數x的除法。

k >>k(二進制) 十進制 \(-12340/{2^k}\)
0 1100111111001100 -12340 -12340.0
1 1110011111100110 -6170 -6170.0
4 1111110011111100 -772 -771.25
8 1111111111001111 -49 -48.203125
補碼的除法(向上舍入)

  咱們能夠經過在移位以前「偏置( biasing)」這個值,來修正這種不合適的舍入。

  下表說明在執行算術右移以前加上一個適當的偏置量是如何致使結果正確舍入的。在第3列,咱們給出了-12340加上偏量值以後的結果,低k位(那些會向右移出的位)以斜體表示。咱們能夠看到,低k位左邊的位可能會加1,也可能不會加1。對於不須要舍入的狀況(k=1),加上偏量隻影響那些被移掉的位。對於須要舍入的狀況,加上偏量致使較高的位加1,因此結果會向零舍入

k 偏量 -12340+偏量 >>k(二進制) 十進制 \(-12340/{2^k}\)
0 0 1100111111001100 1100111111001100 -12340 -12340.0
1 1 1100111111001101 1110011111100110 -6170 -6170.0
4 15 1100111111011011 1111110011111100 -771 -771.25
8 255 1101000011001011 1111111111001111 -48 -48.203125

總結

  如今咱們看到,除以2的冪能夠經過邏輯或者算術右移來實現。這也正是爲何大多數機器上提供這兩種類型的右移。不幸的是,這種方法不能推廣到除以任意常數。同乘法不一樣,咱們不能用除以2的冪的除法來表示除以任意常數K的除法。

浮點數

二進制小數

  一種關於二進制的小數編碼:\(b = \sum\limits_{i = - n}^m {{2^i} \times {b_i}}\)

image-20201024101019148

  二進制小數點向左移動一位至關於這個數被2除,二進制小數點向右移動一位至關於將數乘2。

IEEE浮點表示

IEEE浮點標準用\(V = {( - 1)^s} \times M \times {2^E}\)的形式來表示一個數

  • 符號(sign)s決定這數是負數(s=1)仍是正數(s=0),而對於數值0的符號位解釋做爲特殊狀況處理。

  • 尾數( significand)M是一個二進制小數,它的範圍是$1 \sim 2 - \varepsilon $,或者是$0 \sim 1 - \varepsilon $。

  • 階碼( exponent)E的做用是對浮點數加權,這個權重是2的E次冪(多是負數)。將浮點數的位表示劃分爲三個字段,分別對這些值進行編碼:

  • 一個單獨的符號位s直接編碼符號s

  • k位的階碼字段\(\exp = {e_{k - 1}} \cdots {e_1}{e_0}\)編碼階碼E。

  • n位小數字段\(frac = {f_{n - 1}} \cdots {f_1}{f_0}\)編碼尾數M,可是編碼出來的值也依賴於階碼字段的值是否等於0。

  C語言中的編碼方式:

  單精度浮點格式(float) —— s、exp和frac字段分別爲1位、k = 8位和n = 23位,獲得一個32位表示。

   雙精度浮點格式(double) —— s、exp和frac字段分別爲1位、k = 11位和n = 52位,獲得一個64位表示。

image-20201024102259994

  根據exp的值,被編碼的值能夠分紅三種不一樣的狀況:

image-20201024102336756

   狀況1:規格化的值 —— exp的位模式:既不全爲0(數值0),也不全爲1(單精度數值爲255,雙精度數值爲2047)。

   階碼的值:E = e - Bias(偏置編碼法)

  e是無符號數,其位表示爲 \({e_{k - 1}} \cdots {e_1}{e_0}\),單精度下取值範圍爲1~254.雙精度下取值範圍爲1 ~ 2047。

   偏置值\(Bias = {2^{k - 1}} - 1\),單精度下是127,雙精度下是1023。

  所以階段碼E的取值範圍:單精度下是-126 ~ +127。雙精度下是-1022 ~ 1024。

e的範圍:1~254

Bias的值:127

E的範圍:-126~127

   尾數的值:M=1+f(隱式編碼法,由於有個隱含的1,因此沒法表示0)

   其中\(0 \le f \le 1.0\),f的二進制表示爲\(0.{f_{n - 1}} \cdots {f_1}{f_0}\),也就是二進制小數點在最高有效位的左邊。

  所以添加了一個隱含的1,M的取值範圍爲\(1.0 \le M \le 2.0\)

爲何不在exp域中使用補碼編碼?爲何採用偏置編碼的形式?

exp域若是爲補碼編碼,比較兩個浮點數既要比較補碼的符號位,又要比較編碼位。

而在exp域中採用偏置編碼,咱們只須要比較一次無符號數e的值就能夠了。

舉例:float f = 15213.0

\(\begin{array}{l} {15213_{10}} = {11101101101101_2}\\ \quad {\kern 1pt} \;\;\, = {1.1101101101101_2} \times {2^{13}} \end{array}\)

\(\begin{array}{l} M = {1.1101101101101_2}\\ frac = {11011011011010000000000_2} \end{array}\)

則:

\(\begin{array}{l} E = 13\\ Bias = 127\\ \exp = 140 = {10001100_2} \end{array}\)

image-20201028192446712

  狀況2:非規格化的值 —— exp的位模式爲全0。

  階碼的值:E = 1 - Bias。

  尾數的值:M = f(沒有隱含的1,能夠表示0)

  非規格化數有兩個用途:

  表示數值0 —— 只要尾數M = 0。

   表示很是接近於0.0的數

  狀況3:特殊值 —— exp的位模式爲全1。

  當小數域全爲0時,獲得的值表示無窮,當s = 0時是\(+ \infty\),當s=1時是\(-\infty\)。當小數域爲非零0,獲得NaN(Not a Number)。

浮點數的運算規則

整數和浮點數相乘

  規則:\(x{ + _f}y = Round(x + y)\),\(x{ \times _f}y = Round(x \times y)\),其中\(Round(x \times y)\)要遵循下表的舍入規則。

1.4 1.6 1.5 2.5 -1.5
向0舍入 1 1 1 2 -1
向負無窮舍入 1 1 1 2 -2
向正無窮舍入 2 2 2 3 -1
偶數舍入(四捨五入) 1 2 2 2 -2
兩個浮點數相乘

  兩個浮點數相乘規則:\(({( - 1)^{s1}} \times M1 \times {2^{E1}}) \times ({( - 1)^{s2}} \times M2 \times {2^{E2}}) = {( - 1)^S} \times M \times {2^E}\)

  S:s1^s2

  M:M1+M2

  E:E1+E2

兩個浮點數相加

  浮點數相加規則:\({( - 1)^{s1}} \times M1 \times {2^{E1}} + {( - 1)^{s2}} \times M2 \times {2^{E2}} = {( - 1)^S} \times M \times {2^E}\)

  S和M的值爲兩個浮點數小數點對齊後相加的結果。

  E:E1 (假設E1>E2)
image-20201029112315391

浮點數的偶數舍入

  例若有效數字超出規定數位的多餘數字是1001,它大於超出規定最低位的一半(即0.5),故最低位進1。若是多餘數字是0111,它小於最低位的一半,則舍掉多餘數字(截斷尾數、截尾)便可。對於多餘數字是1000、正好是最低位一半的特殊狀況,最低位爲0則舍掉多餘位,最低位爲1則進位一、使得最低位仍爲0(偶數)。

  注意這裏說明的數位都是指二進制數。

舉例:要求保留小數點後3位。

對於1.0011001,舍入處理後爲1.010(去掉多餘的4位,加0.001)
對於1.0010111,舍入處理後爲1.001(去掉多餘的4位)
對於1.0011000,舍入處理後爲1.010(去掉多餘的4位,加0.001,使得最低位爲0)

對於1.1001001,舍入處理後爲1.101(去掉多餘的4位,加0.001)
對於1.1000111,舍入處理後爲1.100(去掉多餘的4位)
對於1.1001000,舍入處理後爲1.100(去掉多餘的4位,不加,由於最低位已經爲0)

對於1.01011,舍入處理後爲1.011(去掉多餘的2位,加0.001)
對於1.01001,舍入處理後爲1.010(去掉多餘的2位)
對於1.01010,舍入處理後爲1.010(去掉多餘的2位,不加)

注意

  浮點數的運算不支持結合律。

舉例:(1e10+3.14)-1e10=0,3.14+(1e10-1e10)=3.14。由於舍入的緣由,第一個表達式會丟失3.14。

舉例:(1e20 * 1e20)1e-20 求值爲正無窮,而1e20 * (1e201e-20) = 1e20。

C語言中的浮點數

在C語言中,當在int、float和 double格式之間進行強制類型轉換時,程序改變數值和位模式的原則以下(假設int是32位的)

  • 從int轉換成 float,數字不會溢出,可是可能被舍入。
  • 從int或float轉換成 double,由於double有更大的範圍(也就是可表示值的範圍),也有更高的精度(也就是有效位數),因此可以保留精確的數值。
  • 從 double轉換成float,由於範圍要小一些,因此值可能溢出成\(+ \infty\)\(- \infty\)。另外,因爲精確度較小,它還可能被舍入從float或者 double轉換成int,值將會向零舍入。例如,1.999將被轉換成1,而-1.999將被轉換成-1。進一步來講,值可能會溢出。C語言標準沒有對這種狀況指定固定的結果。一個從浮點數到整數的轉換,若是不能爲該浮點數找到一個合理的整數近似值,就會產生這樣一個值。所以,表達式(int)+1e10會獲得-21483648,即從一個正值變成了一個負值。

舉例:int x = ...; float f = ....;double d =... ;

表達式 對/錯 備註
x == (int)(float)x float 沒有足夠的位表示int,轉換會形成精度丟失
x == (int)(double)x
f ==(float)(double)f
d == (double)(float)d float->double精度不夠
f == -(-f)
2/3 == 2/3.0
d<0.0 ==> ((d*2)<0.0)
d>f ==> -f >-d
d*d >=0.0
(d+f)-d == f 沒有結合律

總結

  本章中須要掌握的內容主要有:無符號數,補碼,有符號數的編碼方式,可表示的範圍大小,相互轉換的規則,運算規則。浮點數的編碼方式瞭解便可,這部分有點難以理解,若是後面有用到的話再回來細看,可是對於C語言中其餘數據類型到浮點數的轉換規則是要掌握的。

  養成習慣,先贊後看!若是以爲寫的不錯,歡迎關注,點贊,轉發,一鍵三連謝謝!

如遇到排版錯亂的問題,能夠經過如下連接訪問個人CSDN。

CSDN:CSDN搜索「嵌入式與Linux那些事」

歡迎歡迎關注個人公衆號:嵌入式與Linux那些事,領取秋招筆試面試大禮包(華爲小米等大廠面經,嵌入式知識點總結,筆試題目,簡歷模版等)和2000G學習資料。

相關文章
相關標籤/搜索