--- tags: linux2022 --- # 2022q1 Homework2 (quiz2) contributed by < `StevenChou499` > * [作業需求](https://hackmd.io/@sysprog/BybKYf5xc) * [測驗題目](https://hackmd.io/@sysprog/linux2022-quiz2#2022q1-%E7%AC%AC-2-%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C) --- ## 測驗 `1` > 考慮以下對二個無號整數取平均值的程式碼: > ```c= > #include <stdint.h> > uint32_t average(uint32_t a, uint32_t b) > { > return (a >> 1) + (b >> 1) + (EXP1); > } > ``` > 我們再次改寫為以下等價的實作: > ```c= > uint32_t average(uint32_t a, uint32_t b) > { > return (EXP2) + ((EXP3) >> 1); > } > ``` ### 思考與想法 題目中的第一題因為想到要做平均,所以應該要用 `>>` 來做除以二的動作。但是因為再二進位制中,若兩數的同一位皆為 `1` 則需要進位,而只有 `&` 可以提取出兩數同時出現的 `1` ,因此 `EXP1` 應該是 `a & b` 才對。 而下一題的在再次改寫,因為後面有一個 `>> 1` ,應該是除以二的結果,而如果 `a` 與 `b` 皆需要除以二,應該以 `^` 的方式提取兩者各單獨擁有的位元,再使用 `&` 來找出兩者共同擁有的位元,而因為\兩者皆擁有此位元,所以不需要進行除以二的動作。所以 `EXP2` 為 `a & b` ,且 `EXP3` 為 `a ^ b` 。 :::success EXP1 : a & b EXP2 : a & b EXP3 : a ^ b ::: ### 延伸問題 > 比較上述實作在編譯器最佳化開啟的狀況,對應的組合語言輸出,並嘗試解讀 (可研讀 CS:APP 第 3 章) > 研讀 Linux 核心原始程式碼 include/linux/average.h,探討其 Exponentially weighted moving average (EWMA) 實作,以及在 Linux 核心的應用 ## 測驗 `2` > 改寫〈解讀計算機編碼〉一文的「不需要分支的設計」一節提供的程式碼 min,我們得到以下實作 (max): > ```c= > #include <stdint.h> > uint32_t max(uint32_t a, uint32_t b) > { > return a ^ ((EXP4) & -(EXP5)); > } > ``` ### 思考與想法 因為答案需要回傳 `a` 或是 `b` ,所以首先想到的是若要回傳 `a` 則需要利用 `^` 的特性,也就是任何數值在與 `0` 做 `EXR` 後,還會是自己本身。因此我們可以知道 `((EXP4) & -(EXP5))` 等於 `0` 。而若是需要回傳 `b` ,則可以想到 `b` 在與 `a` 做兩次 `^` 後又會回到 `b` ,因此若需要回傳 `b` ,則 `((EXP4) & -(EXP5))` 等於 `a ^ b` ,以下為兩種結果的表格: | 情況 | a > b (回傳a) | a < b (回傳b) | |:------------------:|:-------------:|:-------------:| | ((EXP4) & -(EXP5)) | 0 | a ^ b | --- 而因為需要回傳 `0` 或是 `a ^ b` 且中間還需要做 `&` 的操作,因此 `(EXP4)` 應該要等於 `a ^ b` ,且 `-(EXP5)` 在 `a > b` 時應該要等於 `0x0` ,在 `a < b` 時應該要等於 `0xFFFFFFFF` ,也就是 `-1` 。依據這樣的條件,在加上去除加上負號後的轉換, `(EXP5)` 應該要是 `0` 或 `1` ,因此 `(EXP5)` 應該為 `(a < b)` ,此時若 `a > b` ,會回傳 `0` ,加上負號後為 `0` ,與 `a ^ b` 做 `^` 後為 `0` ,而 `a ^ 0` 等於 `a` ,就會回傳 `a` 。若 `a < b` ,會回傳 `1` ,加上負號後為 `0xFFFFFFFF` ,與 `a ^ b` 做 `^` 後為 `a ^ b` ,最後則會回傳 `b`,以下為詳細講述計算過程的表格: | 情況 | a > b (回傳a) | a < b (回傳b) | |:--------------------------:|:-------------:|:-------------:| | a < b | 0 | 1 | | -(a < b) | 0x0 | 0xFFFFFFFF | | a ^ b | a ^ b | a ^ b | | (a ^ b) & -(a < b) | 0 | a ^ b | | **a ^ (a ^ b) & -(a < b)** | **a** | **b** | 因此我們可以得知 `EXP4` 等於 `a ^ b` , `EXP5` 等於 `a < b` 。 :::success EXP4 : a ^ b EXP5 : a < b ::: ### 延伸問題 ## 測驗 `3` > 考慮以下 64 位元 GCD (greatest common divisor, 最大公因數) 求值函式: > ```c= > #include <stdint.h> > uint64_t gcd64(uint64_t u, uint64_t v) > { > if (!u || !v) return u | v; > int shift; > for (shift = 0; !((u | v) & 1); shift++) { > u /= 2, v /= 2; > } > while (!(u & 1)) > u /= 2; > do { > while (!(v & 1)) > v /= 2; > if (u < v) { > v -= u; > } else { > uint64_t t = u - v; > u = v; > v = t; > } > } while (COND); > return RET; > } > ``` ### 思考與想法 因為是要求兩數之最大公因數,因此前面的程式碼: ```c=6 for (shift = 0; !((u | v) & 1); shift++) { u /= 2, v /= 2; } ``` 此階段為利用 `shift` 紀錄下總共有幾次方的 `2` 兩數可以進行整除, ```c=9 while (!(u & 1)) u /= 2; do { while (!(v & 1)) v /= 2; ``` 接著下方的程式碼則是將剩下無法成為公因數的 `2` 去除。 而最後一段的 `do while` 迴圈則可以看出是用來計算最大公因數的[輾轉相除法](https://zh.wikipedia.org/zh-tw/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95)。而[輾轉相除法](https://zh.wikipedia.org/zh-tw/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95)的結束條件為兩數字相等也就是兩者相減為 `0` ,且因為當最後兩數相等時, `do while` 迴圈中會進入 `else` 的流程,此時 `v` 會等於 `0` ,因此 `while` 結束的條件為 `v == 0` ,所以 `COND` 為 `v` ,當 `v` 不為 `0` 時代表還需要進行下一次的[輾轉相除法](https://zh.wikipedia.org/zh-tw/%E8%BC%BE%E8%BD%89%E7%9B%B8%E9%99%A4%E6%B3%95),直到 `v` 等於 `0` 為止。而因為最後要回傳兩者之最大公因數,此時之 `u` 為去除二倍數之最大公因數,所以最後還需要再利用一開始求出的 `shift` 乘回算出來的 `u` ,因此 `RET` 為 `u << shift` 。 :::success COND : v RET : u << shift ::: ### 延伸問題 ## 測驗 `4` > 改寫的程式碼如下: > ```c= > #include <stddef.h> > size_t improved(uint64_t *bitmap, size_t bitmapsize, uint32_t *out) > { > size_t pos = 0; > uint64_t bitset; > for (size_t k = 0; k < bitmapsize; ++k) { > bitset = bitmap[k]; > while (bitset != 0) { > uint64_t t = EXP6; > int r = __builtin_ctzll(bitset); > out[pos++] = k * 64 + r; > bitset ^= t; > } > } > return pos; > } > ``` > 其中第 9 行的作用是找出目前最低位元的 1,並紀錄到 t 變數。舉例來說,若目前 bitset 為 000101b,那 t 就會變成000001b,接著就可以透過 XOR 把該位的 1 清掉,其他保留 (此為 XOR 的特性)。 ### 思考與想法 因為變數 `t` 是想要將原數字中二進位最低位的 `1` 保留下來,而由二補數系統(two's complement)的定義,一數字在加上 `-` (負號)並與原本的數相加後,會因為一直進位而變成零,而最一開始進位的位置就是該數在二進位制中最低位的 `1` 。因此 `EXP6` 等於 `bitset & -bitset` ,這樣第一次進位的位置即是兩數同時擁有 `1` 的位置。 ```c 6 0110 -6 1010 + ----------- 0 (1)0000 ^ -開始進位的位置(同時都擁有1) ``` | 6 | -6 | 6 & -6 | |:----:|:----:|:------:| | 0110 | 1010 | 0010 | :::success EXP6 : bitset & -bitset ::: ### 延伸問題 ## 測驗 `5` > 以下是 LeetCode 166. Fraction to Recurring Decimal 的可能實作: > ```c= > #include <stdbool.h> > #include <stdio.h> > #include <stdlib.h> > #include <string.h> > #include "list.h" > > struct rem_node { > int key; > int index; > struct list_head link; > }; > > static int find(struct list_head *heads, int size, int key) > { > struct rem_node *node; > int hash = key % size; > list_for_each_entry (node, &heads[hash], link) { > if (key == node->key) > return node->index; > } > return -1; > } > > char *fractionToDecimal(int numerator, int denominator) > { > int size = 1024; > char *result = malloc(size); > char *p = result; > > if (denominator == 0) { > result[0] = '\0'; > return result; > } > > if (numerator == 0) { > result[0] = '0'; > result[1] = '\0'; > return result; > } > > /* using long long type make sure there has no integer overflow */ > long long n = numerator; > long long d = denominator; > > /* deal with negtive cases */ > if (n < 0) > n = -n; > if (d < 0) > d = -d; > > bool sign = (float) numerator / denominator >= 0; > if (!sign) > *p++ = '-'; > > long long remainder = n % d; > long long division = n / d; > > sprintf(p, "%ld", division > 0 ? (long) division : (long) -division); > if (remainder == 0) > return result; > > p = result + strlen(result); > *p++ = '.'; > > /* Using a map to record all of reminders and their position. > * if the reminder appeared before, which means the repeated loop begin, > */ > char *decimal = malloc(size); > memset(decimal, 0, size); > char *q = decimal; > > size = 1333; > struct list_head *heads = malloc(size * sizeof(*heads)); > for (int i = 0; i < size; i++) > INIT_LIST_HEAD(&heads[i]); > > for (int i = 0; remainder; i++) { > int pos = find(heads, size, remainder); > if (pos >= 0) { > while (PPP > 0) > *p++ = *decimal++; > *p++ = '('; > while (*decimal != '\0') > *p++ = *decimal++; > *p++ = ')'; > *p = '\0'; > return result; > } > struct rem_node *node = malloc(sizeof(*node)); > node->key = remainder; > node->index = i; > > MMM(&node->link, EEE); > > *q++ = (remainder * 10) / d + '0'; > remainder = (remainder * 10) % d; > } > > strcpy(p, decimal); > return result; > } > ``` ### 思考與想法 程式碼講解: 因為其程式碼有一定的長度,因此需要經過 `trace code` 再做完整的判斷會比較好。 先從程式開始執行的第24行開始: ```c=24 char *fractionToDecimal(int numerator, int denominator) { int size = 1024; char *result = malloc(size); char *p = result; if (denominator == 0) { result[0] = '\0'; return result; } if (numerator == 0) { result[0] = '0'; result[1] = '\0'; return result; } /* using long long type make sure there has no integer overflow */ long long n = numerator; long long d = denominator; /* deal with negtive cases */ if (n < 0) n = -n; if (d < 0) d = -d; ``` 此段是在配置足夠的記憶體來存取在做運算後的小數點,並先將分母以及分子為零的狀況排除。 接著是第55行: ```c=55 long long remainder = n % d; long long division = n / d; sprintf(p, "%ld", division > 0 ? (long) division : (long) -division); if (remainder == 0) return result; ``` 此段主要是先計算出該數的商數與餘數,接著利用 `sprintf` 將 `division` 存入 `%ld` 再轉成 `char` 存入 `p` 。 第62行: ```c=62 p = result + strlen(result); *p++ = '.'; /* Using a map to record all of reminders and their position. * if the reminder appeared before, which means the repeated loop begin, */ char *decimal = malloc(size); memset(decimal, 0, size); char *q = decimal; ``` 此段是將小數點加入 `p` ,再定義一個存放小數的變數 `decimal` ,配置空間後再用 `memset` 轉換其全部內容成 `0` 。 第72行: ```c=72 size = 1333; struct list_head *heads = malloc(size * sizeof(*heads)); for (int i = 0; i < size; i++) INIT_LIST_HEAD(&heads[i]); ``` 此段開始要真正做小數運算的動作,先重新定義 `size` 為 `1333` ,接著定義一個 `heads` 並配置記憶體,在利用 `INIT_LIST_HEAD` ,依序進行初始化。 第77行: ```c=77 for (int i = 0; remainder; i++) { int pos = find(heads, size, remainder); if (pos >= 0) { while (PPP > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; } ``` 接著利用 `for` 迴圈開始進行計算,透過呼叫 `find` 函式找出是否有相同的 `key` ,若有則回傳他的 `index` ,若無則回傳 `-1` 。 * `find` 函式: ```c=13 static int find(struct list_head *heads, int size, int key) { struct rem_node *node; int hash = key % size; list_for_each_entry (node, &heads[hash], link) { if (key == node->key) return node->index; } return -1; } ``` 而獲得 `pos` 後,變可以印出小數點的數值,並回傳 `result` 。 第89行: ```c=89 struct rem_node *node = malloc(sizeof(*node)); node->key = remainder; node->index = i; MMM(&node->link, EEE); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; ``` 若沒有找到相同的 `key` ,則需要將其 `remainder` 與 `i` 記錄下來,並將其存入配置圍成的 `struct list_head*` 中,接著最後兩行是存入當前餘數 `remainder` 的數值至 `q` 與更新 `remainder` 的數值。此處的 `+ '0'` ,其實為加上 `48` ,將餘數算出後型態從 `long long` 轉換至 `char` 。 以下為解釋(以 `remainder = 3, d = 6` 為例): ```c (remainder * 10) / d + '0'; 3 * 10 / 6 + '0'; 30 / 6 + '0'; 5 + '0'; 5 + 48; 53 = '5'; // 利用 + '0' 將int轉換成char ``` 0~9 之ASCII編碼: | Binary | Decimal | Character | |:-------:|:-------:|:---------:| | 0110000 | 48 | 0 | | 0110001 | 49 | 1 | | 0110010 | 50 | 2 | | 0110011 | 51 | 3 | | 0110100 | 52 | 4 | | 0110101 | 53 | 5 | | 0110110 | 54 | 6 | | 0110111 | 55 | 7 | | 0111000 | 56 | 8 | | 0111001 | 57 | 9 | 由上述的運作情況,我們可以知道 `PPP` 為 `pos--` ,因為 `pos` 為記住小數點後不重複的位數,因此當 `pos` 等於零時,代表不重複的小數已經計算完了。而在後面的 `MMM` 與 `EEE` 則分別是 `list_add` 與 `&heads[remainder % size]` 。此處的用意是將餘數與位置放入 `struct list_head` 的結構 `node*` 當中,再利用 `list_add` 加入 `head[remainder % size]` 中。 整個運作上其實為一個雜湊表,透過雜湊表找出循環與非循環的小數。 以下為實際運作的示意圖(以分母為 `90` ,分子為 `102` 為例): * 找出第一次運算下的商數以及餘數 (102 / 90 = 1 ... 12) ```c long long n = numerator; // n = 102 long long d = denominator; // d = 90 long long remainder = n % d; // remainder = 12 long long division = n / d; // division = 1 ``` * 將商數存入 `result` 並同時加入小數點 ```c sprintf(p, "%ld", division > 0 ? (long) division : (long) -division); // p = result = 1 if (remainder == 0) return result; p = result + strlen(result); *p++ = '.'; // result = 1. ^ ^ result的位置 p的位置 ``` * 初始化 `decimal` 並準備存入小數 ```c char *decimal = malloc(size); memset(decimal, 0, size); char *q = decimal; // q = decimal = '000000000000000...' ``` * 建立一雜湊表並初始化 ```c size = 1333; struct list_head *heads = malloc(size * sizeof(*heads)); for (int i = 0; i < size; i++) INIT_LIST_HEAD(&heads[i]); ``` ```graphviz digraph INIT { rankdir = "LR" subgraph h1332{ heads1332[label=" {<p1332>prev|<f1332>heads[1332]|<n1332>next}", shape=record, fixedsize=false,width=3] heads1332:p1332 -> heads1332:f1332 heads1332:n1332 -> heads1332:f1332 } omit[label = ".\n.\n.\n",shape = none] subgraph h2{ heads2[label=" {<p2>prev|<f2>heads[2]|<n2>next}", shape=record, fixedsize=false,width=3] heads2:p2 -> heads2:f2 heads2:n2 -> heads2:f2 } subgraph h1{ heads1[label=" {<p1>prev|<f1>heads[1]|<n1>next}", shape=record, fixedsize=false,width=3] heads1:p1 -> heads1:f1 heads1:n1 -> heads1:f1 } subgraph h0{ heads0[label=" {<p0>prev|<f0>heads[0]|<n0>next}", shape=record, fixedsize=false,width=3] heads0:p0 -> heads0:f0 heads0:n0 -> heads0:f0 } } ``` * 進入第一個 `for` 迴圈 (i = 0): ```c for (int i = 0; remainder; i++) { int pos = find(heads, size, remainder); if (pos >= 0) { while (pos-- > 0) *p++ = *decimal++; *p++ = '('; while (*decimal != '\0') *p++ = *decimal++; *p++ = ')'; *p = '\0'; return result; } struct rem_node *node = malloc(sizeof(*node)); node->key = remainder; node->index = i; list_add(&node->link, &heads[remainder % size]); *q++ = (remainder * 10) / d + '0'; remainder = (remainder * 10) % d; } ``` `pos` 回傳為 `-1` ,引此不會進入 `if` 條件式,並且把 `remainder` 與 `i` 加入 `node*` ,並透過 `list_add` 加入 `heads[remainder % size]` ,即為 `heads[12]` 。 ```graphviz digraph INIT { rankdir = "LR" subgraph h1332{ heads1332[label=" {<p1332>prev|<f1332>heads[1332]|<n1332>next}", shape=record, fixedsize=false,width=3] heads1332:p1332 -> heads1332:f1332 heads1332:n1332 -> heads1332:f1332 } omit1[label = ".\n.\n.\n",shape = none] subgraph h12{ heads12[label=" {<p12>prev|<f12>heads[12]|<n12>next}", shape=record, fixedsize=false,width=3] heads12:p12 -> heads12:f12 } omit[label = ".\n.\n.\n",shape = none] subgraph h0{ heads0[label=" {<p0>prev|<f0>heads[0]|<n0>next}", shape=record, fixedsize=false,width=3] heads0:p0 -> heads0:f0 heads0:n0 -> heads0:f0 } node1[label="{<prev>prev|<node>node|<next>next}|{<index>index = 0|<key>key = 12}", shape = record, width = 1] heads12:n12 -> node1:n } ``` 加入雜湊表後,接著將餘數記住並加入 `q` 中,並且重新計算下一次的餘數 ```c *q++ = (remainder * 10) / d + '0'; // q = 1 remainder = (remainder * 10) % d; // remainder = 30 ``` * 再次進入下一個 `for` 迴圈 (i = 1): 進入 `for` 迴圈後, `pos` 會回傳 `-1` ,接著再次加入 `heads[30]` 中。 ```graphviz digraph INIT { rankdir = "LR" subgraph h1332{ heads1332[label=" {<p1332>prev|<f1332>heads[1332]|<n1332>next}", shape=record, fixedsize=false,width=3] heads1332:p1332 -> heads1332:f1332 heads1332:n1332 -> heads1332:f1332 } omit2[label = ".\n.\n.\n",shape = none] subgraph h30{ heads30[label=" {<p30>prev|<f30>heads[30]|<n30>next}", shape=record, fixedsize=false,width=3] heads30:p30 -> heads30:f30 } omit1[label = ".\n.\n.\n",shape = none] subgraph h12{ heads12[label=" {<p12>prev|<f12>heads[12]|<n12>next}", shape=record, fixedsize=false,width=3] heads12:p12 -> heads12:f12 } omit[label = ".\n.\n.\n",shape = none] subgraph h0{ heads0[label=" {<p0>prev|<f0>heads[0]|<n0>next}", shape=record, fixedsize=false,width=3] heads0:p0 -> heads0:f0 heads0:n0 -> heads0:f0 } node1[label="{<prev>prev|<node>node|<next>next}|{<index>index = 0|<key>key = 12}", shape = record, width = 1] node2[label="{<prev>prev|<node>node|<next>next}|{<index>index = 1|<key>key = 30}", shape = record, width = 1] heads12:n12 -> node1:n heads30:n30 -> node2:n } ``` 接著再次紀錄商數至 `q` ,並刷新餘數。 ```c *q++ = (remainder * 10) / d + '0'; // q = 1.3 remainder = (remainder * 10) % d; // remainder = 30 ``` * 第三次進入 `for` 迴圈(i = 2): 進入 `for` 迴圈後, `find` 函式會進入 `heads[30]` 尋找相同的 `key` ,找到相同的 `key` 後,便會回傳 `1` 給 `pos` 。接著進入 `if` 判斷式,將 `decimal` 的內容複製給 `p` ,因為 `pos` 等於 `1` ,所以只會將 `decimal` 的第一位的 `1` 複製給 `p` ,此時 `result` 為 `1.1` 。加上 `(` 並複製剩下的內容直到遇到 `\0` ,最後在加上 `)\0` 即可回傳。 ```c int pos = find(heads, size, remainder); // pos = 1 if (pos >= 0) { while (PPP > 0) *p++ = *decimal++; // result = 1.1 *p++ = '('; // result = 1.1( while (*decimal != '\0') *p++ = *decimal++; // result = 1.1(3 *p++ = ')'; // result = 1.1(3) *p = '\0'; // result = 1.1(3)\0 return result; ``` :::success PPP : pos-- MMM : list_add EEE : &heads[remainder % size] ::: ## 測驗 `6` > \_\_alignof\_\_ 是 GNU extension,以下是其可能的實作方式: > ```c > /* > * ALIGNOF - get the alignment of a type > * @t: the type to test > * > * This returns a safe alignment for the given type. > */ > #define ALIGNOF(t) \ > ((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X) > ``` ### 思考與想法 其程式碼講解: 先定義一個結構體其中擁有 `char c` 與 `t _h` 。 ```c struct { char c; t _h; } ``` 再將其結構之起始位置設定為 `0` 。 ```c (struct { char c; t _h; } *)0 ``` 接著找出其結構中元素 `_h` 的位置。 ```c ((char *)(&((struct { char c; t _h; } *)0)->M) ``` 最後與 `X` 的位置相減。 ```c ((char *)(&((struct { char c; t _h; } *)0)->M) - (char *)X) ``` 因為 `__alignof__` 就是計算 `type t` 的長度,所以 `M` 就是需要求的 `_h` ,而 `X` 就是 `0` 。 以 `_h` 是 `int` 為例: ```c #include <stdio.h> #define ALIGNOF(t) \ ((char *)(&((struct { char c; t _h; } *)0)->_h) - (char *)0) int main(void) { printf("%ld\n", ALIGNOF(int)); return 0; } ``` 其結構實際樣貌: ```graphviz digraph INIT { rankdir = "LR" subgraph mem{ memforint[label=" {<m0>char|<m1>|<m2>|<m3>|<m4>int|<m5>int|<m6>int|<m7>int}", shape=record, fixedsize=fasle, width=5] } } ``` ```c 0:定義結構的起始位置為0 | v | | | | | | | | | {char} { int } ^ ^ | | 0:char的起始位置 4:int的起始位置 ``` 其執行結果: ![](https://i.imgur.com/B87jdHU.png) :::success M : _h X : 0 ::: ## 測驗 `7` > 考慮貌似簡單卻蘊含實作深度的 FizzBuzz 題目: > * 從 1 數到 n,如果是 3的倍數,印出 “Fizz” > * 如果是 5 的倍數,印出 “Buzz” > * 如果是 15 的倍數,印出 “FizzBuzz” > * 如果都不是,就印出數字本身 > > 以下是利用 bitwise 和上述技巧實作的 FizzBuzz 程式碼: ( `bitwise.c` ) > ```c > static inline bool is_divisible(uint32_t n, uint64_t M) > { > return n * M <= M - 1; > } > > static uint64_t M3 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 3 + 1; > static uint64_t M5 = UINT64_C(0xFFFFFFFFFFFFFFFF) / 5 + 1; > > int main(int argc, char **argv) > { > for (size_t i = 1; i <= 100; i++) { > uint8_t div3 = is_divisible(i, M3); > uint8_t div5 = is_divisible(i, M5); > unsigned int length = (2 << KK1) << KK2; > > char fmt[9]; > strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length); > fmt[length] = '\0'; > > printf(fmt, i); > printf("\n"); > } > return 0; > } > ``` ### 思考與想法 因為題目要求依據各數字是否為 `3` 或 `5` 的倍數去做變化,而若只是 `3` 或是 `5` 的倍數只需要印出 `Fizz` 或是 `Buzz` ,所以我們可以知道 `length` 在皆否的情況下為 `2` ,只為其中一數之倍數為 `4` ,同時為兩數之倍數的話為 `8` ,而剛好 `2 << 1` 等於 `4` 且 `2 << 2` 等於 `8` 。因此我們可以知道 `KK1` 與 `KK2` 分別為 `div3` 和 `div5` ,也可以互換,因為不管先後都會作到 `<<` 的動作。 | 情況 | `3` 的倍數 | `5` 的倍數 | `15` 的倍數 | 皆不是 | |:---------------:|:----------:|:----------:|:-----------:|:------:| | `length` 的數值 | 4 | 4 | 8 | 2 | 搞定 `length` 的問題後,後面的 `strncpy` 則是依據不同情況去複製不同位置以及大小之字串。若是 `3` 或是同時是 `3` 與 `5` 的倍數,則要從 `0` 的位置開始複製,若是單純 `5` 的倍數,要從 `4` 的位置開始複製,而若兩數皆不是的話則要從 `8` 的位置開始複製。 | 情況 | `3` 的倍數 | `5` 的倍數 | 同時為 `3` 與 `5` 的倍數 | 皆不是 | |:--------------:|:----------:|:----------:|:------------------------:|:------:| | 開始複製之位置 | 0 | 4 | 0 | 8 | ```c strncpy(fmt, &"FizzBuzz%u"[(9 >> div5) >> (KK3)], length); ``` 而因為 `div5` 已經決定好了,所以若是單純為 `5` 的倍數, `9 >> 5` 等於 `4` ,剛好是開始複製的位置,此時 `KK3` 應該要為 `0` 。而若是 `3` 的倍數或是 `15` 的倍數,則需要一直位移直到數值為 `0` ,便可以從起始位置開始複製,所以 `KK3` 應該是 `div3 << 2` ,這樣在單純只為 `3` 的倍數的情況下,也可以讓 `9` 位移 `4` 次,變成 `0` 的結果。 但是當兩數皆不為倍數時,其值為 `9` ,此時只會印出 `u` 的結果,與 `8` 的結果不符,因此 `9` 應該改為 `8` ,這樣不會影響原本推導的結果,又可以讓兩數皆不為倍數的情況下複製並輸出正確的結果。 ```c strncpy(fmt, &"FizzBuzz%u"[(8 >> div5) >> (KK3)], length); ``` :::success KK1 : div3 KK2 : div5 KK3 : div3 << 2 :::