# 淺談計算機編碼(整數篇) ###### tags: basic > 計算機中對於數字不管是十進位、二進位、十六進位、整數、浮點數等等的表達方式或運算操作,因為計算機本身的限制,都會跟人類世界直覺想像的有點不同,常常會因為某些觀念上的不齊全,直接用我們直覺的方式去撰寫就會有些無法預期的漏洞,所以熟悉跟將這些編碼化為自己的習慣是相當重要的。 > > 這篇的撰寫是根據 CS:APP 這本書的第二章作為基礎,算是作為自己讀後彙整的筆記,並會搭配些許實驗。 ## 實驗環境 ```shell $ uname -a Linux noah-UX330UA 4.15.0-43-generic #46~16.04.1-Ubuntu SMP Fri Dec 7 13:31:08 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux $ gcc -version gcc (Ubuntu 5.5.0-12ubuntu1~16.04) 5.5.0 20171010 ``` ### 16 進位表示 在計算機中最小的位址紀錄單元為 8 個 byte,而如果用二進位表示會太長,用十進位轉換成二進位又會太麻煩,所以都是用十六位元來表示位址。 在 C 語言中用 `0xFFFFFFFF` 的格式來表示記憶體位置,並且可以用`printf("%p", 變數);` 來印出記憶體表示之格式。 以下是三種進位的轉換表。 | 十六進位 |0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ------- | - | - | - | - | - | - | - | - | - | 十進位 | 0 | 1| 2 | 3 | 4| 5 | 6 | 7 | 8 | 二進位 | 0000| 0001| 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 十六進位 |9 | A | B | C | D | E | F | | ------- | - | - | - | - | - | - | - | | 十進位 | 9 | 10| 11 | 12 | 13| 14 | 15 | | 二進位 | 1001| 1010| 1011 | 1100 | 1101 | 1110 | 1111 ### byte 表示順序 byte 的表示順序根據計算機的不同分成兩種 `little endian` 和 `big endian` (大部分的機器都是以 `little endian` 作為表示,而網路傳輸使用的都是 `big endian`)。 * `little endian` 在 `little endian` 中,記憶體位置最低位數(最右)會放在記憶體的前面,例如 `0x01234567`。 | 0x100 | 0x101 | 0x102 | 0x103 | | -------- | -------- | -------- |---- | 67 | 45 | 23 | 01 | * `big endian` 在 `big endian` 中,跟 `little endian` 相反,最高位數放在記憶體最前面。 | 0x100 | 0x101 | 0x102 | 0x103 | | -------- | -------- | -------- |---- | 01 | 23 | 45 | 67 | * [Big-Endian 與Little-Endian 的差異與判斷程式碼- G. T. Wang](https://blog.gtwang.org/programming/difference-between-big-endian-and-little-endian-implementation-in-c/) 可以利用這個連結給的小程式來判斷自己電腦的表示式是哪種。 ## 整數類型 在 C 語言中有許多不同的整數的類型,而這些類型在不同 bits 的機器上會有不一樣的最大值與最小值,下面的列表是在 32 位元跟 64 位元機器上值的範圍。 #### 32位元 | 整數類型 | 最小值 | 最大值 | | ------------- | -------- | ------------ | | [signed] char | -128 | 127 | | unsigned char | 0 | 255 | | short | -32768 | 32767 | | unsigned short | 0 | 65535 | | int | -2147483648 | 2147483647 | | unsigned | 0 | 4294967295 | | long | -2147483648 | 2147483647 | | unsigned long | 0 | 4294967295 | | int32_t | -2147483648 | 2147483647 | | uint32_t | 0 | 4294967295 | | int64_t | -9223372036854775808 | 9223372036854775807| | uint64_t | 0 | 18446744073709551615 | #### 64位元 | 整數類型 | 最小值 | 最大值 | | ------------- | -------- | ------------ | | [signed] char | -128 | 127 | | unsigned char | 0 | 255 | | short | -32768 | 32767 | | unsigned short | 0 | 65535 | | int | -2147483648 | 2147483647 | | unsigned | 0 | 4294967295 | | long |-9223372036854775808 | 9223372036854775807 | | unsigned long | 0 | 18446744073709551615 | | int32_t | -2147483648 | 2147483647 | | uint32_t | 0 | 4294967295 | | int64_t | -9223372036854775808 | 9223372036854775807| | uint64_t | 0 | 18446744073709551615 | ## 整數表示 整數在計算中有兩種表示方式,一種是只能表示非負數,另外一種是能夠表示負數、零、正數,這兩種表示方式在編碼上都有些不同,其在不同位元的機器上也有不同,接下來就來介紹這兩種整數的編碼方法。 #### 無符號整數二進位編碼 $B2U_w(x) = \displaystyle\sum_{i=0}^{w-1}x_i2^i$ 上面的公式是作為將二進位轉成無符號整數的公式。 $B2U_w([1011]) = 1 *2^3 + 0 * 2^2 + 1 * 2^1 + 1 *2^0 = 11$ #### 補數編碼(有符號整數表示法 two's complement) 利用二進位數中的最高位數來表示,如果是 `1` 就為負,`0` 則為正 $B2U_w(x) = -x_{w-1}2^{w-1}+\displaystyle\sum_{i=0}^{w-2}x_i2^i$ $B2U_w([1011]) = -1 *2^3 + 0 * 2^2 + 1 * 2^1 + 1 *2^0 = -8$ $B2U_w([0111]) = 0 *2^3 + 1 * 2^2 + 1 * 2^1 + 1 *2^0 = 7$ ## 有號整數與無號整數之間轉換 > 所謂的有號和無號的轉換並不是在一般數學所表示的將 `-` 去掉就轉換成功,這兩個在編碼上就完全不同,無號是用二進位,有號是用 two's complement ,所能表示數字的範圍大小也不同,所以再做 `int -> unsigned` 或 `unsigned -> int` 時必須要意識到自己在幹麻,還有結果是否會是自己預期。 * **有號整數 -> 無號整數 轉換原理** * 當要從有號整數轉成無號整數時,如果有號整數大於零的時候,就直接轉換成無號整數。 * 如果為負數要轉換為無號整數時,$x + 2^w$ 利用這個公式來計算,$w$ 為數據類型 bits 長。 * 例如在 16 bits 的機器上,要將 $-12345$ 轉換成無號整數,套用上述公式... $-12345 + 2^{16} = 53191$ * 特別注意到因為這樣轉換的方法,將 -1 轉換成無號整數後會變成在該機器上的無號整數最大值... $-1 + 2^{16} = 65535$ * **無號整數 -> 有號整數 轉換原理** * 如果欲轉換的無號整數是小於有號整數的最大值(`參照整數類型的表格`),就直接轉換。 * 如果是大於有號整數的最大值,套用 $x-2^w$ 的公式,$x$ 為無號整數,$w$ 為機器的 bits 數。 * 例如在 16 bits 的機器上, 給一數 $32768$ 為有號整數最大值再加一,套用公式就為 $32768 - 2^{16} = -32768$ ,這樣轉換下來就會得到有號整數的最小值。 * 如果是給無號整數的最大值 $65535$ 代入公式,$65535 - 2^{16} = -1$ 跟 *有號整數 -> 無號整數* 成對射。 ![](https://i.imgur.com/ceSe9Qr.png) 這張圖能清楚的表示兩種表示方式的轉換。 ## 整數運算 #### 無號整數加法 * 在加法的部份,因為類型能夠表達的數字大小有其範圍,如果兩者相加的數超過其範圍,多出來的部份就會在從 0 開始向上加。 ```clike #include <stdio.h> #define UN_MAX = 4294967295 int main() { unsigned number_one = UN_MAX; unsigned number_two = 2; unsigned sum = number_one + number_two; printf("%u\n", sum); return 0; } ``` * 用上面簡單的程式來做個範例,宣告 `unsigned` 變數,而 `unsigned` 的最大值為 $4294967295$,將 `number_one` 宣告為最大值,再加 $2$,得到結果並不是預期的 $4294967297$,結果是 $1$,這代表此結果產生 `overflow` ![](https://i.imgur.com/SJs0D3V.png) * 上圖是以 4 位元的無符號整數作為範例,在 4 位元的數其最大值為 15 , z 軸為平面 x 軸與 y 軸的總和,可以發現在當其兩軸總和超過 15 時就會從 0 開始繞回去往上加。 #### 有號整數加法 * 有號整數的 `overflow` 會有兩種,超出正值範圍的 `overflow` 或超出負值範圍 `overflow` * 如果 $x+y \geq 2^{w-1}$,其得到的結果就會是 $x+y-2^w$ ($2^{w-1}$ 是此類型最大值範圍),這樣為 `Positive overflow`。 * 如果 $x + y \leq -2^{w-1}$,其得到的結果就會是 $x+y+2^w$ ($-2^{w-1}$ 是此類型最小值範圍),這樣為 `negative overflow` * 實驗 ```clike #include <stdio.h> #define INT_MAX 2147483647 #define INT_MIN -2147483648 int main() { int number_one = INT_MAX; int number_two = 1; int sum = number_one + number_two; printf("INT_MAX + 1 = %d\n", sum); number_one = INT_MIN; number_two = -1; sum = -1 + INT_MIN; printf("-1 + INT_MIN = %d\n", sum); return 0; } ``` ```shell INT_MAX + 1 = -2147483648 -1 + INT_MIN = 2147483647 ``` 從上面的小實驗可以看出,當在其類型最大值再加一時,得到的值會是其最小值,而相反的最小值再加負一所得到的值為最大值。 ![](https://i.imgur.com/lCPWEAK.png) 從上圖就可以看出其 `Positive overflow` 得到的值,會從最小範圍上加,而 `Negative overflow` 則是從最大值開始下減。 :::info **延伸閱讀** 1. [解讀計算機編碼](https://hackmd.io/s/rylUqXLsm#) ::: > Reference > [Big-Endian 與Little-Endian 的差異與判斷程式碼- G. T. Wang](https://blog.gtwang.org/programming/difference-between-big-endian-and-little-endian-implementation-in-c/) > [Bits,Bytes,and Integers CMU CS:APP](http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/02-03-bits-ints.pdf)