# 2024q1 Homework5 (assessment)
contributed by < `kevinzxc1217` >
## 閱讀〈[因為自動飲料機而延畢的那一年](https://blog.opasschang.com/the-story-of-auto-beverage-machine-1/)〉
>所有的決策都要有事實做依據,我們把過去飲料店一整年每一天的冰塊使用量轉成圖表,用程式畫出來,下圖是在2015/1/13當天飲料店的冰塊用量,底下的橫軸是一天當中不同的時間,藍色的是該時段的銷售杯數。用杯數回推冰塊用量,紅色的折線圖就是冰塊存量,如果一段時間沒有使用會緩慢回升,但如果冰塊用量大增會迅速掉下去。
「所有的決策都要有事實做依據」,我很喜歡作者的這句話,不管是在寫程式還是創業,做任何決策時不應該是憑感覺,而是提出證據證明所做的決定是否有所依據。
> 於是我便重複「加冰塊、倒冰塊、測量訊號、紀錄」,做了超過480次,很無聊想看實驗紀錄的人可以點這裡。
最後得出了一張漂亮的分佈圖,X軸是訊號大小,Y軸是落入秤上的冰塊實際重量,用最小平方法找出回歸直線之後,就可以給定訊號,預測最有可能的冰塊重量,而95%的冰塊誤差會落在30g之內,大約是不到兩顆冰塊的誤差,在可接受的範圍內。
我發現,身處的每個角落似乎都是校園學習的寫照。我原本僅認為創業可能僅在某些技術上需要程式控制,但事實上,在各種不同的領域上,也可以透過程式進行實驗分析,來達到更好的評估效果。
當我看到作者整理的分佈圖時,它彷彿是我寫作業時為了分析程式效能而進行的實驗,但現在卻應用在測量冰塊用量上,這就是學以致用吧,看到如此熟悉的圖表分析,顯然是上過Jserv老師課程的學生。
> 當初那個天真的少年,他努力花費了一年,最後做出的機器依然不夠穩定,而且因為要當兵無法繼續維護,沒辦法送到飲料店裡,最後只好把機器拆解送到倉庫內,如果我們提早告訴他結局,他還會願意走這麼一遭嗎?
對比14個月前的天真,他恐怕完全想像不到會變成現在這個樣子吧。
他還是會在其他人都跑去讀研究所時選擇延畢嗎?
歷經無數個自我懷疑的夜晚,
歷經無數次的輾轉反側,
歷經無數與失敗與挫折,
歷經無數朋友的幫忙,
用一年的時間換一個夢,
我只想說:「謝謝你們,一路陪我到這裡。」
最後,雖然這篇故事並非典型的創業故事,光彩照人的結局並不總是必然的。然而,我相信作者真正獲得的是在這個過程中所經歷到的成長和體悟,這些經歷都是作者這輩子無可取代的養分。期許自己未來遇到困難時,能夠秉持著作者鍥而不捨的精神,不斷的嘗試,克服重重難關。
## 學習本課程 5 週之後的感想
這門課的獨特之處在於作業是公開的,這不僅在找工作時能夠證明自己做過哪些內容,同時也讓我有機會觀摩其他同學的作業內容。透過觀察,我發現每個人對於同一個問題的解決方法有很大的差異,也因此意識到自己與一些優秀同學的能力差距,這促使我更加努力地學習。
過去,我的程式撰寫可能只是達到功能運作即可,但透過這門課的學習,我學到了更多提高效率的方法。例如將 Recursion 改為 Tail recursion ,可以減少遞迴所需的記憶體空間和運行時間;或是透過指標的指標去進行優化等等。
特別是在做每周的測試題目時,需要閱讀較為複雜的程式碼,可以從中培養閱讀程式的速讀以及理解能力,並且在後續檢討測驗題目時,可以從中學習其理論和思維以及撰寫程式的技巧。
對於之前學習到的排序算法 merge sort 是我當時認知最快的方法,但在實際比較中卻輸給了 Linux 核心內部的 list sort,課程中探討了兩者的差別以及 list sort 之所以更快的原因,除了學習更快的算法之外,這堂課程對於數學推導也是非常重視,每個程式背後都有其數學原理,也讓我意識到數學在寫程式中的重要性。
整體而言,這門課程讓我受益匪淺,學到了許多新的知識,也從中看認識了自己,知道自己的不足。
## 研讀第 1 到第 6 週「[課程教材](https://wiki.csie.ncku.edu.tw/linux/schedule)和 [CS:APP 3/e](https://hackmd.io/@sysprog/CSAPP/https%3A%2F%2Fhackmd.io%2Fs%2FSJ7V-qikG)」
### [你所不知道的 C 語言:linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80-linked-list-%E5%92%8C%E9%9D%9E%E9%80%A3%E7%BA%8C%E8%A8%98%E6%86%B6%E9%AB%94)
#### 從 Linux 核心的藝術談起
> [Linus Torvalds 在 TED 2016 的訪談](https://www.ted.com/talks/linus_torvalds_the_mind_behind_linux?language=zh-tw)
這裡提到使用指標的指標來處理程式的例外,將原本的程式 (10行),改成有「品味」的版本 (4行)。
```c
void remove_list_node(List *list, Node *target)
{
Node *prev = NULL;
Node *current = list->head;
// Walk the list
while (current != target) {
prev = current;
current = current->next;
}
// Remove the target by updating the head or the previous node.
if (!prev)
list->head = target->next;
else
prev->next = target->next;
}
```
剛開始在寫 [2024q1 Homework1 (lab0)](https://hackmd.io/zkT6lHFOTkCrXcKFGegbvA?view) 時,裡面有一部分是要我們針對指定的佇列進行開發,使用鏈結串列的資料結構去完成指定的函式,其中我使用到了許多指標進行開發,就如上方的程式類似,雖然易懂,但仍可透過**指標的指標**去進行優化。
```c
void remove_list_node(List *list, Node *target)
{
// The "indirect" pointer points to the *address*
// of the thing we'll update.
Node **indirect = &list->head;
// Walk the list, looking for the thing that
// points to the node we want to remove.
while (*indirect != target)
indirect = &(*indirect)->next;
*indirect = target->next;
}
```
逐行分析以上程式:
```c
Node **indirect = &list->head;
```
宣告一個指標的指標 `**indirect` ,指標的指標代表該指標僅能去指向指標。 `list->head` 代表 `list` 去指向一個 `List` 結構,取出名為 `head` 的成員,而 `head` 本身是一個指標,所以 `&list->head` 就是將 `head` 的地址取出來,即為指標的指標。
```c
indirect = &(*indirect)->next;
```
由於 `**indirect` 是指向 `head` ,所以 `*indirect` 就是去指向 `head` 所指向的 `nodeA` ,而 `(*indirect)->next` 就是指向 `nodeB` 本身。而 `&(*indirect)->next` 則是 `nodeA` 中,指向 `nodeB` 的 `next` 指標,所以最後賦予給 `indirect` ,就代表將其指向 `nodeA` 中的 `next` 指標,仍為指標的指標。
```graphviz
digraph node_t
{
node [shape= "record"];
rankdir= "LR";
// splines = false
head [shape= plaintext,label= "head"]
indirect_ptr [shape= plaintext,label= "indirect ptr"]
node1 [label= "{<self>A | <n>next}"]
node2 [label= "{<self>B | <n>next}"]
node3 [label= "{<self>C | <n>next}"]
NULL [shape= plaintext]
// {rank = "min" list_head}
// list_head -> node1 -> node3->NULL[
// weight = 100, style=invis
// ]
indirect_ptr:n->head
head:n->node1
node1:n->node2
node2:n->node3
node3:n->NULL
}
```
```graphviz
digraph node_t
{
node [shape= "record"];
rankdir= "LR";
// splines = false
head [shape= plaintext,label= "head"]
indirect_ptr [shape= plaintext,label= "indirect ptr"]
node1 [label= "{<self>A | <n>next}"]
node2 [label= "{<self>B | <n>next}"]
node3 [label= "{<self>C | <n>next}"]
NULL [shape= plaintext]
// {rank = "min" list_head}
// list_head -> node1 -> node3->NULL[
// weight = 100, style=invis
// ]
indirect_ptr:n->node1:n
head:n->node1
node1:n->node2
node2:n->node3
node3:n->NULL
}
```
```c
while (*indirect != target)
```
由於 `indirect` 是指向 `nodeA` 內的 `next` ,則 `*indirect` 則是表示指向 `nodeB` 本身,而 `taget` 本身是指標型態,假設指向串列中的 `nodeB` ,可透過和 `*indirect` 的比較來確定是否為我們想要移除的節點。
```c
*indirect = target->next;
```
當 `indirect` 走訪到我們要的位置後,可以透過改變 `*indirect` 指向的位址,即該節點的 `next` 為何,來決定我們要跳過哪個節點。這裡將 `target->next` 賦予給 `*indirect` ,即表示當前 `nodeA` 的 `next` 指標,將指向 `target->next` ,即 `nodeC` ,透過以上步驟就可以完成 `remove` 。
---
### [你所不知道的 C 語言:數值系統篇](https://hackmd.io/@sysprog/c-numerics#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E6%95%B8%E5%80%BC%E7%B3%BB%E7%B5%B1%E7%AF%87)
#### 算術完全可用數位邏輯實作
> 參考[以 C 語言實作二進位加法](https://kopu.chat/%e4%bb%a5c%e5%af%a6%e4%bd%9c%e4%ba%8c%e9%80%b2%e4%bd%8d%e5%8a%a0%e6%b3%95/)
之前常聽到半加器和全加器,但都沒有認真的去理解,於是趁這次機會來研究其原理。
```c
int add(int a, int b) {
if (b == 0) return a;
int sum = a ^ b; /* 相加但不進位 */
int carry = (a & b) << 1; /* 進位但不相加 */
return add(sum, carry);
}
```
以上程式是加法器的實作,但為什麼`^`可以代表相加但不進位;而`&`可以代表進位但不相加呢?考慮以下例子:
0+0=**0**0
0+1=**0**1
1+0=**0**1
1+1=**1**0
相加後左側的數值和我們將`+`換成做`&`運算產生的數值一樣。
0+0=0**0**
0+1=0**1**
1+0=0**1**
1+1=1**0**
相加後右側的數值和我們將`+`換成做`^`運算產生的數值一樣。
![440px-Half_Adder.svg](https://hackmd.io/_uploads/ryqlQwIe0.png)
也就是說,把 EXOR 閘和 AND 閘組合在一起,便能進行一位元的加法,也就是常聽到的半加器。
![full-adder-757x380](https://hackmd.io/_uploads/ryNwIP8gA.jpg)
而全加器是由兩個半加器與一個 or 閘連組合而成,目的是為了處理當進行多位元做加法時,我們希望考慮能將被加數 (A) 、加數 (B) 與前一個位元來的進位 (Carry-In) 做相加。可以應用在串聯多個全加器來實作出多位元加法器,如「漣波進位傳遞加法器」,而上述的程式瑪也是參考該原理去做設計。
#### 省去迴圈
> 參考[Reverse integer bitwise without using loop](https://stackoverflow.com/questions/21511533/reverse-integer-bitwise-without-using-loop)
```c
int func(unsigned int x) {
int val = 0; int i = 0;
for (i = 0; i < 32; i++) {
val = (val << 1) | (x & 0x1);
x >>= 1;
}
return val;
}
```
這段程式是為了將傳入的值前後翻轉,如 abcdefgh -> hgfedcba 。
```c
new = num;
new = ((new & 0xffff0000) >> 16) | ((new & 0x0000ffff) << 16);
new = ((new & 0xff00ff00) >> 8) | ((new & 0x00ff00ff) << 8);
new = ((new & 0xf0f0f0f0) >> 4) | ((new & 0x0f0f0f0f) << 4);
new = ((new & 0xcccccccc) >> 2) | ((new & 0x33333333) << 2);
new = ((new & 0xaaaaaaaa) >> 1) | ((new & 0x55555555) << 1);
```
可以透過 bit-wise 操作來省去迴圈功能,程式可逐一拆解為:
```c
new = ((new & 0xf0f0f0f0) >> 4) | ((new & 0x0f0f0f0f) << 4);
```
0xf0 = b'11110000
0x0f = b'00001111
(abcdefgh & 0xf0f0f0f0) >> 4) = 0000abcd
(abcdefgh & 0x0f0f0f0f) << 4) = efgh0000
0000abcd | efgh0000 = efghabcd
```c
new = ((new & 0xcccccccc) >> 2) | ((new & 0x33333333) << 2);
```
0xcc = 'b11001100
0x33 = 'b00110011
(efghabcd & 0xcccccccc) >> 2) = 00ef00ab
(efghabcd & 0x33333333) << 2) = gh00cd00
00ef00ab | gh00cd00 = ghefcdab
```c
new = ((new & 0xaaaaaaaa) >> 1) | ((new & 0x55555555) << 1);
```
0xaa = 'b10101010
0x55 = 'b01010101
(ghefcdab & 0xaaaaaaaa) >> 1) = 0g0e0c0a
(ghefcdab & 0x55555555) << 1) = h0f0d0b0
0g0e0c0a | h0f0d0b0 = **hgfedcba**
---
### [你所不知道的 C 語言:遞迴呼叫篇](https://hackmd.io/@sysprog/c-recursion#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E9%81%9E%E8%BF%B4%E5%91%BC%E5%8F%AB%E7%AF%87)
#### Recursion
```c
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib (n - 2);
}
```
先前我對於遞迴呼叫的認知,就是透過傳入參數呼叫自己而已,相較於迭代法精簡且看起來比較厲害。上述是個典型的遞迴例子,用來計算 Fibonacci sequence 。
#### Tail recursion
```c
int fib(int n, int a, int b) {
if (n == 0) return a;
return fib(n - 1 , b, a + b);
}
```
不過看完了本篇文章,發現了居然還有 **Tail recursion** 這個方法,相較於一般遞迴,可以減少遞迴所需的記憶體空間和運行時間。觀察兩者差異,可以發現最主要的差別是在遞迴呼叫時,此方法的計算會在每個函式 `return` 中直接進行,因此不需要保存相關的狀態訊息。
不像之前的遞迴需 call function 至中止條件後才開始計算值,再依序傳回到最上層,因此可大幅降低時間,減少 stack 使用量。
> [Tail Call Optimization: The Musical (2019 年) 也是有趣的短片](https://www.youtube.com/watch?v=-PX0BV9hGZY&ab_channel=Confreaks)
#### 字串反轉
在前面筆記內容有提到[數值反轉](https://hackmd.io/64nfWFSLRneKnnD_P-NDQg?view#%E7%9C%81%E5%8E%BB%E8%BF%B4%E5%9C%88),而對字串做反轉如何進行呢?其中很大一部分的工作是做 SWAP。
```c
void swap(int *a, int *b) {
int t = *a; *a = *b; *b = t;
}
```
以上這是我們常看到的 `SWAP` 方法,但也可以透過**數值運算**和**邏輯運算**的方式將兩者做交換,其好處是 in-place ,不用額外宣告變數即可做交換。
##### 數值運算
```c
*a = *a - *b; /* 兩數相差 */
*b = *a + *b; /* 相加得出 *b */
*a = *b - *a; /* 相減得出 *a */
```
##### 邏輯運算
```c
*a = *a ^ *b; // 求得相差位元
*b = *a ^ *b; // 與相差位元做 XOR 得出 *a
*a = *a ^ *b; // 與相差位元做 XOR 得出 *b
```
---
### [你所不知道的 C 語言:技巧篇](https://hackmd.io/@sysprog/c-trick)
#### 善用 GNU extension 的 typeof
```c
int a;
typeof(a) b = 10; // equals to "int b = 10;"
char s[6] = "Hello";
char *ch;
typeof(ch) k = s; // equals to "char *k = s;"
```
這段程式內提到的 `typeof` 允許我們傳入一個變數,代表的會是該變數的型態。經常在許多巨集上看到其蹤影,原因是因為在巨集內無法得知傳入的參數型態,在需要宣告相同型態的變數時,`typeof` 就會是一個很好的幫手。
```c
#define container_of(ptr, type, member) \
__extension__({ \
const __typeof__(((type *) 0)->member) *__pmember = (ptr); \
(type *) ((char *) __pmember - offsetof(type, member)); \
})
```
這是一個經常使用到的巨集,也有出現在 [lab0-c/list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h) 中,在完成 lab0-c 過程中經常呼叫到該函式,它接受三個參數。ptr 是結構的成員指標,type 是結構的型別,member 是結構中的成員名稱。
```c
__extension__({ ... })
```
是一個修飾字,用來防止 gcc 編譯器產生警告。
```c
const __typeof__(((type *) 0)->member) *__pmember = (ptr);
```
`(type *) 0` 表示將 `0` 轉換為指向 `type` 型別的指標。
`((type *) 0)->member` 表示結構 `type` 中的成員 `member`。
`__typeof__(((type *) 0)->member)` 會返回 `member` 成員的型別。
`const __typeof__(((type *) 0)->member) *__pmember` 宣告了一個指向 `member` 成員的指標 `__pmember`,並將 `ptr` 賦值給它。
```c
(type *) ((char *) __pmember - offsetof(type, member));
```
`offsetof(type, member)` 會返回 `member` 在 `type` 結構中的偏移量。
`(char *) __pmember - offsetof(type, member)` 將 `__pmember` 指標轉換為 `char *`,然後減去 `member` 的偏移量。這樣可以得到結構的開始地址。
`(type *) ((char *) __pmember - offsetof(type, member))` 將上述結果轉換為 type *,即結構的指標。
::: info
這裡不太理解為什麼要將 `__pmember` 轉成 `char` 型態再去和 `offsetof(type, member)` 做運算?
> 在 C 語言規格書中, `char *` 必定是 byte-addressed
> `*((void *) ptr)`
> https://hackmd.io/@sysprog/it-vocabulary
:::
該函式主要功能為只要有指向結構內某個成員的指標,就能找到包含該成員的整個結構,並且可以進一步訪問結構中的其他成員。
使用範例參考先前作業的 [q_delete_dup](https://hackmd.io/zkT6lHFOTkCrXcKFGegbvA?view#q_delete_dup) :
```c
bool q_delete_dup(struct list_head *head)
{
if(!head)
return false;
struct list_head *list_cur = head -> next;
struct list_head *list_next = list_cur -> next;
while(list_next != head){
if(strcmp(container_of(list_cur, element_t, list) -> value,
container_of(list_next, element_t, list) -> value) == 0)
...
}
```
可以透過 `container_of` 找到 `list_cur` 該成員的整個結構,並透過 `->` 去訪問該結構內其它成員 `value`。
---
### [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory#%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84-C-%E8%AA%9E%E8%A8%80%EF%BC%9A%E8%A8%98%E6%86%B6%E9%AB%94%E7%AE%A1%E7%90%86%E3%80%81%E5%B0%8D%E9%BD%8A%E5%8F%8A%E7%A1%AC%E9%AB%94%E7%89%B9%E6%80%A7)
#### Data Alignment
對齊的目的是確保數據類型的開始地址是某個固定的倍數,通常是該數據類型的大小。例如,對於 4 byte 的整數類型,它的地址應該是 4 的倍數。其中 data 的 address 需可以被 2 的冪整除,如果數據的地址不符合對齊要求,可能會導致效能下降或者出現錯誤。
```c
struct s1 {
char c;
int a;
}
```
該範例的 `struct` 包含一個佔 4 byte 的 `int` 以及佔 1 byte 的 `char` ,雖然實際上佔了 5 個 byte ,但為了對齊,所以會自動擴充成佔 8 個 byte 。
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct _s1 {
char a[5];
} s1;
int main() {
s1 p[10];
printf("struct s1 size: %ld byte\n", sizeof(s1));
for(int i = 0; i < 10; i++) {
printf("the struct p[%d] address =%p\n", i, p + i);
}
}
```
但若是以 `char` 組合的 `struct` ,則是根據 `char` 本身佔 1 個 byte 的倍數去對齊即可。如本範例的 `a[5]` ,共佔 5 個 byte 。
```c
struct foo {
int a : 3;
int b : 2;
int : 0; /* Force alignment to next boundary */
int c : 4;
int d : 3;
};
int main() {
int i = 0xFFFF;
struct foo *f = (struct foo *) &i;
printf("a=%d\nb=%d\nc=%d\nd=%d\n", f->a, f->b, f->c, f->d);
return 0;
}
```
這裡還有一個類似的概念是 [bit-field](https://hackmd.io/@sysprog/c-bitfield#Linux-%E6%A0%B8%E5%BF%83-BUILD_BUG_ON_ZERO) ,每個 `int` 型態皆為 32 bit ,其中遇到 `int : 0` 代表強制對齊到下個 `int` ,所以這裡的 `a` 和 `b` 對應到 `i` 為 a = 111 (decimal -1), b = 11 (decimal -1);而由於 `c` 和 `d` 超過 `i` 本身值所分配到的位址(不一定在 0xFFFF 範圍),所以會找不到值,故 `c` 和 `d` 皆為 0 或者亂數。
---
### CS:APP 第 2 章重點提示和練習
```c
int i=0xFFFFFFFF;
i = i << 32; // 此結果未定義
```
左移超過變數長度,其結果未定義。
```c
n >>31
```
上述程式可判斷一個 int 型態的變數 n 是否為正數,由於算術右移,所以若為負數的話其結果為 -1 ,正數為 0。
```c
int n = 10;
for (int i = n - 1 ; i - sizeof(char) >= 0; i--)
printf("i: 0x%x\n",i);
```
該列子看起來是個正常的程式,但執行時卻會導致無窮迴圈,因為 sizeof 回傳值是 unsigned int 型態,而當無號數與有號數在 C 語言混合在單一表示式時,有號數會被轉換為無號數。所以此時 signed 型態的變數 `n` 也會被轉換為 unsigned 的形式,在迴圈跑到 i = 0 時,無號數 0 再減 1 就會變為 0xFFFFFFFF 而產生無窮迴圈。
```c
long a, b, x;
x = a + b;
if ((x ^ a) >= 0 || (x ^ b) >= 0)
```
此方法用來檢查是否有加法導致的溢位,當兩正數相加時變負數或兩負數相加變正數表溢位,可以觀察到兩者的溢位發生時都會使得最高 bit 即 sign bit 發生改變,故可以透過 xor 運算得出 a 和 b 兩者是否相加後的 sign bit 改變,有的話即為溢位。
---
## 簡述想投入的專案
### 並行程式設計
曾經僅學習相關理論,會想嘗試設計並行程式,對其做出貢獻。
### Linux 排程器研究
之前在 stm32 板子上寫過排程,想對其做更充分的研究。
### 實作高效記憶體配置器
想深入研究記憶體相關內容。
### 測驗與作業改進
課程不斷的推進,前面有些作業仍有許多待加強,會希望能夠回頭補齊,並試著對 linux 核心做出貢獻。
---
## 期末專題
- [期末專題網址](https://hackmd.io/@kevinzxc1217/SyDZBUnQA)
>
TODO: 高效記憶體配置器: 閱讀論文 "LLFree: Scalable and Optionally-Persistent Page-Frame Allocation" ( https://github.com/luhsra/llfree-c ) 重現實驗並紀錄問題
> 搭配: https://hackmd.io/@sysprog/c-function (malloc/free 本質上就是 heap 的管理器)
> 要解決的問題是多核處理器中,allocator 會面臨大量的 lock contention,若要有更好的 scalability,就需要 lock-free 的實作
> https://hackmd.io/@sysprog/concurrency/%2F%40sysprog%2Fconcurrency-lockfree : lock-free 指 只要執行足夠長的時間,至少會有一個執行緒會有進展 (progress)
TODO: 理解 https://github.com/luhsra/llfree-c 內建測試程式的目的,並分類解釋 (有可能會遇到編譯/執行的錯誤,善用 GitHub 提交 issue / pull request)
TODO: 比較其他的實作
---
## 與老師一對一討論紀錄
### Q1
我:這段程式碼為什麼要強制將 `__pmember` 轉換成 char 型態再做計算?
```c
#define container_of(ptr, type, member) \
__extension__({ \
const __typeof__(((type *) 0)->member) *__pmember = (ptr); \
(type *) ((char *) __pmember - offsetof(type, member)); \
})
```
老師:在 C 語言規格書中, char * 必定是 byte-addressed,這裡經過 `offsetof` 計算出來的值為 char 型態,前面的 `__pmember` 為 `type` 型態的指標,需先將其也轉成 char 型態才可和 offset 進行運算。
> 參考 [The (char *) casting in container_of()](https://stackoverflow.com/questions/20421910/the-char-casting-in-container-of-macro-in-linux-kernel)
### Q2
老師:*((void *) ptr) 想想看這樣的指標合法嗎?
我:應該不合法....
```c
int value = 10;
void *ptr = &value;
// 這是不正確的,會引起編譯錯誤
// *((void *) ptr) = 20;
// 正確的實例化方式
*((int *) ptr) = 20;
// 輸出: Value: 20
printf("Value: %d\n", value);
```
:::info
後續上網找了資料,發現直接實例化 void 指標是不允許的,因為編譯器需要知道資料型態才能確定需要讀取多少個位元。實例化之前,應將 void 指標轉換為可用的指標類型,如 int ...。
:::