C語言的信息表示

前言

C語言是現在最重要、最流行的編程語言之一,相對於其餘程序設計語言,C語言更偏向底層結構化設計,提供對硬件更精準的控制,但也要求使用者更關注指針、位等字節級編程,本文以C語言爲載體,研究字節級信息的存儲和相關特性,討論字符串、整數、浮點數的編碼方式和相關隱患。不正之處,敬請指教html

1、位+上下文

衆所周知,現代計算機存儲和處理的信息以二進制信號表示,即0和1。計算機內都是以0、1bit串存儲信息,區分不一樣信息的惟一方法是根據解析0、1bit串的上下文。編程

首先,咱們來思考這麼個問題:給定一個串0110 0001,表明什麼含義呢?數組

在沒有任何上下文的時候,咱們直觀的猜想這是個數字,將二進制轉換爲十六進制爲0x61,轉換爲十進制則爲97。 若進一步假定計算機在讀取串0110 0001的上下文是解析ASCII碼,則串0110 0001被計算機解釋爲ASCII碼中的字符a。但若設定計算機在讀取串0110 0001的上下文是unsigned char掩碼,則0110 0001不表明任何含義,假如計算機在讀取該串的上下文是引用一個字長爲8bit的指針,則0110 0001就表明着地址97markdown

單獨的bit串並無意義,信息是經過位+上下文承載的,相同的位串在不一樣上下文被解釋成不一樣的信息。app

2、表示字符串

C語言中的字符串被編碼爲以null結尾的字符數組,字符串的每一個字符一般以ASCII字符碼進行編碼。編程語言

//"123"存儲爲:0x31 0x32 0x33 0x00
//二進制表示:0011 0001 0011 0010 0011 0011 0000 0000
char *str1 = "123";
//"abc"存儲爲:0x61 0x62 0x63 0x00
//二進制表示:0110 0001 0110 0010 0110 0011 0000 0000
char *str2 = "abc";
複製代碼

字符串的存儲方式是不受字節順序和字長影響的,所以字符串的可移植性是最好的。oop

3、表示整數

C語言數字編碼體系是真實世界數字體系的近似表示,在某系特性上存在必定的差別。字體

打個比方,C語言用char類型變量存儲[-128,127]共256個整數,在使用以下表達式時:編碼

/*例1*/
char x = -128;
char y = -x;//真實世界y應該是128,可是在C語言中,y仍是-128!

/*例2*/
char x = 100,y=30;
char sum = x+y;//真實世界y應該是130,可是在C語言中,y-126複製代碼

若對C語言數字編碼體系不瞭解,很容易致使程序出現極其隱祕的bug。下面咱們着重討論C語言是如何表示整數、浮點數。加密

(一)表示無符號

1. 概述

C語言針對整數提供兩種編碼方式:無符號編碼(unsigned)、有符號編碼(signed)。

首先,咱們來看一下十進制的正整數表示方式,思考咱們如何解釋123數字。應該爲 123 = 3 1 0 0 + 2 1 0 1 + 1 1 0 2 123=3*10^0+2*10^1+1*10^2

類比到無符號8bit的二進制0110 0001,有:

01100001 = 1 2 0 + 1 2 5 + 1 2 6 = 97 0110 0001=1*2^0 + 1*2^5 + 1*2^6=97

思考若用16bit空間存儲97,bit串應該是?

考慮 w w 位的bit串,公式化爲(公式1):

[ x w 1 , x w 2 , , x 1 , x 0 ] = x w 1 2 w 1 + x w 2 2 w 2 + + x 1 2 1 + x 0 2 0 = i = 0 w 1 x i 2 i [x_{w-1},x_{w-2},\cdots,x_{1},x_{0}] = x_{w-1}*2^{w-1}+x_{w-2}*2^{w-2}+\cdots+x_1*2^1+x_0*2^0=\sum_{i=0}^{w-1} x_i2^i

其中, x w x_{w} 爲第 w w 位的bit取值(注意是位,非字節)

基本C數據類型中,一般以1字節、2字節、4字節、8字節做爲存儲大小,則對應 w w 取值爲八、1六、3二、64

2. 取值範圍

