# 2018q3 Homework1 contributed by < `brad84622` > # 開發工具和規格標準篇 - 從 C99 開始, C 語言與 C++ 就分道揚鑣 - C99 用 Designated initializers 而 C++ 認為該由 Constructor 執行,因 Constructor 實作很可能發生在main之前 ## ISO/IEC 9899 (簡稱C99) - [C 語言規格書](http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1570.pdf)能澄清很多誤解 ``` clike int main(){ char str[] = "Hello World"; int *p = &str; printf("%x %x\n", str, p + 1); } ``` - 上式可以正確運行 ``` clike int main(){ char str[] = "Hello World"; int *p = &str; printf("%x %x\n", str, p / 1); } ``` - 但下式會出現 ==invalid operands to binary==,為什麼?  這就要看C99規格書,**6.5.3.3 Unary arithmetic operators**部分,就會發現在**Address and indirection operators**的部分只提供 `+` , `-` , `~` , `!` 這幾種運算 這就是為什麼我們需要閱讀規格書,而不是腦補"應該"有哪些功能 - Sizeof() 是Operator,而非Function # 指標篇 - C語言只有Call-by-Value - C語言中執行時期明確佔用空間的,就是物件(object) - Array, function, and pointer types都被稱為derived declarator types ## `void *` - `void *` 與 `char *` 應有相同的表示法 ```clike int main() { void *A = 0; char B = *(char *) A; //需要強制轉型 } ``` - 但在存取時需要強制轉型,否則無法編譯過,因 void* 並無佔有明確的空間 ## 指標的指標(a pointer of a pointer) - 是個常見用來改變 **「傳入變數原始數值」** 的技巧 - 考慮以下 2 段程式碼: ```Clike 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); //*ptrA=1 return 0; } ``` ```Clike 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); //*ptrA=2 return 0; } ``` - 上段副本p之lifetime僅於func內,而下段副本p之lifetime卻在於整個main >中英文、數字間都應以空白隔開 >[name=課程助教][color=red] ## Pointers vs. Arrays - in declaration - extern, 如 extern char x[]; => 不能變更為 pointer 的形式 - definition/statement, 如 char x[10] => 不能變更為 pointer 的形式 - parameter of function, 如 func(char x[]) => 可變更為 pointer 的形式 => func(char *x) - in expression - array 與 pointer 可互換 - 在取值時,array 的行為與 pointer 幾乎一樣,但 array 會是用兩步取值,而 pointer 是三步。(array 的位址本身加上 offset,共兩步,而使用 pointer時,cpu 需先載入 pointer 位址,再用 pointer 的值當作位址並加上 offset 取值) :::info 避免用「兩步」或「三步」這類不精準用語,可作點實驗,對照輸出的組合語言需要幾道指令 :notes: jserv ::: 範例如下 ```clike= int main() { int Array[3]={0,1,2}; int *p; p=Array; int B=Array[2]; int C=*(p+2); } ``` 其轉成組合語言為 ```= main: push rbp mov rbp, rsp mov DWORD PTR [rbp-28], 0 mov DWORD PTR [rbp-24], 1 mov DWORD PTR [rbp-20], 2 lea rax, [rbp-28] mov QWORD PTR [rbp-8], rax mov eax, DWORD PTR [rbp-20] // 9 - 10 行為 int B=Array[2]; mov DWORD PTR [rbp-12], eax mov rax, QWORD PTR [rbp-8] //11 - 13 行為 int C=*(p+2); mov eax, DWORD PTR [rax+8] mov DWORD PTR [rbp-16], eax mov eax, 0 pop rbp ret ``` 可以看出同樣是取Array的第三個值 **2**,pointer(11-13行)在組合語言會比array(9-10行)多出一道指令 # 函式呼叫篇 ## 藏在 Heap 裡的細節 - free() 釋放的是 pointer 指向位於 heap 的連續記憶體,而非 pointer 本身佔有的記憶體 (*ptr)。 :::warning :question: 請問: ```clike #include <stdlib.h> int main() { int *p = (int *) malloc(1024); free(p); free(p); return 0; } ``` 照[函式呼叫篇](https://hackmd.io/s/SJ6hRj-zg)所言,double free執行應該會出現以下失敗訊息 ``` *** Error in './free': double free or corruption (top): 0x000000000067a010 *** ``` 但我在windows10及ubuntu18.04都能執行,請問為什麼呢? ::: :::info 參見 [Diagnosing Memory Heap Corruption in glibc with MALLOC_CHECK_](https://www.novell.com/support/kb/doc.php?id=3113982),設定好 `MALLOC_CHECK_` 環境變數 (注意有底線),再做實驗 :notes: jserv ::: # 遞迴呼叫篇 ## 遞迴程式沒有你想像中的慢 - 因現代的編譯器最佳化技術,遞迴的程式不一定比迭代 (iterative) 的版本來得慢。 好比找最大公因數(GCD)常用的歐幾里得演算法(輾轉相除法),以下為範例。 - [ ]遞迴版本 ```clike unsigned gcd_rec(unsigned a, unsigned b) { if (!b) return a; return gcd_rec(b, a % b); } ``` - [ ]迭代版本 ```clike unsigned gcd_itr(unsigned a, unsigned b) { while (b) { unsigned tmp = b; b = a % b; a = tmp; } return a; } ``` - 分析 clang/llvm 編譯的輸出 (參數 -S -O2),發現兩者轉譯出的 inner loop 的組合語言完全一樣: ``` LBB0_2: movl %edx, %ecx xorl %edx, %edx divl %ecx testl %edx, %edx movl %ecx, %eax jne LBB0_2 ``` - 但遞迴版本的原始程式碼更簡潔,且對應輾轉相除法的數學定義。 # goto 和流程控制 ## switch 背後的 goto 和實作考量 - switch 可依據整數索引值進行多重分支 (multiway branching),不僅能提高 C 程式的可讀性,而且相較於大量的 if-else 敘述,switch 透過跳躍表 (jump table) 的實作技巧可提升效率。 - 考慮到以下程式: ```clike void switch_eg(long x, long n, long *dest) { long val = x; switch (n) { case 100: val *= 13; break; case 102: val += 10; /* Fall through */ case 103: val += 11; break; case 104: case 106: val *= val; break; default: val = 0; } *dest = val; } ``` - 為了有效處理 switch 一類的多重分支,GCC 引入 computed goto 這個 GNU extension,可以將 label 的記憶體地址存入 void * 型態的 pointer 中,之前 switch_eg 函式的程式碼在 GCC 內部會轉換成以下等價程式: ```clike void switch_eg_impl(long x, long n, long *dest) { /* table of code pointers */ static void *jt[7] = { &&loc_A, &&loc_def, &&loc_B, &&loc_C, &&loc_D, &&loc_def, &&loc_D, }; unsigned long index = n - 100; long val; if (index > 6) goto loc_def; /* multiway branch */ goto *jt[index]; loc_A: /* case 100 */ val = x * 13; goto done; loc_B: /* case 102 */ x = x + 10; /* Fall through */ loc_C: val = x + 11; goto done; loc_D: /* case 104, case 106 */ val = x * x; goto done; loc_def: /* default case */ val = 0; done: *dest = val; } ``` - 比對 switch_eg 和 switch_eg_impl,原程式對 100, 102 到 104,以及 106 情況給予特定的處置,變數 n 可以是任意有效範圍內的整數。 編譯器首先將 n 減去 100,以便讓取值範圍移到 0 到 6 之間,並透過新建立的變數 index 作為跳躍表的索引值,而跳躍表正是由 ==computed goto== 組成。 # linked list 和非連續記憶體操作 ![](https://i.imgur.com/Qof9hCw.png) - 3 exceptional cases, we need to take care of: - list is empty - delete the head node - node is not in the list ## 從刪除 linked-list node 看程式設計的品味 - 原本的程式碼 ```clike 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; } ``` - 有品味的程式碼 ```clike 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; } ``` - 從「要更新什麼位置的資料」思考,無論是 head 或者非 head,更新的是同一類型的資料,不用特別操作,自然省下額外的處理 # 技巧篇 ## 明確初始化特定結構的成員 - C99 規格給予許多便利之處,比如: ```clike const char *lookup[] = { [0] = "Zero", [1] = "One", [4] = "Four" }; assert(!strcasecmp(lookup[0], "ZERO")); ``` - 也可變化如下: ```clike enum cities { Taipei, Tainan, Taichung, }; int zipcode[] = { [Taipei] = 100, [Tainan] = 700, [Taichung] = 400, }; ``` ## C99 Variable Length Arrays ```clike void f(int m, int C[m][m]) { double v1[m]; ... #pragma omp parallel firstprivate(C, v1) ... } ``` - Visual C++ 不目前支援可變長度陣列 - 一個特例是 [Arrays of Length Zero](https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html) # 心得 作為一個土木系的跨考生,以往寫程式都僅以能夠正確執行為目標,沒有考慮過規格、可讀性、執行效能...等等的議題,經歷這次作業有種重新學程式的感覺。 :::info 在這個世紀,只要你做不出有強度 (讓上萬人每天使用的資訊系統) 的成果,就是「非本科」,反之,你過去做了什麼都沒關係。 :notes: jserv ::: 看了教材才知道有太多不了解的地方,也讓我知道自己最大的問題是「==不知道自己問題在哪==」,以前不覺得是問題的地方才知道有著大學問,為了搞懂這些問題就去google,爬到不懂的地方又再google下去,雖然花費很多時間,但也因此了解到很多知識。 這次教材花費時間比我預計多出很多,一部分是自己根基不穩所導致,也可惜沒來得及全部看完,之後希望能陸續將其補完。