# 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 ...。 :::