一個頗有趣的現象,假設用8bit存儲無符號,最大取值爲 X M a x 8 = 2 8 1 = 255 XMax_8=2^8-1=255 ,思考爲啥不是 2 8 2^8 而是還要減1呢?

其實在給定的8bit中,二進制表示範圍爲0000 0000~1111 1111,那最大的就是全部存儲空間的bit取值爲1,即1111 1111。該值爲1 0000 0000-1,按公式1很容易知道1 0000 00002^8

聰明的你應該猜得出,給定長度爲 w w 的bit串存儲無符號整數,最大值都是奇數,爲 X M a x w = 2 w 1 XMax_w=2^w-1

無符號的最小值即所有bit取值爲0,所以不管bit串多少位, X M i n w = 0 XMin_w=0

C數據類型 最小值 最大值
unsigned char(8bit) 0 255
unsigned short(16bit) 0 65535
unsigned int(32bit) 0 4294967295

其餘無符號的數據類型均可以經過存儲空間的bit數肯定取值範圍。

(二)表示有符號

1. 概述

相比於無符號數的直觀,有符號數顯得更加晦澀。由於有符號數引入了補碼、減法、非對稱性。

1.1 原碼

首先,仍是咱們看一下十進制的負整數,好比-123,咱們就在123前面加個-號即表示負整數,咱們是否也可讓計算機用-0110 0001二進制形式表示-97

別忘了,咱們計算機只能存儲0、1,它不能存儲-號,所以簡單粗暴的作法是不行的,必須找一種編碼方式,讓二進制能夠表示負整數。

應該能夠很容易知道,那我就用0,1bit串的最高位做爲符號位(這種編碼稱爲原碼),若最高位爲0表明正數,最高位爲1表明負數。來看看這種編碼方式:

97 = 11100001 = 1 ( 1 2 0 + 1 2 5 + 1 2 6 ) -97= 1110 0001 = -1*(1*2^0 + 1*2^5 + 1*2^6)

這種編碼形式很直觀,但人們很快發現原碼並不適合計算機編碼。

爲何呢?由於計算機CPU沒有人那般智能,爲簡化硬件設計的複雜度,CPU只有加法器,沒有減法器!這意味着CPU將全部的減法都轉化爲加法進行運算。咱們嘗試在CPU角度利用原碼編碼方式上計算2-1:\

  1. 將2編碼爲 0000 0010
  2. 因爲只能作加法,咱們將2-1視爲2+(-1),則-1編碼爲 1000 0001
  3. 運算2+(-1)結果爲:1000 0011,即-3!很明顯錯誤了。

所以,採用原碼做爲有符號數的編碼不利於CPU簡化運算和硬件設計。

1.2 反碼
第二種編碼方式反碼被提出了,這種編碼方式將無符號編碼中最高位的權重從 2 w 1 2^{w-1} 轉換爲 2 w 1 1 -(2^{w-1}-1) ,在char類型中, w w 取值爲8,則 2 w 1 1 = 127 -(2^{w-1}-1) = -127 。 好比:

10000001 = 1 ( 2 w 1 1 ) + 1 2 0 = 126 1000 0001=1*(-(2^{w-1}-1)) + 1*2^0 = -126

這種編碼方式有個特色:正負數之間在位級上正好取反

1 = 00000001 1 = 00000001
1 = 11111110 -1 = 11111110

同時,反碼正好能彌補原碼在CPU中運算的不足,仍舊以2-1舉例:

  1. 將2編碼爲 0000 0010
  2. 因爲只能作加法,咱們將2-1視爲2+(-1),將1編碼爲0000 0001,則取反後-1編碼爲 1111 1110
  3. 運算2+(-1)結果爲:0000 0010+ 1111 1110 =1 0000 0000
  4. 根據反碼計算規則,若是有進位出現,則要把它送回到最低位去相加(循環進位),最後結果爲0000 0001,即爲1

經過反碼的運算能夠得出,CPU在只有加法器的條件下,經過配合簡單的位級取反和移位操做,就能快速的計算出最終減法結果。這種編碼規則能適應底層硬件設計,但你應該很快發現反碼取值有一個很奇怪的特性,對數字0有兩種編碼方式,即:

