--- tags: c, double type, IEEE 754, jserv --- # IEEE 754複習、double與string ## 問題描述 今天看書遇到了有趣的程式如下: ```cpp #include <stdio.h> void print_bytes(void* data, size_t length){ char delim = ' '; unsigned char* ptr = data; for(size_t i = 0; i < length; i++){ printf("%c 0x%x", delim, *ptr); delim = ','; ptr++; } printf("\n"); } int main(){ int a = 9; double b = 18.9; print_bytes(&a, sizeof(int)); print_bytes(&b, sizeof(double)); return 0; } ``` 這程式的輸出是這樣的: 0x9, 0x0, 0x0, 0x0 0x66, 0x66, 0x66, 0x66, 0x66, 0xe6, 0x32, 0x40 第一個輸出沒有問題,就是一般的2補數。第二個輸出就百思不得其解,於是重新複習了一次IEEE754,也就是被規定的浮點數在電腦內部的儲存方式。 ## 如何計算IEEE754 IEEE754分成三種:32bit、64bit、128bit,不管那一種,都分成三塊: sign bit、exponent、mantissa(簡稱S, E, M) ![](https://i.imgur.com/Mp0cHII.jpg) 不同的bit數,E跟M所佔的長度也不同。 32bit底下,E佔8bit,M佔23bit; 64bit底下,E佔11bit,M佔52bit; 128bit底下,E佔15bit,M佔112bit。 現在就來針對64bit底下,如何計算IEEE754作說明,以程式中的18.9為例。 18可以寫成10010,至於0.9要寫成二進位,可以想想看: 0.9 \* 2 = 1.8 0.8 \* 2 = 1.6 0.6 \* 2 = 1.2 0.2 \* 2 = 0.4 0.4 \* 2 = 0.8 0.8 \* 2 = 1.6 ... 上面的算式,先把0.9乘以2,得到小數點左邊結果為1,右邊結果為0.8。再把 0.8乘以2,得到小數點左邊結果為1,右邊結果為0.6,再把右邊結果乘以2一直循環,直到發現重複為止。把乘以2結果的小數點左邊擷取下來,就是答案。所以,0.9寫成二進位是111001100(之後一直1100循環)。 所以,18.9 = $$(10010.111001100110011001100110011001100110011001100110)_2 =(1.0010111001100110011001100110011001100110011001100110)_2 * 2^4 $$ = $$ (1.0010111001100110011001100110011001100110011001100110)_2 * 2^{1027-1023} $$ 1023是bias,1027二進位是10000000011,就是E的部份。而M的部份,就是小數點右邊的數字。 所以18.9的二進位IEEE754表示法是 0100000000110010111001100110011001100110 0110011001100110 01100110 寫成16進位(注意第一位sign bit不要算進去)就是 0x4032E66666666666 而因為執行的電腦是little endian,所以最高位byte放在最高位記憶體位址,所以看起來是反的(最高位byte我們會寫在最左邊,最高位記憶體位址會在最右邊)。 ## double 與 string 以下,討論jserv的[2018q1 第 5 週測驗題 (中)](https://hackmd.io/@jserv/HkQjgqI5G?type=view)。 既然是二進位,那麼應該能轉成字串,我們修改一下print bytes函式: ```cpp #include <stdio.h> void print_bytes(void* data, size_t length){ char delim = ' '; unsigned char* ptr = data; for(size_t i = 0; i < length; i++){ printf("%c 0x%x", delim, *ptr); delim = ','; ptr++; } printf("\n"); ptr = data; for(size_t i = 0; i < length; i++){ putchar(*ptr); ptr++; } printf("\n"); } int main(){ int a = 9; double b = 18.9; double c[] = { 3823806048287157.0, 96 }; print_bytes(&a, sizeof(int)); print_bytes(&b, sizeof(double)); print_bytes(&c, sizeof(c)); return 0; } ``` 我們修改了print_bytes,使得除了byte以外還可以輸出byte代表的文字。這次,我們把注意力集中在main裡宣告的新陣列c。 如果執行程式,`print_bytes(&c, sizeof(c));`這行實際的輸出會是 0x6a, 0x73, 0x65, 0x72, 0x76, 0x2b, 0x2b, 0x43, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x58, 0x40 以及 jserv++CX@ 可以發現6a對應j,73對應s...以此類推,而0x0會被省略不印。 至於0x6a, 0x73, 0x65, 0x72, 0x76, 0x2b, 0x2b, 0x43其實就是3823806048287157.0(以後簡稱為`c0`)的IEEE754表示法(little endian),其實就是0x432b2b767265736a。 從剛剛`如何計算IEEE754`的說明,其實可以知道2進位的前11位,也就是16進位的前三位--0x432,掌管了指數的部分。如果我們把c0乘以2,對電腦來說,就只是把0x432+1 = 0x433,其他bit不變。 所以c0乘以2後,c0的byte如下: 0x6a, 0x73, 0x65, 0x72, 0x76, 0x2b, 0x3b, 0x43 也就是倒數第2個byte由2b變3b,3b在ascii表示`;`,所以字串會變成`jserv+;C`。 好,如果想把最後一個英文字母變成D,代表我們只能動到0x43這個byte。我們已經知道0x43是C,0x44是D,這代表著,我們需要把代表指數的0x432變成0x442。每乘一次2,指數就會加1,所以,我們要把`c0`乘以$2^{16}$,指數才會加16。 所以回到2018q1 第 5 週測驗題 (中),程式碼是 ```cpp #include <stdio.h> double m[] = { 3823806048287157.0, 96 }; void gen() { if (m[1]--) { m[0] *= 2; gen(); } else puts((char *) m); } int main() { gen(); return 0; } ``` 其實代表了`c0`乘上了$2^{96}$,所以0x432+96 = 0x432+0x60 = 0x492,0x49就是I,所以答案是jserv++I。 也可以把96換成別的數字實驗看看。 ## Reference [IEEE 754 - Standard binary arithmetic float](http://www.softelectro.ru/ieee754_en.html) [IEEE 754標準中文介紹](http://ieee-754.blogspot.com) [網頁板的32bit IEEE 754轉換](https://www.h-schmidt.net/FloatConverter/IEEE754.html) [網頁板的64bit IEEE 754轉換](http://www.binaryconvert.com/convert_double.html?) [解釋如何轉換--1](https://stackoverflow.com/questions/39840400/view-double-variable-in-memory) [解釋如何轉換--2](https://blog.xuite.net/mangohost/wretch/123102638-[轉載]用IEEE+754表示浮點數) [解釋如何轉換--3](http://jmiiv.blogspot.com/2008/08/ieee-754.html) [float跟double的差別](https://reurl.cc/e5lqLL) [2018q1 第 5 週測驗題 (中)](https://hackmd.io/@jserv/HkQjgqI5G?type=view)