C語言是現在最重要、最流行的編程語言之一,相對於其餘程序設計語言,C語言更偏向底層結構化設計,提供對硬件更精準的控制,但也要求使用者更關注指針、位等字節級編程,本文以C語言爲載體,研究字節級信息的存儲和相關特性,討論字符串、整數、浮點數的編碼方式和相關隱患。不正之處,敬請指教html
衆所周知,現代計算機存儲和處理的信息以二進制信號表示,即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
就表明着地址97
。markdown
單獨的bit串並無意義,信息是經過位+上下文承載的,相同的位串在不一樣上下文被解釋成不一樣的信息。app
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
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
數字。應該爲
。
類比到無符號8bit
的二進制0110 0001
,有:
思考若用
16bit
空間存儲97
,bit串應該是?
考慮 位的bit串,公式化爲(公式1):
其中, 爲第 位的bit取值(注意是位,非字節)
基本C數據類型中,一般以1字節、2字節、4字節、8字節做爲存儲大小,則對應 取值爲八、1六、3二、64
2. 取值範圍
一個頗有趣的現象,假設用8bit存儲無符號,最大取值爲 ,思考爲啥不是 而是還要減1呢?
其實在給定的8bit中,二進制表示範圍爲0000 0000
~1111 1111
,那最大的就是全部存儲空間的bit取值爲1,即1111 1111
。該值爲1 0000 0000-1
,按公式1很容易知道1 0000 0000
爲2^8
。
聰明的你應該猜得出,給定長度爲 的bit串存儲無符號整數,最大值都是奇數,爲 。
無符號的最小值即所有bit取值爲0,所以不管bit串多少位,
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表明負數。來看看這種編碼方式:
這種編碼形式很直觀,但人們很快發現原碼並不適合計算機編碼。
爲何呢?由於計算機CPU沒有人那般智能,爲簡化硬件設計的複雜度,CPU只有加法器,沒有減法器!這意味着CPU將全部的減法都轉化爲加法進行運算。咱們嘗試在CPU角度利用原碼編碼方式上計算2-1
:\
0000 0010
;2-1
視爲2+(-1)
,則-1
編碼爲 1000 0001
;2+(-1)
結果爲:1000 0011
,即-3!很明顯錯誤了。所以,採用原碼做爲有符號數的編碼不利於CPU簡化運算和硬件設計。
1.2 反碼
第二種編碼方式反碼被提出了,這種編碼方式將無符號編碼中最高位的權重從
轉換爲
,在char類型中,
取值爲8,則
。 好比:
這種編碼方式有個特色:正負數之間在位級上正好取反
同時,反碼正好能彌補原碼在CPU中運算的不足,仍舊以2-1
舉例:
0000 0010
;2-1
視爲2+(-1)
,將1編碼爲0000 0001
,則取反後-1
編碼爲 1111 1110
;2+(-1)
結果爲:0000 0010+ 1111 1110 =1 0000 0000
;0000 0001
,即爲1
。經過反碼的運算能夠得出,CPU在只有加法器的條件下,經過配合簡單的位級取反和移位操做,就能快速的計算出最終減法結果。這種編碼規則能適應底層硬件設計,但你應該很快發現反碼取值有一個很奇怪的特性,對數字0有兩種編碼方式,即:
也就是說反碼將編碼00000000
解釋爲+0
,將編碼11111111
解釋爲-0
。這種解釋方式和真實事件存在差別,畢竟咱們真實世界不會將0
區分正負。
爲了改進該缺點,現代計算機都採用一種稱爲補碼的編碼形式,經過將負數空間再減1
,使得編碼1111 1111~1000 0000
從 -0~-127
降低到-1~-128
。
現代計算機基本都是使用補碼做爲有符號整數的編碼方式
1.2 補碼
補碼編碼方式將最高位的權重視爲 ,注意:反碼的最高位權重爲 。
借鑑無符號數的公式1,容易推出 位有符號數的編碼定位(公式2):
注意:補碼編碼方式也有個特色,正負數之間在位級上正好取反加1,好比1
和-1
的編碼以下:
再來看看2-1
的運算:
0000 0010
;2-1
視爲2+(-1)
,將1編碼爲0000 0001
,則取反加1後-1
編碼爲 1111 1111
;2+(-1)
結果爲:0000 0010+ 1111 1111 =1 0000 0001
;0000 0001
,即爲1
。2. 無符號和有符號數的轉換
本文有講述過信息=位+上下文,那對於C語言而言,有符號和無符號數在存儲上並沒有區別,有的僅僅是對同一位串的不用解析。
公式1對比公式2:
能夠從公式中看出,若無符號數轉有符號數:
若無符號數轉有符號數,則:
好比在強制類型轉換中:
/*無符號轉有符號*/
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;
複製代碼
相比於整數的編碼方式,浮點數的編碼更加精細,計算機對浮點數的編碼一般使用IEEE754
標準。
考慮十進制小數的定點表示法,10的負指數次冪表示小數位,例如表示
:
對應的,能夠對二進制使用相同的定點表示法,例如用二進制表示 :
這種表示方法很直觀,可是在表示很是大的數或很是靠近0的數須要至關多的存儲空間,好比表示數字
或
,須要使用至少101個bit的內存空間。
所以,相似十進制表示法,引入科學計數法,經過
形式表示一個數。在這種表示法中,只須要精確的存儲
和
就能表示一個數,哪怕這個數很是大或者很是小。好比表示
,只需在存儲空間中存儲
,
便可。
正式的,IEEE754
標準經過形如
表示一個數,其中:
s
:符號位,表示數值的正負數,參考上述整數的原碼錶示法M
:尾數位,表示一個二進制小數,即上述中的
,經過frac
字段計算得出E
:階碼位,表示對二進制小數加權,即上述中的
,經過exp
字段計算得出32位浮點數表示:符號位1位,exp
字段8位,frac
字段23位 64位浮點數表示:符號位1位,
exp
字段11位,frac
字段52位
IEEE754
標準經過形如
表示一個數,所以想要解析或者編碼浮點數,必須計算出s
,M
,E
三個值。根據存儲中的exp
不一樣取值,對浮點數區分不一樣的編碼狀況,每種編碼狀況對應不一樣的計算方式:
只要肯定下
exp
字段和frac
字段總位數,則肯定浮點數能夠表示多少個不一樣數字。但能夠經過調節exp
字段和frac
字段佔用的位數,肯定浮點數的表示範圍和密集度。當exp
字段擁有更多位數時,浮點數的表示範圍更寬,當frac
字段擁有更多位數時,浮點數表示更加密集
當exp的值不爲全0或全1狀況下,浮點數爲規範化值。對於k
位exp
,n
位frac
,計算方式以下:
E
:B
:32位浮點數
B
值爲127,64位浮點數B
值爲1023
exp
減去偏置值B
表示有符號的E
:好比在32位浮點數中,內存中存儲的exp
爲128,則
注意此處用了偏置形式表示有符號
E
,而不用補碼形式表示,主要緣由在於偏置形式表示能夠直接經過二進制位比較不一樣浮點數的大小,而不用作任何補碼計算。
經過計算,能夠得出32位浮點數
E
取值範圍爲-126~127
,64位浮點數E
取值範圍爲-1022~1023
M
:frac
字段視爲小數位,例如frac
取值爲1101
,則視爲小數位後爲0.1101
frac
字段加1計算M
:好比frac
取值爲1101
,則M
取值爲1.1101
該方式能夠得到一個額外精度位,由於不管
frac
取值爲什麼值,均將第一位視爲1
當exp的值爲全0狀況下,浮點數爲非規範化值,這種編碼表示0或很是接近0的數值。對於k
位exp
,n
位frac
,計算方式以下:
E
:B
:32位浮點數
B
值爲-126,64位浮點數B
值爲-1022
1
減去偏置值B
表示有符號的E
:好比在32位浮點數中,內存中存儲的exp
爲0,則
注意此處經過
B
計算E
的方法爲: ,目的是和規格化表示相銜接,由於規格化表示最靠近0的取值時,也是
M
:frac
字段視爲小數位,例如frac
取值爲1101
,則視爲小數位後爲0.1101
frac
字段得出M
:好比frac
取值爲1101
,則M
取值爲0.1101
在規格化表示中,
,這種表示方式不管frac
取值爲什麼值,均將第一位視爲1,也就表明沒法使M
爲0(注意frac
取爲無符號表示,不能取值爲負數),所以,爲了使得M
取值爲0,在非規格化中,直接經過小數位frac
字段得出M
:
因爲精巧的設計非規格化的
E
與M
計算方式,使得非規格化和規格化表示能夠平滑過渡,即最大的非規格化數僅比規格化數小一點
最後是當exp
取值爲全1,則當frac
取值爲全0時,表示無窮大,當frac
取值不爲全0時,表示NaN
,表明不是個數。
浮點數經過
形式表示一個數值,但不是任何數值均可以在有限空間中經過肯定的
形式表示,所以經過IEEE 754
編碼方式表示浮點數,避免不了存在溢出狀況。分兩種狀況討論:
M
沒法精確表示數值:好比十進制的0.1
,轉化爲二進制爲0.00011001100[1100]
,括號內的值爲無限循環數值,沒法經過有限存儲空間表示,發生溢出,致使浮點數只能近似表示。
- 當
,即上述討論的E
沒法表示過大或太小的數值:
好比十進制的
,沒法經過32
位浮點數表示出來。
本文主要圍繞計算機信息的表示,討論了字符串、無符號整數、有符號整數以及浮點數在字節級別的編碼形式。
+0
和-0
問題,同時也知足了硬件加法器的設計要求IEEE 754
標準進行編碼,即採用
形式表示一個數字,只須要存儲
和
便可表示浮點數,下降極大數或靠近0的數值對存儲空間的要求,但也使得存儲形式更加複雜。參考文章:
1.深刻理解計算機系統
2.計算機爲何要用補碼?