00000000 = 0 00000000 = 0
11111111 = 1 ( 2 w 1 1 ) + 1 2 6 + + 1 2 0 = 127 + 127 = 0 11111111 = 1*(-(2^{w-1}-1)) + 1*2^6 + \cdots + 1*2^0 = -127 + 127 = 0

也就是說反碼將編碼00000000解釋爲+0,將編碼11111111解釋爲-0。這種解釋方式和真實事件存在差別,畢竟咱們真實世界不會將0區分正負。

爲了改進該缺點,現代計算機都採用一種稱爲補碼的編碼形式,經過將負數空間再減1,使得編碼1111 1111~1000 0000-0~-127降低到-1~-128

現代計算機基本都是使用補碼做爲有符號整數的編碼方式

1.2 補碼

補碼編碼方式將最高位的權重視爲 2 w 1 -2^{w-1} ,注意:反碼的最高位權重爲 2 w 1 1 -(2^{w-1}-1)

借鑑無符號數的公式1,容易推出 w w 位有符號數的編碼定位(公式2):

[ x w 1 , x w 2 , , x 1 , x 0 ] = x w 1 2 w 1 + i = 0 w 2 x i 2 i [x_{w-1},x_{w-2},\cdots,x_{1},x_{0}] =-x_{w-1}2^{w-1} + \sum_{i=0}^{w-2} x_i2^i

注意:補碼編碼方式也有個特色,正負數之間在位級上正好取反加1,好比1-1的編碼以下:

