--- tags: 你所不知道的 C 語言, 進階電腦系統理論與實作, NCKU Linux Kernel Internals, 作業系統 --- # 函式呼叫、遞迴呼叫 contributed by <`RusselCK` > ###### tags: `RusselCK` ## [函式呼叫篇](https://hackmd.io/@sysprog/c-function?type=view) * [History of C](http://en.cppreference.com/w/c/language/history) * C 語言不允許 nested function * function 和 data 一樣在 function 內部定義 * nested function 是編譯器的擴展 * 簡化了 C 編譯器的設計 * ==C 語言所有的 function 在語法層面都是位於最頂層 (top-level)== * callback function - [數學定義的 Function](https://www.cs.colorado.edu/~srirams/courses/csci2824-spr14/functionsCompositionAndInverse-17.html) #### Process 和 C 程式的關聯 :::success 背景知識: 1. IRQ (interrupt request) 2. ISR (Interrupt Service Routines) 3. IRQ mode 4. [MMIO (memory mapped I/O) v.s PMIO](https://www.itread01.com/content/1508402471.html) 以網路卡的流程為例: * 封包進來 -> interrupt -> ISR -> IRQ mode -> 下圖綠色的區塊裡面 (IORQ) 進行記憶體操作(讀取/寫入資料) ![image alt](https://images2017.cnblogs.com/blog/1094457/201710/1094457-20171019112241084-1805450176.png) ::: * runtime = 執行時期 - [ ] [The Internals of "Hello World" Program](http://www.slideshare.net/jserv/helloworld-internals) ![](https://i.imgur.com/te1ncIc.png) * bus ![](https://i.imgur.com/1qQkXA7.png) * PCI bus : 高速 ( 越靠近 CPU 越快 ) * ISA bus : 低速 ![](https://i.imgur.com/LEG4Lhs.png) ![](https://i.imgur.com/kMpQxLS.png) ![](https://i.imgur.com/CVvtBQL.png) ![](https://i.imgur.com/K9lurWJ.png) * 程式 不會 直接接觸 Memory * MMU * Virtual address 轉 Physical address ![](https://i.imgur.com/gtZIL7P.png) * 保護 資源 ![](https://i.imgur.com/fcAlgbf.png) * 寫、讀、執行 ![](https://i.imgur.com/C26aVUL.png) * Prepocessing、Compiling、Assembling、Linking ![](https://i.imgur.com/n77Z6lk.png) * Relocation ![](https://i.imgur.com/ljekoi4.png) * Symbol ![](https://i.imgur.com/oHLwGhs.png) * Symbol 對應到 Virtual address ![](https://i.imgur.com/BNks067.png) ![](https://i.imgur.com/cgvIqBj.png) * 如何把程式載下來? ![](https://i.imgur.com/fHyaWnb.png) ![](https://i.imgur.com/49AoJtg.png) * Loader * 從 磁碟( or 網路) 載入到 記憶體 ![](https://i.imgur.com/F5Iy6OE.png) * AS : Address Space * .text : 存放 機械碼 ( 對應有效的 CPU 指令 ) * 透過 loader 載入到 記憶體 - program break : 虚拟内存中数据段的结束位置 ![](https://i.imgur.com/UchG5jk.png) ![](https://i.imgur.com/35QqaHA.png) * 參考 動態連結篇 筆記 - function call 會佔用 stack 區域 ( FILO ) ![](https://i.imgur.com/DcQbFbc.png) * Return address ( 有去有回 ) * Arguments, Local variable, Return address ![](https://hackpad-attachments.s3.amazonaws.com/embedded2015.hackpad.com_2q5oxqltYTG_p.299401_1449482197657_undefined) * instructions: 自 object file (ELF) 映射 (map) 到 process 的 program code (機械碼) * static data: 靜態初始化的變數 * BSS: 全名已 ==不可考==,一般認定為 "Block Started by Symbol”,未初始化的變數或資料 * 可用 `size` 指令來觀察 * Heap 或 data segment: 執行時期才動態配置的空間 * sbrk 系統呼叫 (sbrk = set break)  * malloc/free 實際的實做透過 sbrk 系統呼叫 * [虚拟内存探究-- 第四篇:malloc, heap & the program break](http://blog.coderhuo.tech/2017/10/18/Virtual_Memory_malloc_and_heap/) - malloc通过调用brk或sbrk增加program break的值 ![](https://i.imgur.com/DU1pmS1.png) * video: [Call Stack](https://www.youtube.com/watch?v=5xUDoKkmuyw): 生動地解釋函式之間的關聯 - 直播影片中有不使用 gcc 便可觀察記憶體使用的演示 ```shell $ sudo cat /proc/1/maps | less 55cff6602000-55cff678b000 rw-p [heap] 7fff7e13f000-7fff7e160000 rw-p [stack] ``` #### 從遞迴學習 function call 1. `infinite.c` ```cpp int func() { static int count = 0; return ++count && func(); } int main() { return func(); } ``` 用 GDB 執行和測試,記得加上 `-g`: ```shell $ gcc -o infinite infinite.c -g $ gdb -q infinite Reading symbols from infinite...done. (gdb) r Starting program: /tmp/infinite Program received signal SIGSEGV, Segmentation fault. 0x00000000004004f8 in func () at infinite.c:3 3 return ++count && func(); (gdb) p count $1 = 524032 ``` :::warning * 出現 Segmentation fault ( 使用到錯誤的記憶體區域 ) * 但又沒有使用指標,哪裡出問題? * 【答案】上面的遞迴式沒有停下來的時候,會不斷的呼叫自己 ( 占用 stack ) * 直到 記憶體的 stack 不夠用 * Segmentation fault ::: :::success * 觀察 UNIX Process 中的 stack 空間: ```shell $ sudo cat /proc/1/maps | grep stack 7fff7e13f000-7fff7e160000 rw-p 00000000 00:00 0 [stack] ``` * 60000~Hex~ - 3f000~Hex~ = 21000~Hex~ = 135168~Dec~ 135168 * 4 = 540672 * 這跟前面的數字很接近! * 函式就是「做好」與「做滿」 ::: 2. 加入 參數 ( parameter ) ```cpp int func(int x) { static int count = 0; return ++count && func(x++); } int main() { return func(0); } ``` 將得到: ```shell Program received signal SIGSEGV, Segmentation fault. 0x0000000000400505 in func (x=1) at infinite.c:3 3 return ++count && func(x++); (gdb) p count $1 = 262016 ``` :::warning * 沒有解決 函式不斷呼叫自己的問題,但得到一個有趣的結果 * count 由 524032 減少到 262016 * 參數 ( parameter ) 占用 stack ::: 3. 加入 Local variable ```cpp int func(int x) { static int count = 0; int y = x; // local var return ++count && func(x++); } int main() { return func(0); } ``` 將得到以下: ```shell Program received signal SIGSEGV, Segmentation fault. 0x00000000004004de in func (x=<error reading variable: Cannot access memory at address 0x7fffff7fefec>) at infinite.c:1 1 int func(int x) { (gdb) p count $1 = 174677 ``` :::warning * 沒有解決 函式不斷呼叫自己的問題,但又得到一個有趣的結果 * count 由 262016 減少到 174677 * 參數 ( parameter ) 占用 stack * Local variable 也占用 stack ::: * stack 裡面有 * x (parameter) * y (local variable) * return address :::info * return value 會存放在 暫存器 ( e.g. $v0, $v1 in MIPS ) * 那如果回傳值為 `struct` , 暫存器塞不下怎麼辦? 【解答】 將 `struct`所在位址 存入 暫存器 ::: - [ROP (Return Oriented Programming)](https://yojo3000.github.io/CTF/ROP-Return-Oriented-Programming/) - 該技術允許攻擊者在安全防禦的情況下執行代碼 - stack 和 heap 裡的程式碼都是 rw- ( 可讀可寫不可執行 ) - 幹壞事是進步的原動力 #### 資安問題 : stack-based buffer overflow * [CVE-2015-7547](https://access.redhat.com/security/cve/cve-2015-7547) > vulnerability in glibc’s DNS client-side resolver that is used to translate human-readable domain names, like google.com, into a network IP address. [ [解說](http://thehackernews.com/2016/02/glibc-linux-flaw.html) ] * [Buffer Overflow : Example of Using GDB to Check Stack Memory](http://www.cs.ucf.edu/~czou/CAP6135-12/bufferOverFlow-gdb-example.ppt) 準備 `gdb-example.c`,其內容為: ```cpp #include <stdio.h> void foo(char *input) { int a1 = 11; int a2 = 22; char buf[7]; strcpy(buf, input); } void main(int argc, char **argv) { foo(argv[1]); } ``` 編譯,記得加上 `-g` 和 `-m32` 用 i686 架構執行 GDB: ```shell $ setarch i686 -R gdb -q ./gdb-example (gdb) break 6 Breakpoint 1 at 0x804847a: file gdb-example.c, line 6. (gdb) run “whatever” Starting program: /tmp/gdb-example "whatever" Breakpoint 1, foo (input=0xffffd406 "whatever") at gdb-example.c:6 6 strcpy(buf, input); ``` 觀察 stack 內容: ```shell (gdb) info frame Stack level 0, frame at 0xffffd180: eip = 0x8048490 in foo (gdb-example.c:6); saved eip = 0x80484da called by frame at 0xffffd1b0 source language c. Arglist at 0xffffd178, args: input=0xffffd40c "whatever" Locals at 0xffffd178, Previous frame’s sp is 0xffffd180 Saved registers: ebp at 0xffffd178, eip at 0xffffd17c (gdb) x &a1 0xffffd15c: 0x0000000b (gdb) x &a2 0xffffd160: 0x00000016 (gdb) x buf 0xffffd165: 0x44f7f9c0 (gdb) x 0xffffd160 0xffffd160: 0x00000016 ``` ...自己玩 #### 藏在 Heap 裡的細節 * ==free() 釋放的是 pointer 指向位於 heap 的連續記憶體== * 而非 pointer 本身佔有的記憶體 (`*ptr`)。 1. 舉例來說: ```cpp #include <stdlib.h> int main() { int *p = (int *) malloc(1024); free(p); free(p); return 0; } ``` 編譯不會有錯誤,但運作時會失敗: ``` *** Error in './free': double free or corruption (top): 0x000000000067a010 *** ``` :::info * **`void free (void* ptr);`** * Deallocate memory block * A block of memory previously allocated by a call to `malloc`, calloc or realloc is deallocated, making it available again for further allocations. * If `ptr` does not point to a block of memory allocated with the above functions, it causes undefined behavior. * ==If `ptr` is a null pointer, the function does nothing.== - 由於 alignment 議題,`malloc()` 可能會配置比預期還多的記憶體 * 參考 記憶體對齊 筆記 ::: 2. 倘若改為以下: ```cpp #include <stdlib.h> int main() { int *p = (int *) malloc(1024); free(p); p = NULL; free(p); return 0; } ``` 則會編譯和執行都成功。 * 為了防止對同一個 pointer 作 free() 兩次的操作,而導致程式失敗, ==`free()` 後應該設定為 `NULL`==。 :::warning * 為什麼 glibc 可以偵測出上述程式的 "double free or corruption" 呢? ::: ...影片後面老師有試玩 #### malloc / free **在 GNU/Linux 裡頭觀察 malloc** 事先安裝 gdb 和包含除錯訊息的套件 ```shell $ sudo apt install gdb gdb-dbg ``` GDB 操作: ```shell $ gdb -q `which gdb` Reading symbols from /usr/bin/gdb... (gdb) start ... Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe4c8) at ./gdb/gdb.c:25 ... (gdb) p ((double(*)())pow)(2.,3.) $1 = 8 (gdb) call malloc_stats() Arena 0: system bytes = 135168 in use bytes = 28000 Total (incl. mmap): system bytes = 135168 in use bytes = 28000 max mmap regions = 0 max mmap bytes = 0 $2 = -168929728 (gdb) call malloc_info(0, stdout) <malloc version="1"> <heap nr="0"> ``` * glibc 提供了 `malloc_stats()` 和 `malloc_info()` 這兩個函式,可顯示 process 的 heap 資訊 * `malloc_trim()` ## [遞迴呼叫篇](https://hackmd.io/@sysprog/c-recursion?type=view) *「[遞迴(recurse)只應天上有,凡人該當用迴圈(iterate)](http://coder.aqualuna.me/2011/07/to-iterate-is-human-to-recurse-divine.html)」* * [遞迴 (Recursion)](https://notfalse.net/9/recursion) * [Recursive Programming](https://www.cs.cmu.edu/~adamchik/15-121/lectures/Recursions/recursions.html) #### 遞迴程式沒有你想像中的慢 * 最大公因數 (Greatest Common Divisor,縮寫 GCD) 典型的演算法為輾轉相除法,又稱歐幾里得算法 (Euclidean algorithm) 1. 遞迴的實作 ```cpp unsigned gcd_rec(unsigned a, unsigned b) { if (!b) return a; return gcd_rec(b, a % b); } ``` 2. 迭代的實作 ```cpp unsigned gcd_itr(unsigned a, unsigned b) { while (b) { unsigned tmp = b; b = a % b; a = tmp; } return a; } ``` :::warning * 迭代比遞迴多了 ==區域變數== 的使用 * 在 平行運算 ( 多核、雲端 等 ) 的環境下,為了追蹤這個 區域變數 ,會需要額外的空間來記錄,降低效能 ::: - 分析 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 ``` * 顯然遞迴版本的原始程式碼簡潔,而且對應於輾轉相除法的==數學定義==。 #### 尾端遞迴 (tail recursion) * 這是實務上一種重要的遞迴種類,指的是具備: 1. 直接遞迴 (direct recursion) 2. 線性遞迴 (linear recursion) 3. 該 函式執行的最後一件事 為遞迴呼叫,且 只能是遞迴呼叫 * 其中,第三點尤其重要: * 最後執行的內容,一定要豪乾淨的, * 只有呼叫遞迴,不能做其他事! :::warning * 可將 遞迴途中的變數 包進 遞迴式的參數 * 達到 Tail Recursion 的模式 * 使 編譯器 能夠進行 Tail Call Optimization, TCO ::: * Head recursion #### 案例分析: 等效電阻 :::info | | 公式 | |:----:|:---------------------------------------------:| | 串聯 | $r = r_1 + r_2$ | | 並聯 | $\frac{1}{r} = \frac{1}{r_1} + \frac{1}{r_2}$ | ::: ``` r r r -----###-----------###----------- . . . --------###------------- A | | | | | # # # # # r # r # r # # r # # # # # # | | | | | ---------------------------------- . . . ------------------------ B 1 2 3 n - 1 ``` 可視為 ``` r ----------###------------- A -------- A | | | # # # R(r, n - 1) # r # ==> # R(r, n) # # # | | | --------------------------- B -------- B ``` * 數學推導: $$ \frac{1}{R(r,n)} = \frac1r + \frac1{R(r, n - 1) + r} $$ * The recursive function is therefore: $$ R(r,n)= \begin{cases} r & \text{if n = 1}\\ 1 / (\frac1r + \frac1{R(r, n - 1) + r}) & \text{if n > 1} \end{cases} $$ * it is possible to show that $$ R(r,n) = \frac{r F(2n - 1)}{F(2n)}, $$ where $F$ is the Fibonacci function. ```python def circuit(n, r): if n == 1: return r; else: return 1 / (1 / r + 1 / (circuit(n - 1, r) + r)) ``` #### 腦力激盪:數列輸出 1. 以下 C 程式的作用為何? ```cpp= int p(int i, int N) { return (i < N && printf("%d\n", i) && !p(i + 1, N)) || printf("%d\n", i); } ``` * 程式碼分析 : ```clike=2 (i < N && printf("%d\n", i) && !p(i + 1, N)) ``` * `&&` 的運算規則為**左值優先** * ==只要左值不成立,右值都不會執行== * 若 `i` $\geq$ `N` ,不會印出 `i`,也不會呼叫 `p` * 若 `i` < `N` ,`i` 會被輸出,接著會呼叫 `p` * 換言之,遞迴成立條件是 `i` < `N` :::info * [C Operator Precedence](http://en.cppreference.com/w/c/language/operator_precedence) * [printf(3)](https://linux.die.net/man/3/printf) 會回傳印出的字元數目,這裡一定回傳非零值 * 查詢 man page,對應到 GNU/Linux 的指令為 `man 3 printf` ::: ```clike=3 || printf("%d\n", i) ``` * `||` 的運算規則為==當左值成立時,右值就不會執行== * 因此 **`p` 前面的 `!` 很重要**。 * 因為 p 的回傳值一定是 true (由於 `printf` 一定回傳非零值),而為確保會執行到這第二個 `printf`,要將回傳值作 NOT,讓第一部份的結果為 false * 如果去掉 NOT,則只會輸出 `i -> N` :::info 把 `!p(i+1, N)` 的反相`!` 拿掉後,得到以下結果: ``` /* i = 1 , n = 5 */ 1 2 3 4 5 ``` ::: * 第二個 `printf` 要等 `p` 執行完畢才會被執行。 * 遞迴呼叫在實做層面來說,就是藉由 stack 操作,這裡一旦被 push 到 stack 印一次,被 pop 出來再印一次 * 綜合以上分析,我們可得知上述程式碼的作用為 **「印出 i -> N -> i 的整數序列,N 只會出現一次」**。 2. 延伸問題: 把 `&&` 前後的敘述對調,是否影響輸出? 從 `i < N && printf("%d\n", i)` 改為 `printf("%d\n", i) && (i < N)` * 如果對調 `printf` 跟 `i < N` 則會**輸出 `i -> N N -> i` (N 出現兩次)** * 因為 `printf` 會先被執行,不會受到 `i < N` 成立與否影響。 3. 延伸問題:`i` 和 `N` 組合的有效上下界為何? * 不妨將原本程式碼改寫為以下: ```cpp #include <limits.h> #include <stdio.h> int p(int i, int N) { return (i < N && printf("%d\n", i) && !p(i + 1, N)) \ || printf("%d\n", i); } int main() { return p(0, INT_MAX); } ``` * 在 Linux x86_64 環境編譯並執行後,會得到類似以下輸出: (數字可能會有出入) ``` 261918 261919 261920 程式記憶體區段錯誤 ``` * 這裡的 segmentation fault 意味著 stack overflow,遞迴呼叫時,每一層都有變數存放於 stack 中,每次呼叫也要保存回傳位址 (return address) * 為了滿足 [x86_64 ABI / calling convention](https://en.wikipedia.org/wiki/X86_calling_conventions), * 回傳位址佔 8 bytes, * (int) `i` 和 (int) `N` 這兩個變數合計 8 bytes, * 函式的區域變數 (給 `printf()` 用的 format string 的位置) 8 bytes, * p 回傳值 int 佔 4bytes , * 總共 32 bytes。 * 計算過程: ( 假設 可用 stack size 為 8MB ) ``` 8MB = 8,388,608 bytes 8,388,608 / 32 = 262,144 次 ``` * 綜合上述,推算出 `0 < N - i < 262144` :::info * 可用 `ulimit -s` 來改 stack size,預設是 8 MB,計算後可推得,每個 stack 大約佔用 32 bytes,N 的限制取決於 stack size 的數值。 * 延伸閱讀: [通過 ulimit 改善系統效能](https://www.ibm.com/developerworks/cn/linux/l-cn-ulimit/) ::: #### [Fibonacci sequence](https://hackmd.io/nWwDmm64SyqmlhGpgNbbag?both#%E9%81%9E%E8%BF%B4%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88) * 數學定義如下: $F_n = F_{n-1}+F_{n-2},$ $F_0 = 0, F_1 = 1$ 數列值: | $F_0$ | $F_1$ | $F_2$ | $F_3$ |$F_4$|$F_5$|$F_6$|$F_7$|$F_8$|$F_9$|$F_{10}$|$F_{11}$|$F_{12}$|$F_{13}$|$F_{14}$|$F_{15}$|$F_{16}$|$F_{17}$| $F_{18}$|$F_{19}$|$F_{20}$| | ----| ---- | ---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- |---- | | 0| 1 |1|2|3|5|8|13|21|34|55|89|144|233|377|610|987|1597|2584|4181|6765| 1. 遞迴法 (Recursive) * 將 Fibonacci 數列定義 轉為程式碼 ```cpp int fib(int n) { if (n == 0) return 0; if (n == 1) return 1; return fib(n - 1) + fib (n - 2); } ``` ```graphviz digraph hierarchy { graph [nodesep="1"]; 11 [label="Fib(5)" shape=box]; 21 [label="Fib(4)" shape=box]; 22 [label="Fib(3)" shape=box]; 31 [label="Fib(3)" shape=box]; 32 [label="Fib(2)" shape=box]; 33 [label="Fib(2)" shape=box]; 34 [label="Fib(1)=1" shape=box]; 41 [label="Fib(2)" shape=box]; 42 [label="Fib(1)=1" shape=box]; 43 [label="Fib(1)=1" shape=box]; 44 [label="Fib(0)=0" shape=box]; 45 [label="Fib(1)=1" shape=box]; 46 [label="Fib(0)=0" shape=box]; 51 [label="Fib(1)=1" shape=box]; 52 [label="Fib(0)=0" shape=box]; 11 ->{21 22} 21 ->{31 32} 22 ->{33 34} 31 -> {41 42} 32 -> {43 44} 33 -> {45 46} 41 -> {51 52} } ``` 2. 非遞迴法 (Iterative) * 縮減原先使用遞迴法的函式呼叫造成之執行時期開銷。 * 先討論基本方法。 ```cpp int fib(int n) { int pre = -1; int result = 1; int sum = 0; for (int i = 0; i <= n; i++) { sum = result + pre; pre = result; result = sum; } return result; } ``` * 我們可減少變數的使用: ```cpp int fib(int n) { int pre = -1; int i = n; n = 1; int sum = 0; while (i > 0) { i--; sum = n + pre; pre = n; n = sum; } return n; } ``` 3. Tail recursion * 第一次 call function 後即開始計算值 * 大幅降低時間,減少 stack 使用量 ```cpp int fib(int n, int a, int b) { if (n == 0) return a; return fib(n - 1 , b, a + b); } ``` ```graphviz digraph hierarchy { nodesep=1.0 // increases the separation between nodes node [fontname=Courier,shape=box] //All nodes will this shape and colour "Fib(5,0,1)"->"Fib(4,1,1)" "Fib(4,1,1)"->"Fib(3,1,2)" "Fib(3,1,2)"->"Fib(2,2,3)" "Fib(2,2,3)"->"Fib(1,3,5)" "Fib(1,3,5)"->"return 5" } ``` 4. Q-Matrix $$ A^1 = \left( \begin{array}{ccc} 1 & 1 \\ 1 & 0 \\ \end{array} \right) = \left( \begin{array}{ccc} F_2 & F_1 \\ F_1 & F_0 \\ \end{array} \right) $$ $$ \begin{split} A^{n+1} &= AA^n \\ &= \left( \begin{array}{ccc} 1 & 1 \\ 1 & 0 \\ \end{array} \right) \left( \begin{array}{ccc} F_{n+1} & F_{n} \\ F_{n} & F_{n-1} \\ \end{array} \right) \\ \\ &= \left( \begin{array}{ccc} F_{n+1}+F_{n} & F_{n}+F_{n-1} \\ F_{n+1} & F_{n} \\ \end{array} \right) \\ \\ &= \left( \begin{array}{ccc} F_{n+2} & F_{n+1} \\ F_{n+1} & F_{n} \\ \end{array} \right) \end{split} $$ * Divide and Conquer Method $$ A^{2n+1} =A^{n+1}A^n $$ ```cpp int **matrix_multiply(int a[2][2],int b[2][2]) { int *t = malloc(sizeof(int) * 2 * 2); memset(t, 0, sizeof(int) * 2 * 2) for (int i = 0 ; i < 2 ; i ++ ) for (int j = 0 ; j < 2 ; j ++ ) for (int k = 0 ; k < 2 ; k ++) t[i][j] += a[i][k] * b[k][j]; return t; } // 使用 Divide-and-Conquer 方法加速矩陣次方 int **matrix_pow(int a[2][2] , int n) { if (n == 1) return a; if (n % 2 == 0) { int t[2][2]; t = matrix_pow(a , n >> 1); return matrix_multiply(t , t); } int t1[2][2], t2[2][2]; t1 = matrix_pow(a, n >> 1); t2 = matrix_pow(a, n >> 1 + 1); return matrix_multiply(t1, t2); } ``` ```cpp int fib(int n) { if (n < = 0) return 0; int A1[2][2] = { { 1 , 1 } , { 1 , 0 } }; int result[2][2]; result = matrix_pow(A1, n); return result[0][1]; } ``` 5. Fast doubling $$ \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^n = \begin{bmatrix} F(n+1) & F(n) \\ F(n) & F(n-1) \end{bmatrix} $$ $$ \begin{split} \begin{bmatrix} F(2n+1) \\ F(2n) \end{bmatrix} &= \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^{2n} \begin{bmatrix} F(1) \\ F(0) \end{bmatrix}\\ \\ &= \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^n \begin{bmatrix} 1 & 1 \\ 1 & 0 \end{bmatrix}^n \begin{bmatrix} F(1) \\ F(0) \end{bmatrix}\\ \\ &= \begin{bmatrix} F(n+1) & F(n) \\ F(n) & F(n-1) \end{bmatrix} \begin{bmatrix} F(n+1) & F(n) \\ F(n) & F(n-1) \end{bmatrix} \begin{bmatrix} 1 \\ 0 \end{bmatrix}\\ \\ &= \begin{bmatrix} F(n+1)^2 + F(n)^2\\ F(n)F(n+1) + F(n-1)F(n) \end{bmatrix} \end{split} $$ ```cpp int fib(int n) { if (n == 0) return 0; int t0 = 1; // F(n) int t1 = 1; // F(n + 1) int t3 = 1; // F(2n) int t4; // F(2n+1) int i = 1; while (i < n) { if ((i << 1) <= n) { t4 = t1 * t1 + t0 * t0; t3 = t0 * (2 * t1 - t0); t0 = t3; t1 = t4; i = i << 1; } else { t0 = t3; t3 = t4; t4 = t0 + t4; i++; } } return t3; } ``` * 演算法比較 | 方法 | 複雜度 | |:-------------- |:-----------:| | Recursive | $O(2^n)$ | | Iterative | $O(n)$ | | Tail Recursion | $O(n)$ | | Q-Matrix | $O(log\ n)$ | | Fast Doubling | $O(log\ n)$ | * [An integer formula for Fibonacci numbers](https://blog.paulhankin.net/fibonacci/) ![](https://i.imgur.com/IKgxobv.png) #### 腦力激盪:字串反轉 - 用 C 語言實做 `char *reverse(char *s)`,反轉 NULL 結尾的字串, 限定 **in-place** 與 **遞迴** * 先思考實做 swap 的技巧 * 參考 [Bitwise 筆記](https://hackmd.io/GSuuqlufRHGzar6WEFFPPg?both#4-XOR-swap) * 解法 I : 一直 swap '\0'之前的character * 演算法描述: 1. 先把首尾互換 ![](https://i.imgur.com/X0d2SxY.png) 2. 把尾端和'\0'互換 ![](https://i.imgur.com/bEB8Bt5.png) 3. 重複遞迴,直到'\0'在中間 ![](https://i.imgur.com/BpX1VrP.png) 4. 層層把'\0'換回最尾即可 ![](https://i.imgur.com/A94cmup.png) :::spoiler 過程示意 :::info a b c ==d== ==e== -> a b c ==e== ==d== a b ==c== ==e== d -> a b ==e== ==c== d a b e ==c== ==d== -> a b e ==d== ==c== a ==b== ==e== d c -> a ==e== ==b== d c a e b ==d== ==c== -> a e b ==c== ==d== a e ==b== ==c== d -> a e ==c== ==b== d a e c ==b== ==d== -> a e c ==d== ==b== ==a== ==e== c d b -> ==e== ==a== c d b e a ==c== ==d== b -> e a ==d== ==c== b e a d ==c== ==b== -> e a d ==b== ==c== e ==a== ==d== b c -> e ==d== ==a== b c e d a ==b== ==c== -> e d a ==c== ==b== e d ==a== ==c== b -> e d ==c== ==a== b e d c ==a== ==b== -> e d c ==b== ==a== ::: ::: ```cpp static inline void swap(char *a, char *b) { *a = *a ^ *b; *b = *a ^ *b; *a = *a ^ *b; } char *reverse(char *s) { if((*s == '\0') || (*(s + 1) == '\0')) return NULL; reverse(s + 1); swap(s, (s + 1)); if (reverse(s + 2) != '\0') reverse(s + 2); reverse(s + 1); } ``` * 程式碼解析: * 假設我們要反轉: $$ 5\space 4\space 3\space 2\space 1\space $$ 1. $reverse(s + 1)$ 先反轉後4個: $$ 5\space 1\space 2\space 3\space 4\space $$ 2. 然後再把1跟5對調: $$ 1\space 5\space 2\space 3\space 4\space $$ 3. 為了讓後四個能再利用$reserve(s + 1)$反轉,我們必須先將後三個調轉, * 也就是調用$reverse(s+2)$: $$ 1\space 5\space 4\space 3\space 2\space $$ 4. 這樣一來我們就能調用$reverse(s+1)$來調轉後四個: $$ 1\space 2\space 3\space 4\space 5\space $$ * 因此,$reverse(s)$可以拆成: * $reverse(s+1)$ -> $swap(s, s+1)$ -> $reverse(s+2)$ -> $reverse(s+1)$ - 缺點:時間複雜度為 $$ T(n) = O(n^2) $$ * 解法 II : 計算字串長度時,即可交換字元 ```cpp int rev_core(char *head, int idx) { if (head[idx] != '\0') { int end = rev_core(head, idx + 1); if (idx > end / 2) // 走到中間 swap(head + idx, head + end - idx); return end; } return idx - 1; // 回傳 字串長度 } char *reverse(char *s) { rev_core(s, 0); return s; } ``` * 時間複雜度降為 $$ T(n) = O(n) $$ #### 案例分析: 類似 find 的程式 * 給定 [opendir(3)](http://man7.org/linux/man-pages/man3/opendir.3.html) 與 [readdir(3)](http://man7.org/linux/man-pages/man3/readdir.3.html) 函式,用遞迴寫出類似 [find](https://man7.org/linux/man-pages/man1/find.1.html) 的程式 * find 的功能:列出包含目前目錄和其所有子目錄之下的檔案名稱 :::info * DIR *opendir(const char *name); * struct dirent *readdir(DIR *dirp); * dirent = direct entry ::: :::warning * opendir 回傳的型態為 pointer to DIR * 為什麼不 回傳 pointer to dirent 就好 ? * 反而是 先回傳 pointer to DIR , 然後再帶入 readdir ,最後再從回傳的 pointer to dirent 得到想要的資料 【解答】 * DIR 結構 會根據不同的作業系統及檔案系統 而有不同的落差 * dirent 結構 不論哪種環境下 定義都相同 * 因此,我們先用 opendir 確認好 DIR 的結構 * 再帶入 readdir 取出 我們要的資料 - 可兼顧 跨平台的差異 及 效能 ::: ```cpp #include <sys/types.h> #include <dirent.h> void list_dir(const char *dir_name) { DIR *d = opendir(dir_name); if (!d) return; /* fail to open directory */ while (1) { struct dirent *entry = readdir(d); // 下一層 if (!entry) break; const char *d_name = entry->d_name; printf ("%s/%s\n", dir_name, d_name); if ((entry->d_type & DT_DIR) // 檢查 型態是否為 目錄 && strcmp(d_name, "..") && strcmp(d_name, ".")) { char path[PATH_MAX]; int path_length = snprintf(path, PATH_MAX, "%s/%s", dir_name, d_name); printf ("%s\n", path); if (path_length >= PATH_MAX) return; // 檢查路徑名稱是否太長 /* Recursively call "list_dir" with new path. */ list_dir (path); } } if (closedir (d)) return; } ``` * 練習: 如何連同檔案一併列印? * 練習:上述程式碼實做有誤,額外的 `.` 和 `..` 也輸出了,請修正 source: [rd.c](https://www.lemoda.net/c/recursive-directory/rd.c) #### 案例分析: Merge Sort * [Program for Merge Sort in C](http://www.thecrazyprogrammer.com/2014/03/c-program-for-implementation-of-merge-sort.html): 內含動畫 * [非遞迴的版本](http://stackoverflow.com/questions/1557894/non-recursive-merge-sort) ![](https://i.imgur.com/XqHJUHw.gif) ![](https://i.imgur.com/XikIhSX.png) * [Merge Sort](http://www.personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/Sorting/mergeSort.htm): 複雜度分析 #### 函數式程式開發 * 進行中: [MapReduce with POSIX Thread](https://hackmd.io/s/Hkb-lXkyg) * Functional programming 在 [concurrency 環境](https://hackmd.io/s/H10MXXoT) 變得日益重要 * 如何透過軟體,讓 多核環境 更有效率