# 2020q3 Homework1 (lab0) contributed by < `Weiting0114` > ## 簡介 lab0 中主要是透過許多工具,讓我們養成良好的寫程式習慣,包含: * Git pre-commit hook * [git-good-commit](https://github.com/tommarshall/git-good-commit): 學習撰寫良好的 Git commit message * [cppcheck](http://cppcheck.sourceforge.net/): 靜態程式碼分析工具 * [clang-format](https://clang.llvm.org/docs/ClangFormat.html): 確保一致的程式風格 * [GNU Aspell](http://aspell.net/): 拼字檢查 * [valgrind](https://valgrind.org/): 動態程式分析工具 * [AddressSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer): 檢查記憶體的使用 詳細的環境設定以及lab的相關說明請參閱 [I01: lab0](https://hackmd.io/@sysprog/2020-lab0?fbclid=IwAR2YOjtMYzE_kB5vqvBUOSM9OlR5hgvz7NHUGg21NOcN9Hb8vccxEV-yC3o) [Github 程式碼](https://github.com/Weiting0114/lab0-c) ## 環境 ```lang=shell $ uname -a Linux eric 5.4.0-47-generic #51~18.04.1-Ubuntu SMP Sat Sep 5 14:35:50 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ``` ## 作業要求 於 `queue.[c/h]` 中完成下列功能 * `q_new`: 建立新的「空」佇列; * `q_free`: 釋放佇列所佔用的記憶體; * `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則); * `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則); * `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素; * `q_size`: 計算佇列中的元素數量; * `q_reverse`: 以反向順序重新排列鏈結串列,只能重排已經存在的元素; * `q_sort`: 以==遞增順序==來排序鏈結串列的元素 * 功能需要在 $O(1)$ 時間內執行 ## 開發過程 #### **`queue.h`** * 將 `queue_t` 結構體增加兩位成員,`tail`、`size`。增加 `list_ele_t *tail` 提供Queue快速尋訪,達成執行時間需在 $O(1)$ 內執行完畢。 ```c= /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; int size; } queue_t; ``` #### **`q_new`** * 透過判斷式確認記憶體分配是否成功,避免空指標的操作。 ```c= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (q){ q->head = NULL; q->tail = NULL; q->size = 0; } return q; } ``` #### **`q_free`** * 利用新節點 `tmp` 儲存要釋放的資料 * 透過 `q->head` 的移動,將整個 `queue` 清空 >在清除節點時須先釋放資料再釋放節點,否則記憶體中會殘留資料 ```c= void q_free(queue_t *q) { if (q == NULL) return; while (q->head) { list_ele_t *temp; temp = q->head; q->head = q->head->next; free(temp->value); free(temp); } free(q); } ``` #### **`q_insert_head`** * 將新元素 `newh` 插入 `queue` 的最前端 * 判斷 `queue` 是否為 `NULL` ,避免在之後的計算上造成 memory leak * 分配記憶體給新的元素(`newh`),判斷是否分配成功,若失敗,則將之前分配的 `newh` 清除 * 利用 C library <string.h> 的 strncpy 複製 s,並存入 `newh->value` * 在新增第一個元素時,須將 `q->head`、 `q->tail` 指向新插入的元素。之後隨著插入元素的增加,將 `q->head` 逐漸向後移 :::info :exclamation: 下列是未判斷 `queue` 是否為 `NULL`,會在 `trace-07-robust` 出錯,利用 valgrid 查看發現是 memory leak。 ```lang = shell $ valgrind --memcheck:show-leak-kinds=all ./qtest < traces/trace-07-robust.cmd ==5649== Address 0x10 is not stack'd, malloc'd or (recently) free'd ==5649== Segmentation fault occurred. You dereferenced a NULL or invalid pointer ... ==5649== HEAP SUMMARY: ==5649== in use at exit: 9,165 bytes in 30 blocks ==5649== total heap usage: 53 allocs, 23 frees, 10,442 bytes allocated ==5649== ==5649== LEAK SUMMARY: ==5649== definitely lost: 0 bytes in 0 blocks ==5649== indirectly lost: 0 bytes in 0 blocks ==5649== possibly lost: 0 bytes in 0 blocks ==5649== still reachable: 9,165 bytes in 30 blocks ==5649== suppressed: 0 bytes in 0 blocks ``` ::: ```cpp= bool q_insert_head(queue_t *q, char *s) { if (q == NULL) return false; list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); if (newh == NULL) return false; newh->value = malloc(sizeof(char) * strlen(s) + 1); if (newh->value == NULL) { free(newh); return false; } strncpy(newh->value, s, strlen(s) + 1); if (q->size == 0) { newh->next = NULL; q->head = q->tail = newh; } else { newh->next = q->head; q->head = newh; } q->size += 1; return true; } ``` #### **`q_insert_tail`** * 作法與 `q_insert_head` 差不多 * 將新元素 (`newt`) 放入 `queue` 的尾端 * 若插入為第一個元素,則將 head 和 tail 同時指向新元素 (`newt`) ```c= bool q_insert_tail(queue_t *q, char *s) { if (q == NULL) return false; list_ele_t *newt; newt = malloc(sizeof(list_ele_t)); if (newt == NULL) return false; newt->value = malloc(sizeof(char) * strlen(s) + 1); if (newt->value == NULL) { free(newt); return false; } strncpy(newt->value, s, strlen(s) + 1); if (q->size == 0) { q->head = q->tail = newt; } else { q->tail->next = newt; q->tail = newt; } newt->next = NULL; q->size += 1; return true; } ``` #### **`q_remove_head`** * 藉由確認 `q->head` 是否為 `NULL`,確認 `queue` 是否為空 * 若 `sp` 不為 `NULL`,將要刪除後的字串複製進去 * 新增一個指標 `rm_node` 以供釋放 (`free`) * 將 `rm_node` 的成員都清空後,再釋放 `rm_node` ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (q == NULL || q->head == NULL || q->size == 0) return false; list_ele_t *rm_node = q->head; if (sp) { memset(sp, '\0', bufsize); strncpy(sp, rm_node->value, bufsize - 1); } /* *Method as same as free function *Free node's value and then free value */ q->head = q->head->next; q->size -= 1; free(rm_node->value); free(rm_node); return true; } ``` :::info :exclamation: 在 `trace-06-string` 測試中出現了error,原因是因為沒有設定字串的結束 ( `'\0'`),因此利用 memset 將 `sp` 的內容全部寫為零。 ```cpp=8 memset(sp, '\0', bufsize); ``` <!-- * 利用valgrind進行測試: ```shell $ valgrind --memcheck:show-leak-kinds=all ./qtest < traces/trace-06-string.cmd ``` --> > ERROR: Removed value aardvark_bear_dolphin_gerbil_jXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX != expected value meerkat_panda_squirrel_vulture ::: #### **`q_size`** * 若 `q` 不存在,則返回 `0` * 藉由直接取得 `q->size`,達成在 $O(1)$ 時間內完成 `q_size()` * 在 `q_new` 時,便已將 `q->size` 設為 `0` ,因此就算 queue 為空,還是會返回 `0` ```c= int q_size(queue_t *q) { if (q == NULL) return 0; return q->size; } ``` #### **`q_reverse`** * 利用 `q->size` 檢查 queue 是否有足夠的資料可以做反轉 * 將 `q->tail` 設定為 `q->head`,利用 `curnxt` 來儲存 `q->head->next` ,避免待會將資料反向後,無法取得下一筆資料 ```cpp= void q_reverse(queue_t *q) { if (q == NULL || q->size <= 1) return; list_ele_t *cur, *curnxt; q->tail = q->head; cur = q->head; curnxt = q->head->next; while (curnxt != NULL) { q->head = curnxt; curnxt = q->head->next; q->head->next = cur; cur = q->head; } q->tail->next = NULL; } ``` * 原始 queue 的狀況 ```graphviz digraph linked_list{ rankdir=LR; node [shape=record]; a [label="{ <data> a | <ref> }"] b [label="{ <data> b | <ref> }"]; c [label="{ <data> c | <ref> }"]; d [label="{ <data> d | <ref> }"]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d :data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d:ref:d -> NULL :data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; head [shape=box]; head -> a; tail [shape=box]; tail -> d; } ``` * 進入迴圈之前 queue 的狀況 ```graphviz digraph linked_list{ rankdir=LR; node [shape=record]; a [label="{ <data> a | <ref> }"] b [label="{ <data> b | <ref> }"]; c [label="{ <data> c | <ref> }"]; d [label="{ <data> d | <ref> }"]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false, arrowsize=1.2]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d:ref:d -> NULL:data[arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; cur -> a; curnxt -> b; head [shape=box]; head -> a; tail [shape=box]; tail -> a; } ``` * 進入迴圈之後將 `q->head` 、 `curnxt` 向後移動 * 並將兩兩元素做交換 <pre> while (curnxt != NULL) { <span style="color:blue">q->head = curnxt;</span> <span style="color:blue">curnxt = q->head->next;</span> <span style="color:red">q->head->next = cur;</span> <span style="color:red">cur = q->head;</span> } <span style="color:green">q->tail->next = NULL;</span> </pre> 下圖<font color=blue>藍色</font>部份是為了將整個 `queue` 走過一遍,<font color=red>紅色</font>的部份是將 `node` 做反轉: ```graphviz digraph linked_list{ rankdir=LR; node [shape=record]; a [label="{ <data> a | <ref> }"] b [label="{ <data> b | <ref> }"]; c [label="{ <data> c | <ref> }"]; d [label="{ <data> d | <ref> }"]; a:ref:a -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; b:ref:b -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d:ref:d -> NULL[arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; cur -> a; curnxt [fontcolor=blue, shape=box, color=blue] curnxt -> c [color=blue]; head [fontcolor=blue, shape=box, color=blue]; head -> b [color=blue]; tail [shape=box]; tail -> a; } ``` ```graphviz digraph linked_list{ rankdir=LR; node [shape=record]; a [label="{ <data> a | <ref> }"]; b [label="{ <data> b | <ref> }"]; c [label="{ <data> c | <ref> }"]; d [label="{ <data> d | <ref> }"]; head [fontcolor=blue, color=blue]; cur [label="{cur}"][fontcolor=red, color=red]; curnxt [label="{curnxt}"][fontcolor=blue, color=blue]; a:ref:c -> b [arrowtail=dot, dir=both, tailclip=false]; b:ref:c -> a:data [arrowtail=dot, dir=both, tailclip=false] [color=red]; cur -> b [color=red]; head -> b [color=blue]; tail -> a; curnxt -> c:data [color=blue]; c:ref:c -> d [arrowtail=dot, dir=both, tailclip=false]; d:ref:d -> NULL [arrowtail=dot, dir=both, tailclip=false]; } ``` #### **`q_sort`** * 參考 [Linked LIst Sort](https://npes87184.github.io/2015-09-12-linkedListSort/),使用 Merge sort 排序來完成 * `merge_sort()` * 將一個 Linked list 拆成兩個 * 藉由兩個指標 (`fast`, `slow`) 不同的前進速度 (`fast` : `slow` = 1 : 2),進而找出位於中間的節點。演算法是利用[龜兔賽跑 (Floyd’s algorithm)](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm) * `fast` 會指向最後的 `element` * `slow` 會指向中間的 `element` * 將 `fast` 指向 `slow->next` 後,便可以得到兩個等長的 `linked list` (`head` 和 `fast`) * 將兩個已排序的 Linked list 合成一個 Linked list,並回傳 * 參考 [jwang0306](https://hackmd.io/@jwang0306/lab0-c) 的 `merge`,將比較字串的部分縮進 `while` * 遍歷 `l1` 和 `l2`,並比較兩者元素,來建立新 Linked list ```cpp= void q_sort(queue_t *q) { if (q == NULL || q->size <= 1) return; q->head = split(q->head); while (q->tail) { q->tail = q->tail->next; } } ``` **`split`** ``` cpp= list_ele_t *split(list_ele_t *head) { if (head == NULL || head->next == NULL) return head; /*Split list*/ list_ele_t *slow = head, *fast; for (fast = head->next; fast && fast->next; fast = fast->next->next) { slow = slow->next; } fast = slow->next; slow->next = NULL; // sort each list list_ele_t *left = split(head); list_ele_t *right = split(fast); // merge sorted left and sorted right return merge(left, right); } ``` **`merge`** ``` cpp= list_ele_t *merge(list_ele_t *left, list_ele_t *right) { list_ele_t *head = NULL; list_ele_t **tmp = &head; while (left && right) { if (strcmp(left->value, right->value) < 0) { *tmp = left; left = left->next; } else { *tmp = right; right = right->next; } tmp = &((*tmp)->next); } if (left) *tmp = left; if (right) *tmp = right; return head; } ``` :::info [你所不知道的C語言: linked list 和非連續記憶體操作](https://hackmd.io/@sysprog/c-linked-list?type=view) Linus Torvalds 的解說: > People who understand pointers just use a "pointer to the entry pointer", and initialize that with the address of the list_head. And then as they traverse the list, they can remove the entry without using any conditionals, by just doing`*pp = entry->next`. 因此我們先做一個指向 head 的指標: `Node **indirect = &list->head;` ```mermaid graph LR subgraph linked list head==>node1==>node2 end subgraph pointer to pointer indirect==>head end ``` 在走訪 list 的時候,先`*indirect` (dereference `indirect` ,找出它所指向的 node ,此刻為 head), 接著```(*tmp)->next```(找出它的 `->next` ,這邊為 head 的 next ,也就是 node1),然後 `indirect = &((*indirect)->next)` (將其 reference 存回 `indirect` ,此刻 `indirect` 就變為指向 node1 的指標): ```mermaid graph LR subgraph linked list head==>node1==>node2 end subgraph pointer to pointer indirect==>node1 end ``` ::: ## 指標的指標 * 擷取自[你所不知道的 C 語言:指標篇](https://reurl.cc/EzmAVa) * 利用下列兩段程式碼解說為何要使用指標的指標 考慮以下程式碼: ```cpp=1 int B = 2; void func(int *p) { p = &B; } int main() { int A = 1, C = 3; int *ptrA = &A; func(ptrA); printf("%d\n", *ptrA); return 0; } ``` 在第 5 行 (含) 之前的記憶體示意: ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structptr [label="ptrA|<ptr> &A"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structa:A:nw } ``` 第 6 行 `ptrA` 數值傳入 `func` 後的記憶體示意: ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structp [label="p|<p>&A"] structptr [label="<name_ptr> ptrA|<ptr> &A"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structa:A:nw structp:p -> structa:A:nw } ``` `func` 將變數 `p` 的值指定為 `&B` ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structp [label="p|<p>&B"] structptr [label="<name_ptr> ptrA|<ptr> &A"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structa:A:nw structp:p -> structb:B:nw } ``` 由上圖可見,原本在 main 中的 `ptrA` 內含值沒有改變。這不是我們期望的結果,該如何克服呢?可透過「指標的指標」來改寫,如下: ```cpp=1 int B = 2; void func(int **p) { *p = &B; } int main() { int A = 1, C = 3; int *ptrA = &A; func(&ptrA); printf("%d\n", *ptrA); return 0; } ``` 在第 5 行 (含) 之前的記憶體示意: ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structptr [label="ptrA|<ptr> &A"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structa:A:nw } ``` 第 6 行時,傳入 `func` 的參數是 `&ptrA`,也就是下圖中的 `&ptrA(temp)`: ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structadptr [label="&ptrA(temp)|<adptr> &ptrA"] structptr [label="<name_ptr> ptrA|<ptr> &A"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structa:A:nw structadptr:adptr -> structptr:name_ptr:nw } ``` 進入 func 執行後,在編譯器產生對應參數傳遞的程式碼中,會複製一份剛傳入的 `&ptrA`,產生一個自動變數 `p`,將 `&ptrA` 內的值存在其中,示意如下: ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structp [label="p(in func)|<p> &ptrA"] structadptr [label="&ptrA(temp)|<adptr> &ptrA"] structptr [label="<name_ptr> ptrA|<ptr> &A"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structa:A:nw structadptr:adptr -> structptr:name_ptr:nw structp:p -> structptr:name_ptr:nw } ``` 在 `func` 中把 `p` 指向到的值換成 `&B`: ```graphviz digraph structs { node[shape=record] {rank=same; structa,structb,structc} structp [label="p(in func)|<p> &ptrA"] structadptr [label="&ptrA(temp)|<adptr> &ptrA"] structptr [label="<name_ptr> ptrA|<ptr> &B"]; structb [label="<B> B|2"]; structa [label="<A> A|1"]; structc [label="<C> C|3"]; structptr:ptr -> structb:B:nw structadptr:adptr -> structptr:name_ptr:nw structp:p -> structptr:name_ptr:nw } ``` 經過上述「指標的指標」,`*ptrA` 的數值從 1 變成 2,而且 `ptrA` 指向的物件也改變了。 ## Valgrind - Massif * 環境設定: 安裝 [Massif](https://reurl.cc/GrAN13) `$ sudo apt install massif-visualizer` * 首先執行以下命令,這裡使用 `trace-06-string.cmd`作為範例 ```shell $ valgrind --tool=massif ./qtest < traces/trace-06-string.cmd ``` * 在該資料夾可得到記憶體資料的文字檔 `massif.out.<pid>` ,執行下列指令 ```shell $ ms_print massif.out.16246 ``` :::info :exclamation: `pid` 會根據執行 valgrind 時的執行緒改變,因此要注意其對應的編號 ::: * 執行完 `ms_print massif.out.16246` 即可得到下列結果 ![](https://i.imgur.com/0m24yrL.png)