# 2020q3 Homework1 (lab0) contributed by < `tungtylee` > ###### tags: `homework` ###### 為網路參與課程的學員 (非修課/非旁聽) ## Outline [TOC] ## 開發環境 ``` $ uname -a Linux hwm 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 ``` ## 作業需求 ### 完成 [queue.c](https://github.com/sysprog21/lab0-c/blob/master/queue.c) 僅提供介面 ([interface](https://en.wikipedia.org/wiki/Interface-based_programming)) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下:已 * [x] `q_new`: 建立新的「空」佇列; * [x] `q_free`: 釋放佇列所佔用的記憶體; * [x] `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則); * [x] `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則); * [x] `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素。 * [x] `q_size`: 計算佇列中的元素數量。 * [x] `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列元素,換言之,它只能重排已經存在的元素; * [x] ==`q_sort`==: 「[進階電腦系統理論與實作](http://wiki.csie.ncku.edu.tw/sysprog/schedule)」課程所新增,以==遞增順序==來排序鏈結串列的元素,可參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法; ### 未完成 1. [ ] 探索 [GNU/Linux 開發環境](https://hackmd.io/@sysprog/gnu-linux-dev)並善用 [Git](https://git-scm.com/) 進行版本控制 * 整合 [Valgrind](https://valgrind.org/) 動態程式分析工具,作為精準的記憶體分析和除錯 3. [ ] 除了原本對佇列的插入、刪除、反轉等操作外,額外要求針對佇列和鏈結串列撰寫排序程式,並思考高效能的實作; * 原本作業註解提示 `It should operate in O(1) time` 卻未有自動評分,本課程參照 [dudect](https://github.com/oreparaz/dudect) 工具及對應的論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf),提出以實驗而非理論驗證時間複雜度的方法 5. [ ] 支援 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),強化執行時期的記憶體檢測; ### q_new * Check returned value of `malloc`: 一開始的 `q_new` 十分單純只是需要判斷 `malloc` 是否失敗。但隨著後面功能的新增,介面新增了 `tail` 以及 `size`,因此須新增改兩個變數的初始化。(第八行及第九行) ```c= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); /* TODO: What if malloc returned NULL? */ /* ANS: Make the queue NULL */ if (q) { q->head = NULL; q->tail = NULL; q->size = 0; } else { q = NULL; } return q; } ``` ### q_free * Free `queue_t`, `list_ele_t`, and string in `list_ele_t`: 由於有三種結構會需要使用動態配置記憶體,因此在釋放記憶體需要三種結構都進行,我採用 linked list 上從頭走訪並釋放,釋放 `list_ele_t` 中的字串優先,但接著要刪除自己本身時,會遺失下一個連結位置,因此我們我們先暫存位置至 `past_ele_ptr` ,先移動至下一個連結位置再將前一個元素釋放掉。 ```c= /* Free all storage used by queue */ void q_free(queue_t *q) { /* TODO: How about freeing the list elements and the strings? */ /* Free queue structure */ /* It consists queue_t, list_ele_t, and string in list_ele_t */ list_ele_t *curr_ele_ptr; if (q) { curr_ele_ptr = q->head; } else curr_ele_ptr = NULL; while (curr_ele_ptr) { list_ele_t *past_ele_ptr = curr_ele_ptr; free(curr_ele_ptr->value); curr_ele_ptr = curr_ele_ptr->next; free(past_ele_ptr); } free(q); } ``` ### q_insert_head * `strncpy`, `strlen` 字串處理函數的使用: 在 `q_insert_head` 中最要小心的部份就是 C 語言的字串拷貝,在這邊我先用 `strlen` 算出字串長度,並加上 `\0` 的字串結尾,配置完記憶體再採用 `strncpy` ,此版本提供了上限,較 `strcpy` 安全。但實際上比較讓人擔心的問題是,當作輸入的 `char* s` 是否為一個正確的字串,有無正確使用 `\0` 在字串尾端。本版本還需要對無法正確配置字串進行處理,若無法配置則須釋放已配置記憶體。 ```c= bool q_insert_head(queue_t *q, char *s) { + if (!q) + return false; + list_ele_t *newh; /* TODO: What should you do if the q is NULL? */ newh = malloc(sizeof(list_ele_t)); + /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ - newh->next = q->head; - q->head = newh; - return true; + if (newh) { + size_t len = strlen(s) + 1; + newh->value = malloc(sizeof(char) * len); + if (newh->value) { + strncpy(newh->value, s, len); + } else { + free(newh); + return false; + } + // Update q when all data are ready + newh->next = q->head; + q->head = newh; + return true; + } else + return false; } ``` ### q_insert_tail * 介面修改: 作業內提到需要能夠 `O(1)` 複雜度,因此不進一步修改 queue 介面無法達成。 ```c= /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; size_t size; } queue_t; ``` * `q_create_elem`: 由於 `q_insert_head` 與 `q_insert_tail` 有部份程式碼相同,因此我抽象化出 `q_create_elem` ,但是有兩個東西需要回傳包含創建是否成功及新產生的 `list_ele_t` 位置。因此我採用了一個參數 `list_ele_t **newelem` 來得到配置的位址。 ```c= bool q_create_elem(queue_t *q, char *s, list_ele_t **newelem) { if (!q) return false; /* TODO: What should you do if the q is NULL? */ *newelem = malloc(sizeof(list_ele_t)); /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ if (*newelem) { size_t len = strlen(s) + 1; (*newelem)->value = malloc(sizeof(char) * len); if ((*newelem)->value) { strncpy((*newelem)->value, s, len); } else { free(*newelem); return false; } return true; } else return false; } ``` * `q_insert_head` 使用 `q_create_elem` 改寫: ```c= bool q_insert_head(queue_t *q, char *s) { list_ele_t *newelem; bool ret = q_create_elem(q, s, &newelem); if (ret) { // Update q when all data are ready if (q->head == NULL) q->tail = newelem; newelem->next = q->head; q->head = newelem; q->size++; } return ret; } ``` * `q_insert_tail` 使用 `q_create_elem` 改寫: ```c= bool q_insert_tail(queue_t *q, char *s) { list_ele_t *newelem; bool ret = q_create_elem(q, s, &newelem); if (ret) { // Update q when all data are ready newelem->next = NULL; if (q->head == NULL) { q->head = newelem; q->tail = newelem; } else { q->tail->next = newelem; q->tail = newelem; } q->size++; } return ret; } ``` ### q_remove_head * 使用較安全的 `strncpy` , 避免字串沒有正確結尾 採用 `sp[bufsize - 1] = 0` ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* TODO: You need to fix up this code. */ /* TODO: Remove the above comment when you are about to implement. */ if (q) { if (q->size > 0) { if (q->tail == q->head) q->tail = NULL; if (sp) { strncpy(sp, q->head->value, bufsize); sp[bufsize - 1] = 0; } free(q->head->value); list_ele_t *pasthead = q->head; q->head = q->head->next; free(pasthead); q->size--; return true; } else return false; } else return false; } ``` ### q_size * 使用 `size` 欄位: 在各種操作中,務必要持續維護正確的 `size` ```c= int q_size(queue_t *q) { /* TODO: Remove the above comment when you are about to implement. */ if (q) return q->size; return 0; } ``` ### q_reverse * 循序走訪即可,但需要保留前一個元素位置: 前一個元素位置我保存在 `oldprev` ,當全部走訪完畢,再交換 `queue_t` 中的 `head` 與 `tail` ```c= void q_reverse(queue_t *q) { /* TODO: Remove the above comment when you are about to implement. */ if (q) { list_ele_t *curr, *oldprev, *oldnext; curr = q->head; oldprev = NULL; while (curr) { oldnext = curr->next; curr->next = oldprev; oldprev = curr; curr = oldnext; } // Swapping oldprev = q->head; q->head = q->tail; q->tail = oldprev; } } ``` ### q_sort * 第一版採用 bubble sort: 氣泡排序只需要用兩個迴圈,把最大的一直往後排,因此能很快實做出排序,供第二版比較使用。要進行交換時有可能會與開頭元素相關,因此我使用 Jserv 老師 在課堂提到 Linus Torvalds 的小技巧。我這邊也使用了 `prev_indirect` 來進行元素的交換。 ```c= void q_sort_slow(queue_t *q) { // Use bubble sort if (q) { list_ele_t *curr; for (int remaining = q->size; remaining > 0; remaining--) { curr = q->head; list_ele_t **prev_indirect = &(q->head); int iter = 0; while (curr) { iter++; if (iter > remaining) break; // compare and swap if (curr->next) { int cmp = strcmp(curr->value, curr->next->value); if (cmp > 0) { list_ele_t *oldnextnext; if (curr->next == q->tail) q->tail = curr; // *prev_indirect -> curr -> currnext *prev_indirect = curr->next; oldnextnext = curr->next->next; curr->next->next = curr; curr->next = oldnextnext; } } prev_indirect = &(curr->next); curr = curr->next; } } } } ``` * 第二版 merge sort (使用 `size` ) 其中 merge sort 會需要將鍊結串列切成兩段,我是會利用 `size` 將串列分成前 `size/2` 元素留在`head` 其他在 `second`。 ```c= list_ele_t *list_merge_sort(list_ele_t *head, size_t size) { if (size < 2) return head; size_t firstsz = size / 2; size_t secondsz = size - firstsz; // split to two parts list_ele_t *second = head; for (int i = 0; i < firstsz - 1; i++) second = second->next; list_ele_t *tmp = second; second = second->next; tmp->next = NULL; list_ele_t *outfirst = list_merge_sort(head, firstsz); list_ele_t *outsecond = list_merge_sort(second, secondsz); return join_result(outfirst, outsecond); } ``` * 第二版 merge sort 中的 join_result 單純從頭開始尋訪,然後將兩個串列中小的元素挑出來,實做中還是會分是否為第一個元素,未來會想把此處進行修改 `if (head == NULL)` 。 ```c= list_ele_t *join_result(list_ele_t *first, list_ele_t *second) { if (!first) return second; if (!second) return first; list_ele_t *head = NULL; list_ele_t *curr = NULL; while (first && second) { int cmp = strcmp(first->value, second->value); if (cmp > 0) { if (head == NULL) { head = second; curr = head; } else { curr->next = second; curr = curr->next; } second = second->next; } else { if (head == NULL) { head = first; curr = head; } else { curr->next = first; curr = curr->next; } first = first->next; } } if (first) { curr->next = first; } if (second) { curr->next = second; } return head; } ``` * 第二版 merge sort 遇到的 bug 在實作時,花了三四個小時除蟲也與 `bubble sort` 進行比較,但最花時間的臭蟲竟然是忘記更新 `q->head`。 ```c= void q_sort(queue_t *q) { if (q && q->head) { if (q->size >= 2) { list_ele_t *result = list_merge_sort(q->head, q->size); q->head = result; // !! update the head while (result) { q->tail = result; result = result->next; } } } } ``` ### Github * https://github.com/tungtylee/lab0-c [ca835f0f] ``` +++ TESTING trace trace-17-complexity: # Test if q_insert_tail and q_size is constant time complexity Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 100/100 ``` ### 心得 第一次參與該課程作業撰寫,只有簡單將queue相關功能完成。更有趣的將會再自行學習。 * 執行時期的記憶體檢測 * Valgrind 動態程式分析工具 * dudect 工具 ## 暫存區(待整理) ### pre-commit hook * 作業說明提到引入 pre-commit hook, 因此隨意進行 commit 會跳出訊息 * Commit bad commit messages ``` $ git commit -am 'AAAA' Following files need to be cleaned up: queue.c AAAA [line 1] - Possible misspelled word(s): AAAA - Capitalize the subject line - Do no write single worded commits How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ Proceed with commit? [e/n/?] n ``` * clang-format 並沒有被強制使用 ``` [!] queue.c does not follow the consistent coding style. Make sure you indent as the following: clang-format -i queue.c Following files need to be cleaned up: queue.c ``` ### Static analysis: variableScope, memleak, unusedFunction 其實本專案包含使用 `cppcheck` 對開發幫助非常大,特別是 `[memleak]` , 減少開發時期產生的臭蟲。 * [variableScope] ``` queue.c:31:32: style: The scope of the variable 'past_ele_ptr' can be reduced. [variableScope] list_ele_t *curr_ele_ptr, *past_ele_ptr; ``` * [memleak] ```c bool q_insert_head(queue_t *q, char *s) { if (!q) return false; list_ele_t *newh; /* TODO: What should you do if the q is NULL? */ newh = malloc(sizeof(list_ele_t)); /* Don't forget to allocate space for the string and copy it */ /* What if either call to malloc returns NULL? */ if(newh) { size_t len = strlen(s); newh->value = malloc(sizeof(char)*len); if(newh->value) { strncpy(newh->value, s, len); } else return false; //pdate q when all data are ready newh->next = q->head; q->head = newh; return true; } else return false; } ``` ``` queue.c queue.c:69:13: error: Memory leak: newh [memleak] return false; ^ Fail to pass static analysis. ``` * [unusedFunction] ``` Following files need to be cleaned up: queue.c queue.c:194:0: style: The function 'q_sort_slow' is never used. [unusedFunction] ^ Fail to pass static analysis. ``` ### 其他 * Our computer will always crash when I try to issue `make test` twice