---
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)

不同的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)