11111111 = 1 2 7 + 1 2 6 + + 1 2 1 + 1 2 0 = 128 + 127 = 1 1111 1111 = -1*2^7+1*2^6+\cdots+1*2^1+1*2^0 = -128 + 127 = -1
00000001 = 1 2 0 = 1 0000 0001 = 1*2^0 = 1
1 = 11111111 = 取反( 00000001 + 1 -1 = 1111 1111 = 取反(0000 0001)+1

再來看看2-1的運算:

  1. 將2編碼爲 0000 0010
  2. 因爲只能作加法,咱們將2-1視爲2+(-1),將1編碼爲0000 0001,則取反加1後-1編碼爲 1111 1111
  3. 運算2+(-1)結果爲:0000 0010+ 1111 1111 =1 0000 0001
  4. 根據補碼計算規則,計算結果對 2 w 2^{w} 取模,最後結果爲0000 0001,即爲1

2. 無符號和有符號數的轉換

本文有講述過信息=位+上下文,那對於C語言而言,有符號和無符號數在存儲上並沒有區別,有的僅僅是對同一位串的不用解析。

公式1對比公式2:

i = 0 w 1 x i 2 i = x w 1 2 w 1 + i = 0 w 2 x i 2 i (公式 1 \sum_{i=0}^{w-1} x_i2^i=x_{w-1}2^{w-1} + \sum_{i=0}^{w-2} x_i2^i (公式1)
x w 1 2 w 1 + i = 0 w 2 x i 2 i (公式 2 -x_{w-1}2^{w-1} + \sum_{i=0}^{w-2} x_i2^i(公式2)

能夠從公式中看出,若無符號數轉有符號數:

有符號數值 = 無符號數值 2 x w 1 2 w 1 = 無符號數值 x w 1 2 w 有符號數值=無符號數值-2*x_{w-1}2^{w-1}=無符號數值-x_{w-1}2^{w}

若無符號數轉有符號數,則:

無符號數值 = 有符號數值 + 2 x w 1 2 w 1 = 有符號數值 + x w 1 2 w 無符號數值=有符號數值+2*x_{w-1}2^{w-1}=有符號數值+x_{w-1}2^{w}

好比在強制類型轉換中:

/*無符號轉有符號*/
unsigned char x = 255;
/*最高位權重從128轉變爲-128(變化了-256),即255-256=-1*/
char y = (char)x; //y=-1;
複製代碼
/*有符號轉無符號*/
char x = -128;
/*最高位權重從-128轉變爲128(變化了+256),即-128+256=128*/
unsigned char y = (unsigned char)x;//x=128
複製代碼

有興趣的同窗能夠將8bit無符號的100轉換爲有符號數,將8bit有符號的100轉換爲無符號。

3. 取值範圍

一個有趣的事實,使用補碼錶示有符號數,則會出現不對稱性。到底是什麼不對稱呢?

咱們對比有符號數的正數和負數二進制串表示範圍:

正數:0000 0001 ~ 0111 1111

負數:1000 0000 ~ 1111 1111

發現了嗎?不看最高位的狀況下,正數表示範圍從000 0001開始,而負數從000 0000開始。表明負數的範圍要比正數表示的範圍大1,這就是正負數取值範圍的不對稱性。

因此8bit有符號的二進制取值範圍爲:

正數:0000 0001 ~ 0111 1111 => 1 ~ 127

負數:1000 0000 ~ 1111 1111 => -128 ~ -1

0:0000 0000

綜上,8bit有符號取值範圍爲-128 ~ 127,在此就不羅列二、四、8字節有符號數的取值範圍了,原理都同樣,請小夥伴自行推導。

本節的最後,提個小小的思考題:

char x = -128;
//y的取值是?
char y = -x;
複製代碼

4、表示浮點數

相比於整數的編碼方式,浮點數的編碼更加精細,計算機對浮點數的編碼一般使用IEEE754標準。
考慮十進制小數的定點表示法,10的負指數次冪表示小數位,例如表示 12.3 4 10 12.34_{10} :

12.34 = 1 1 0 1 + 2 1 0 0 + 3 1 0 1 + 4 1 0 2 12.34 = 1*10^1 + 2*10^0 + 3*10^{-1} + 4*10^{-2}

對應的,能夠對二進制使用相同的定點表示法,例如用二進制表示 1. 5 10 1.5_{10} :

1. 5 10 = 1 + 1 2 = 1 2 0 + 1 2 1 = [ 1.1 ] 2 1.5_{10} = 1 + \frac{1}{2}=1*2^0 + 1*2^{-1} = [1.1]_{2}

這種表示方法很直觀,可是在表示很是大的數或很是靠近0的數須要至關多的存儲空間,好比表示數字 2 100 2^{100} 2 100 2^{-100} ,須要使用至少101個bit的內存空間。
所以,相似十進制表示法,引入科學計數法,經過 x 2 y x*2^y 形式表示一個數。在這種表示法中,只須要精確的存儲 x x y y 就能表示一個數,哪怕這個數很是大或者很是小。好比表示 2 100 2^{100} ,只需在存儲空間中存儲 x = 1 x=1 , y = 100 y=100 便可。

正式的,IEEE754標準經過形如 ( 1 ) s M 2 E (-1)^s*M*2^E 表示一個數,其中:

  • s:符號位,表示數值的正負數,參考上述整數的原碼錶示法
  • M:尾數位,表示一個二進制小數,即上述中的 y y ,經過frac字段計算得出
  • E:階碼位,表示對二進制小數加權,即上述中的 x x ,經過exp字段計算得出

32位浮點數表示:符號位1位,exp字段8位,frac字段23位 32位浮點數表示 64位浮點數表示:符號位1位,exp字段11位,frac字段52位 64位浮點數表示

IEEE754標準經過形如 ( 1 ) s M 2 E (-1)^s*M*2^E 表示一個數,所以想要解析或者編碼浮點數,必須計算出s,M,E三個值。根據存儲中的exp不一樣取值,對浮點數區分不一樣的編碼狀況,每種編碼狀況對應不一樣的計算方式:

image.png

只要肯定下exp字段和frac字段總位數,則肯定浮點數能夠表示多少個不一樣數字。但能夠經過調節exp字段和frac字段佔用的位數,肯定浮點數的表示範圍和密集度。當exp字段擁有更多位數時,浮點數的表示範圍更寬,當frac字段擁有更多位數時,浮點數表示更加密集

(一)規格化值

當exp的值不爲全0或全1狀況下,浮點數爲規範化值。對於kexp,nfrac,計算方式以下:

  • 計算E:
  1. 引入偏置值B:
B = 2 k 1 1 B = 2^{k-1}-1

32位浮點數B值爲127,64位浮點數B值爲1023

  1. 經過exp減去偏置值B表示有符號的E
E = e x p B E = exp - B

好比在32位浮點數中,內存中存儲的exp爲128,則 E = e x p B = 128 127 = 1 E=exp-B=128-127=1

注意此處用了偏置形式表示有符號E,而不用補碼形式表示,主要緣由在於偏置形式表示能夠直接經過二進制位比較不一樣浮點數的大小,而不用作任何補碼計算。

經過計算,能夠得出32位浮點數取值範圍爲-126~127,64位浮點數取值範圍爲-1022~1023

  • 計算M:
  1. frac字段視爲小數位,例如frac取值爲1101,則視爲小數位後爲0.1101
  2. 經過小數位frac字段加1計算M
M = f r a c + 1 M = frac + 1

好比frac取值爲1101,則M取值爲1.1101

該方式能夠得到一個額外精度位,由於不管frac取值爲什麼值,均將第一位視爲1

(二)非規格化的值

當exp的值爲全0狀況下,浮點數爲非規範化值,這種編碼表示0或很是接近0的數值。對於kexp,nfrac,計算方式以下:

  • 計算E:
  1. 引入偏置值B:
與規範值同樣: B = 2 k 1 1 與規範值同樣:B = 2^{k-1}-1

32位浮點數B值爲-126,64位浮點數B值爲-1022

  1. 經過1減去偏置值B表示有符號的E
E = 1 B E = 1 - B

好比在32位浮點數中,內存中存儲的exp爲0,則 E = 1 B = 1 127 = 126 E=1-B=1-127=-126

 注意此處經過B計算E的方法爲: E = 1 B E = 1 - B ,目的是和規格化表示相銜接,由於規格化表示最靠近0的取值時,也是 1 B 1-B

  • 計算M:
  1. frac字段視爲小數位,例如frac取值爲1101,則視爲小數位後爲0.1101
  2. 直接經過小數位frac字段得出M
M = f r a c M = frac

好比frac取值爲1101,則M取值爲0.1101

在規格化表示中, M = f r a c + 1 M = frac+1 ,這種表示方式不管frac取值爲什麼值,均將第一位視爲1,也就表明沒法使M爲0(注意frac取爲無符號表示,不能取值爲負數),所以,爲了使得M取值爲0,在非規格化中,直接經過小數位frac字段得出M M = f r a c M = frac

因爲精巧的設計非規格化的EM計算方式,使得非規格化和規格化表示能夠平滑過渡,即最大的非規格化數僅比規格化數小一點

(三)特定值

最後是當exp取值爲全1,則當frac取值爲全0時,表示無窮大,當frac取值不爲全0時,表示NaN,表明不是個數。

(四)溢出

浮點數經過 x 2 y x*2^y 形式表示一個數值,但不是任何數值均可以在有限空間中經過肯定的 x 2 y x*2^y 形式表示,所以經過IEEE 754編碼方式表示浮點數,避免不了存在溢出狀況。分兩種狀況討論:

  • x x ,即上述討論中的M沒法精確表示數值:

好比十進制的0.1,轉化爲二進制爲0.00011001100[1100],括號內的值爲無限循環數值,沒法經過有限存儲空間表示,發生溢出,致使浮點數只能近似表示。

- 當 y y ,即上述討論的E沒法表示過大或太小的數值:

好比十進制的 3.4 1 0 39 3.4*10^{39} ,沒法經過32位浮點數表示出來。

總結

本文主要圍繞計算機信息的表示,討論了字符串、無符號整數、有符號整數以及浮點數在字節級別的編碼形式。

  1. 字符串的每一個字符一般使用ASCII編碼進行存儲,具備良好的移植性
  2. 無符號整數使用無符號編碼,編碼形式簡潔直觀,不一樣的存儲空間大小決定無符號不一樣的整數取值範圍
  3. 有符號整數使用補碼存儲,該編碼形式解決了+0-0問題,同時也知足了硬件加法器的設計要求
  4. 浮點數使用IEEE 754標準進行編碼,即採用 x 2 y x*2^y 形式表示一個數字,只須要存儲 x x y y 便可表示浮點數,下降極大數或靠近0的數值對存儲空間的要求,但也使得存儲形式更加複雜。

參考文章:
1.深刻理解計算機系統
2.計算機爲何要用補碼?

相關文章
相關標籤/搜索