前言
最近在學習中涉及到計算機儲存、傳輸數字和字符等操作,由於對字節、2進制、10進制、16進制、ASCII碼的概念以及它們之間的關系和轉換理解的不夠透徹,導致在通訊、MD5消息摘要算法等時候出現問題,是因為數據轉成計算機認識的01的這個環節出現問題。由於這個問題並不是那么容易發現,所以我也算是花了挺多時間才解決了這個問題,記錄下解決過程,順便也當復習一下計算機組成原理。
ASCII碼
在計算機中,所有的數據在存儲和運算時都要使用二進制數表示(因為計算機用高電平和低電平分別表示1和0),例如,像a、b、c、d這樣的52個字母(包括大寫)以及0、1等數字還有一些常用的符號(例如*、#、@等)在計算機中存儲時也要使用二進制數來表示,而具體用哪些二進制數字表示哪個符號,當然每個人都可以約定自己的一套(這就叫編碼),而大家如果要想互相通信而不造成混亂,那么大家就必須使用相同的編碼規則,於是美國有關的標准化組織就出台了ASCII編碼,統一規定了上述常用符號用哪些二進制數來表示。
ASCII 碼一共規定了128個字符(0000 0000-0111 1111)的編碼,比如空格SPACE是32(二進制0010 0000),大寫的字母A是65(二進制0100 0001 )。這128個符號(包括32個不能打印出來的控制符號),只占用了一個字節的后面7位(低7位),最前面的一位(高1位)統一規定為0(不要和數字的符號位搞混)。
當然除了ASCII碼,還有UTF-8、GBK等。
字節
字節(Byte)普通計算機系統能讀取和定位到最小信息單位,即我們通過計算機儲存和傳輸數據的時候都是先把數據轉成字節。
字節即Byte,一個字節代表8個比特(Bit),字節通常縮寫為B,比特通常縮寫為b。字節的大小是8Bit,即字節的范圍是0000 0000 - 1111 1111,對於無符號型,它表示的十進制范圍是[0,255],對於有符號型,高一位表示符號位,它表示的十進制范圍是[-128,127]。
計算機若何儲存數據
計算機只認識0和1(因為計算機只有高低電平兩個狀態),數據要想通過計算機儲存或者傳輸,首先是要把數據轉成計算機能認識的格式即01數據。
我們舉個例子,以儲存十進制數字28和-28為例,首先將十進制數轉成二進制。
需要注意的是: 數字在計算機中儲存的是補碼,而字符是在計算機中儲存的是字符對應的編碼(不要和數字的補碼搞混)。
數字
儲存十進制數字28和-28為例,首先將十進制數轉成二進制,高1位為0代表正數,為1代表負數
28(10) = 0001 1100(2)(原碼)
-28(10) = 1001 1100(2)(原碼)
然后計算機將二進制數字進行補碼運算,運算結果如下
28(10) = 0001 1100(2)(原碼) = 0001 1100(2)(補碼)
-28(10) = 1001 1100(2)(原碼) = 1110 0100(2)(補碼)
然后計算機保存的就是補碼,當要取出數據的時候,就將補碼逆運算一下,即可求出原碼,再將原碼轉換一下就可以得到真實的數據了。
下面以Java語言演示這個過程,首先我們要清楚Java的byte、short、int、long都是有符號的(signed)
public class test {
public static void main(String[] args) throws NoSuchAlgorithmException {
int a1 = 28;
int a2 = -28;
// 轉成二進制表示
String b1 = Integer.toBinaryString(a1);
String b2 = Integer.toBinaryString(a2);
// 轉成無無符號表示
String b3 = Integer.toUnsignedString(a1);
String b4 = Integer.toUnsignedString(a2);
System.out.println("28儲存到計算機后為:" + b1);
System.out.println("-28儲存到計算機后為:" + b2);
System.out.println("取出儲存的28 以無符號表示:" + b3);
System.out.println("取出儲存的-28 以無符號表示:" + b4);
}
}
運行輸出:
28儲存到計算機后為:11100
-28儲存到計算機后為:11111111111111111111111111100100
取出儲存的28 以無符號表示:28
取出儲存的-28 以無符號表示:4294967268
我們驗證一下結果,驗證了計算機確實是以補碼的方式儲存數字。這里有個小問題,就是我們知道int型有4個字節即32個比特,但是28卻輸出了111005個比特而已,是因為toBinaryString()方法把11100前面的0給忽略了。
取出的時候,我們以無符號的標准去處理,導致取出存入的-28結果是4294967268和我們存入的不一樣,這是因為-28是負數,負數的補碼和原碼不一樣,而用無符號處理的話就是直接將11111111111111111111111111100100轉成結果了。而為什么28用有無符號處理結果都一樣是因為正數的原碼和補碼一樣,這樣驗證了Java的數據類型都是有符號的。
至於計算機為什么用補碼來儲存數字,而不是原碼,原因是:
拿單字節整數來說,無符號型,其表示范圍是[0,255],總共表示了256個數據。有符號型,其表示范圍是[-128,127]。
先看無符號,原碼和補碼都一樣,0表示為0000 0000,255表示為1111 1111,剛好滿足了要求,可以表示256個數據。
再看有符號的,若是用原碼表示,0表示為0000 000。因為咱們有符號,所以應該也有個負0(雖然它還是0)1000 0000。這樣的話那就有2個0,也就是只能表示255個數據,不能夠滿足我們的要求。而用補碼則很好的解決了這個問題。
字符
在計算機中,對非數值的字符進行處理時,要對字符進行數字化,即用二進制編碼來表示字符。其中西文字符最常用到的編碼方案有ASCII編碼和EBCDIC編碼。對於漢字,我國也制定的相應的編碼方案,比如 GBK,GB2312等。
比如字符a的ASCII碼十進制值為97,在計算機中用二進制表示就是 01100001。下面同樣用Java來演示計算機是如何儲存字符的。
采用UTF-8和GBK兩種編碼儲存漢字
public class test {
public static void main(String[] args) throws NoSuchAlgorithmException {
String a1 = "中";
// 采用兩種不同的編碼儲存"中"這個漢字 比較兩種編碼
byte[] b1 = a1.getBytes("GBK");
byte[] b2 = a1.getBytes("UTF-8");
String c1 = binary(b1,2);
String c2= binary(b2,2);
System.out.println("GBK儲存對應的二進制:" + c1);
System.out.println("UTF-8儲存對應的二進制:" +c2);
}
/**
* 將byte[]轉為各種進制的字符串
* @param bytes byte[]
* @param radix 基數可以轉換進制的范圍,從Character.MIN_RADIX到Character.MAX_RADIX,超出范圍后變為10進制
* @return 轉換后的字符串
*/
public static String binary(byte[] bytes, int radix){
return new BigInteger(1, bytes).toString(radix);// 這里的1代表正數
}
}
我們調試看看,發現GBK編碼采用2個字節儲存,儲存的數據分別是10進制的-42和-48對應的二進制分別是11010110和11010000(補碼),即漢字中對應的二進制為1101011011010000,即16進制的D6D0,查看GBK對照表,發現16進制編碼D6D0對應的漢字確實是中
而UTF-8編碼采用3個字節儲存,同理將對應的二進制111001001011100010101101轉成16進制,為E4B8AD,通過UTF-8編碼查詢,發現漢字中對應的16進制編碼確實是E4B8AD