# 2025q1 Homework1 (lab0) contributed by <`miaoshahaha`> {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $lscpu rchitecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-14500 CPU family: 6 Model: 191 Thread(s) per core: 2 Core(s) per socket: 14 Socket(s): 1 Stepping: 2 CPU(s) scaling MHz: 19% CPU max MHz: 5000.0000 CPU min MHz: 800.0000 BogoMIPS: 5222.40 ``` ## 開發指定的佇列操作 ### 開發準備 在實作 lab0-c 之前,要先詳閱 `queue.h` 以及 `list.h`,其中雙向鏈結串列的設計如下: ```c struct list_head { struct list_head *prev; struct list_head *next; }; ``` 並且將 list_head 嵌入至結構體 element_t,可藉由 `container_of` 巨集推算出 list 節點的記憶體位址。 ```c typedef struct { char *value; struct list_head list; } element_t; ``` `container_of` 巨集可以在給定成員的型態及名稱,傳回成員的位址減去結構體的起始位址。 ```c #define container_of(ptr, type, member) \ ((type *) ((char *) (ptr) - offsetof(type, member))) ``` ### `q_new` 建立新的空佇列 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) { return NULL; } INIT_LIST_HEAD(head); return head; } ``` 先宣告一個 `list_head` 結構體 `head`,並且分配記憶體空間,如果分配空間失敗,則回傳 `NULL` ,這邊用到了 `INIT_LIST_HEAD`,可以將 `head` 的 `next` 跟 `prev` 指標指向自己。 這邊最一開始就要注意,當我們呼叫 `malloc` 的時候,並不保證每一次都會成功的分配記憶體空間,所以在呼叫 `malloc` 之後,必須確認是否成功分配記憶體空間,以防止 Segmentation Fault: **C99 (7.20.3)** > If the space cannot be allocated, a null pointer is returned. ### `q_free` 釋放佇列所佔用的記憶體空間 ```c void q_free(struct list_head *head) { if (!head) return; element_t *entry, *safe = NULL; list_for_each_entry_safe (entry, safe, head, list) { q_release_element(entry); } free(head); } ``` 當我們要釋放記憶體空間的時候,除了要釋放雙向環狀鏈結佇列之外,還必須 連帶將 `element_t` 結構體中的元素釋放,釋放的方法是遍歷佇列中每個節點,透過 `list_for_each_entry` 可以拜訪每個節點,而其程式碼為: ```c #define list_for_each_entry(entry, head, member) \ for (entry = list_entry((head)->next, typeof(*entry), member); \ &entry->member != (head); \ entry = list_entry(entry->member.next, typeof(*entry), member)) ``` 裡面的 `list_entry` 就是藉由 `container_of` 來得知其中元素所在的記憶體位址為何。 ```c #define list_entry(node, type, member) container_of(node, type, member) ``` ### `q_insert_head` 在佇列開頭插入給定的新節點 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new_ele = malloc(sizeof(element_t)); if (!new_ele) return false; new_ele->value = strdup(s); if (!new_ele->value) { free(new_ele); return false; } list_add(&new_ele->list, head); return true; } ``` 做插入的時候先分配記憶體空間給結構體 `element_t` ,並且用 `strdup` 函式複製字串,它可以將副本的起始位址的指標回傳,最後我們透過 `list_add` 來新增節點至佇列開頭的下一個位置。 ### `q_insert_tail` 在佇列尾端插入給定的新節點 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *new_ele = malloc(sizeof(element_t)); if (!new_ele) return false; new_ele->value = strdup(s); if (!new_ele->value) { free(new_ele); return false; } list_add_tail(&new_ele->list, head); return true; } ``` 這裡的做法跟插入節點到開頭唯一的差別就是用到了 `list_add_tail`, 我們可以透過 `list_add` 以及 `list_add_tail` 來決定要將節點新增到頭部或尾端。 ### `q_remove_head` 在佇列開頭移去給定的節點 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ele = list_first_entry(head, element_t, list); if (sp) { memcpy(sp, ele->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_del(&ele->list); return ele; } ``` 這裡的做法是要移去給定的節點,這裡有個要留意的地方是,在 `queue.h` 的標頭檔有說明 'remove' 和 'delete' 是不同的,我們只需要移除節點即可,而不需要釋放記憶體空間。 移去給定節點方法是,如果 `*sp` 不是 `NULL` 則將元素移除,並且將移除元素字串複製 `bufsize - 1` 的大小到 `*sp` ,然後在最後加上 NULL terminator。這裡找出字串位址的方法是透過 `list_first_entry` 這個巨集先找出元素的位址,再間接的找出元素中 `*value` 的位址。 再來有個困難的地方是如何將字串複製到 `*sp` ,直覺的想法是直接用 `strcpy` 或者是 `strncpy`,但我們需要的是 `bufsize - 1` 的大小,所以必須要給定一個複製的範圍,因此不能用 `strcpy`,所以想法是用 `strncpy` 來複製字串到 `*sp` 當中,不過這邊有個考量是,在 C 語言規格書中所規範的 `strncpy` 是: **C99 (7.21.2.4)** >If the array pointed to by s2 is a string that is shorter than n characters, null characters are appended to the copy in the array pointed to by s1, until n characters in all have been written. 如果複製字串小於我們所給定的大小,`strncpy` 會將超過原本字串的部分全部補 '0' ,因為是不必要的操作所以會導致效率不佳。其他的複製字串的方式就是使用 `memcpy`,它的差別是給定多少的大小,它就複製多少的空間,這個方法符合 `q_release_head` 的需求,所以我們只需要按照它的需求,複製 `bufsize - 1` 大小,並且在最後自行補上 NULL terminator 就可以。 ### `q_remove_tail` 在佇列尾端移去給定的節點 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ele = list_last_entry(head, element_t, list); if (sp) { memcpy(sp, ele->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_del(&ele->list); return ele; } ``` 這裡的方法大致上跟 `q_remove_head` 相同,不一樣的地方在我們要移除的是尾端的節點,因此就用 `list_last_entry` 來得到尾端元素的記憶體位址,就可以做移除了。 ### `q_size` 計算目前佇列的總節點量 ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; struct list_head *next; int cnt = 0; list_for_each (next, head) { cnt++; } return cnt; } ``` 這裡只要遍歷每個節點即可,用一個計數器來計算節點數量。 ### `q_delete_mid` 移走佇列中間的節點 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *slow = head->next, *fast = head->next->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } element_t *del_ele = list_entry(slow, element_t, list); list_del(&del_ele->list); q_release_element(del_ele); return true; } ``` 要找出中間節點的方法只要透過快慢指標就可以快速找到,當找到中間節點之後,再透過 `list_entry` 得到元素的記憶體位址,接著移除節點,並釋放元素佔據的記憶體空間即可,這裡透過 `queue.h` 中的靜態變數 `q_release_element` 來釋放元素記憶體空間。 ### `q_swap` ```c void q_swap(struct list_head *head) { if (!head) return; struct list_head *node, *safe; for (node = (head)->next, safe = node->next; node != (head) && safe != (head); node = node->next, safe = node->next) { list_move(node, safe); } } ``` 這邊要針對佇列中兩兩節點做交換,接著用 `list_move` 來交換節點。 ### `q_reverse` ```c void q_reverse(struct list_head *head) { if (!head) return; struct list_head *node; struct list_head *safe; list_for_each_safe (node, safe, head) { list_move(node, head); } } ``` 以反向順序重新排列鏈結串列,並且不配置額外的記憶體空間來操作,這個做法可以透過遍歷每個節點,並且將正在拜訪的節點移到頭部,如此即可將鏈結串列反向排列。 ### `q_reverseK` ```c void q_reverseK(struct list_head *head, int k) { if (!head) return; struct list_head *tmp = head; LIST_HEAD(tmp_head); struct list_head *node, *safe; int cnt = 0; list_for_each_safe (node, safe, head) { cnt++; if (cnt == k) { list_cut_position(&tmp_head, tmp, node); q_reverse(&tmp_head); list_splice_init(&tmp_head, tmp); } cnt = 0; tmp = node->next; } } ``` 透過 `q_release` 來進行反向順序的操作,並且使用計數器計算遍歷過的節點,當遍歷的節點等於 `k` ,便將該段串列進行反向排列。 ## 在 qtest 提供新的命令 shuffle 在 `qtest.c` 中會呼叫 `console_init()` 命令,我們可以在 `console_init()` 中新增 `shuffle` 命令,觀察新增命令使用了 `ADD_COMMAND` 巨集如下: ```c #define ADD_COMMAND(cmd, msg, param) add_cmd(#cmd, do_##cmd, msg, param) ``` 所以要建立新的命令,只要在 `qtest.c` 檔案中先提供 `do_*` 函式,並且在 `console_init()` 中新增即可,假設要新增 `shuffle` 命令: ```c bool do_shuffle(int argc, char *argv[]) { /* my_shuffle_function*/ return (bool) printf("Hello, World\n"); } ```