620 likes | 789 Views
第6章 函數與巨集. 6-1 由上而下的設計方法 6-2 建立函數 6-3 函數的參數呼叫方式 6-4 變數的有效範圍 6-5 遞迴函數 6-6 C 語言的巨集 6-7 C 語言的標準函式庫. 6-1 由上而下的設計方法-基礎1. 模組化主要是針對解決問題的方法,把一件大型的工作切割成無數的小工作,切割的工作屬於一種結構化分析的範疇,我們最常使用的是「由上而下的設計方法」( Top-down Design), 其主要是使用程序為單位來切割工作,也就是所謂的「程序式程式設計」( Procedural Design)。. 6-1 由上而下的設計方法-基礎2.
E N D
第6章 函數與巨集 • 6-1 由上而下的設計方法 • 6-2 建立函數 • 6-3 函數的參數呼叫方式 • 6-4 變數的有效範圍 • 6-5 遞迴函數 • 6-6 C語言的巨集 • 6-7 C語言的標準函式庫
6-1 由上而下的設計方法-基礎1 • 模組化主要是針對解決問題的方法,把一件大型的工作切割成無數的小工作,切割的工作屬於一種結構化分析的範疇,我們最常使用的是「由上而下的設計方法」(Top-down Design),其主要是使用程序為單位來切割工作,也就是所謂的「程序式程式設計」(Procedural Design)。
6-1 由上而下的設計方法-基礎2 • 由上而下的設計方法是在面對問題時,先考慮將整個解決問題的方法分解成數個大「模組」(Modules),然後針對每一個大模組,一一分割成數個小模組,如此一直細分,最後等這些細分小問題的小模組完成後,再將它們組合起來,如此一層層的向上爬,完成整個軟體系統或應用程式的設計。
6-1 由上而下的設計方法-注意事項 • 獨立性:每一個分割模組間的關聯性愈少,處理起來就會愈快。所謂獨立性,是指當處理某一個子問題時,無需考慮到其它子問題。換一句話說,獨立性是要將每一個問題都定義成一件簡單且明確的問題。 • 結合問題:小心的控制子問題間的結合方法,而且要注意結合這些子問題的邏輯順序,避免語焉不詳的結果。 • 子問題間的溝通:雖然獨立性可以減少各問題間的關聯性,但是並無法避免掉全部的溝通。
6-1 由上而下的設計方法-實例 • 例如:目前有一個工作是繪出房屋的圖形,如下圖所示:
6-1 由上而下的設計方法-第一步 • 從房屋繪圖工作可以粗分為三個小工作,如下所示: • 繪出屋頂和外框。 • 繪出窗戶。 • 繪出門。
6-1 由上而下的設計方法-第二步 • 接著將第一個小工作【繪出屋頂和外框】(Draw Outline)再次進行分割成二個小工作,如下所示: • 繪出屋頂。 • 繪出房屋的外框。
6-1 由上而下的設計方法-繼續步驟 • 只需重複上述分析,持續一步一步的向下分割工作,例如:因為窗戶共有2個,所以【繪出窗戶】可以分為【繪出窗戶1】和【繪出窗戶2】,而【繪出門】可以分為【繪出門框】和【繪出門把】。 • 最後,在將問題分割成一個個小問題後,每一個小問題就是一個C語言的函數,只需完成這些函數即可解決整個繪出房屋的問題。
6-2 建立函數 • 6-2-1 函數是一個黑盒子 • 6-2-2 建立C語言的函數 • 6-2-3 函數的原型宣告 • 6-2-4 函數的參數 • 6-2-5 函數的傳回值
6-2 建立函數 • C語言的模組單位是「函數」(Functions),函數是一個獨立的程式單元,使用函數可以將大工作分割成一個個小型的工作,也可以重複使用以前已經建立的函數或直接呼叫C語言標準函式庫的函數。
6-2-1 函數是一個黑盒子-說明 • 在C語言的程式敘述執行函數稱為「函數呼叫」(Functions Call),事實上,程式設計者並不需要了解函數內部實際的程式碼,也不想知道其細節,函數如同一個「黑盒子」(Black Box),只要告訴程式設計者如何使用這個黑盒子的「使用介面」(Interface)即可。
6-2-1 函數是一個黑盒子-圖例 • 圖例可以看出呼叫函數只需知道需要傳入的參數,然後從函數取得什麼傳回值,這就是函數和外部溝通的使用介面,實際函數內容的程式碼是隱藏在使用介面後,函數實際內容的程式碼撰寫稱為「實作」(Implementations)。
6-2-1 函數是一個黑盒子-規則 • 函數的使用介面需要直接、良好定義和容易了解。 • 在使用函數時,並不需要知道任何有關內部實作的問題,唯一需要知道的是如何使用它的使用介面。 • 在實作程序時,並不用考量或知道到底是誰需要使用此函數,只需滿足使用介面定義的輸入參數和傳回值即可。
6-2-1 函數是一個黑盒子-語法與語意 • 函數的「語法」(Syntactic)是說明函數需要傳入何種資料型態的「參數」(Parameters)和傳回值。 • 「語意」(Semantic)是指出函數可以作什麼事? • 撰寫函數時,需要了解函數的語法規則,而呼叫函數時需要了解其語意規則,如此才可以正確的呼叫函數。
6-2-2 建立C語言的函數-語法 • C語言的函數是由函數名稱和程式區塊所組成,其語法格式如下所示: 傳回值型態 函數名稱( 參數列 ) { 程式敘述; …… return 傳回值; } • 傳回值型態是函數傳回值的資料型態,函數名稱如同變數命名方式由設計者自行命名,函數使用return關鍵字傳回函數值。
6-2-2 建立C語言的函數-範例 • 一個沒有參數列和傳回值的函數,如下所示: void writeString() { printf("歡迎使用C/C++!\n"); } • 在括號內定義傳入的參數列,不過這個函數並沒有任何參數,所以空白,也可以使用void,如下所示: void writeString(void) { }
6-2-2 建立C語言的函數-呼叫 • 在C語言的程式碼呼叫函數需要使用函數名稱,其語法格式如下所示: 函數名稱( 參數列 ); • 因為前述函數writeString()沒有傳回值和參數列,所以呼叫函數只需使用函數名稱,如下所示: writeString();
6-2-3 函數的原型宣告-語法 • ANSI-C語言的函數分為「宣告」(Declaration)和「定義」(Definition)兩個部分,範例Ch6-2-2.c的函數程式區塊是實際的函數定義,程式並沒有宣告函數,這是因為呼叫函數的程式碼位在定義之後,所以並不需要先行宣告。 • 如果呼叫函數的程式碼是在函數定義之前,就需要在程式開頭宣告函數的原型,其語法格式如下所示: 傳回值型態 函數名稱( 參數列 );
6-2-3 函數的原型宣告-實例 • 函數原型宣告是程式碼,在最後需加上「;」分號,如下所示: void writeString(void); void one2Five(); • 程式碼是writeString()和one2Five()函數的原型宣告,因為沒有參數,可以使用空白或void表示沒有參數。 • 擁有參數的函數原型宣告,如下所示: void printTriangle(char, int); void one2N(int);
6-2-4 函數的參數-說明 • 函數的參數列是一個資訊傳遞的機制,可以從外面將資訊送入函數的黑盒子,參數列是函數的使用介面。函數如果擁有參數列,在呼叫時,因為傳入不同的參數值,就可以產生不同的執行結果。
6-2-4 函數的參數-範例 • 一個擁有參數列的函數範例,如下所示: void printTriangle(char ch, int rows) { /* 變數宣告 */ int i, j; /* 巢狀迴圈列印三角形 */ for ( i = 1; i <= rows; i++ ) { for ( j = 1; j <= i; j++ ) printf("%c", ch); printf("\n"); } }
6-2-4 函數的參數-正式參數 • printTriangle()函數定義的參數稱為「正式參數」(Formal Parameters)或「假的參數」(Dummy Parameters)。 • 正式參數是識別字,其角色如同變數,需要指定資料型態,並且可以在函數的程式碼區塊中使用,如果參數不只一個請使用「,」符號分隔。
6-2-4 函數的參數-呼叫參數的函數與實際參數 • 函數擁有參數列,在呼叫時需要加上參數列(或稱為引數),如下所示: printTriangle('*', rows); • 上述呼叫函數的參數稱為「實際參數」(Actual Parameters),參數可以是常數值,例如:'*'、變數或運算式,例如:rows,其運算結果的值需要和正式參數定義的資料型態相同(編譯程式會強迫型態轉換成相同的型態),函數的每一個正式參數都需要對應一個同型態的實際參數。
6-2-5 函數的傳回值-語法 • C語言函數的傳回值型態不是void,而是指定的資料型態int或char等,就表示這個函數擁有傳回值。 • 因為函數在執行完程式區塊後,需要傳回值,傳回指令的語法格式如下所示: return 運算式;
6-2-5 函數的傳回值-範例 • 擁有傳回值的函數範例,如下所示: int n2N(int start, int end) { /* 變數宣告 */ int i; int total = 0; /* 迴圈敘述 */ for ( i = start; i <= end; i++ ) total += i; return total; }
6-2-5 函數的傳回值-呼叫 • 函數擁有傳回值,在呼叫時可以使用指定敘述取得傳回值,如下所示: total = n2N(start, end); • 程式碼的變數total可以取得函數的傳回值,變數total的資料型態與函數傳回值型態是相同的。
6-3 函數的參數呼叫方式 • 6-3-1 傳值的參數呼叫 • 6-3-2 傳址的參數呼叫
6-3-1 傳值的參數呼叫 • C語言傳值的參數呼叫只是將複製的參數值傳到函數,所以在函數存取參數值並不是原來傳入的變數,當然也就不會更改呼叫的變數值。 void swap(int x, int y) { int temp; temp = x; x = y; y = temp; }
6-3-2 傳址的參數呼叫 • C語言的傳址呼叫就是傳遞指標變數,指標變數是一個指向其它變數位址的變數,它是一個位址值,在參數的變數名稱前只需使用「*」號標示是指標變數,傳遞進函數的是參數的變數位址,而不是變數值,如下所示: void swap(int *x, int *y) { int temp; temp = *x; *x = *y; *y = temp; }
6-4 變數的有效範圍 • 6-4-1 區域與全域變數 • 6-4-2 靜態變數 • 6-4-3 暫存器變數
6-4 變數的有效範圍 • C語言名稱的「有效範圍」(Scope)是指該名稱(通常是指變數)在程式中可以存取的的程式碼區域。 • 例如:在函數中宣告的變數或參數都只可以在函數的程式區塊中存取,不同函數的同名變數是毫不相干的不同變數。
6-4-1 區域與全域變數 • C語言的變數範圍將影響變數值的存取,C語言的變數範圍,如下所示: • 區塊範圍(Block Variable Scope):在程式區塊宣告的變數,變數只能在區塊內使用,在區塊之外的程式碼並不能存取此變數。 • 區域變數範圍(Local Variable Scope):在函數內宣告的變數或參數,變數只能在宣告的程式區塊使用,在函數外的程式碼並無法存取此變數。 • 全域變數範圍(Global Variable Scope):如果是在函數外宣告的變數,在整個程式檔案都可以存取此變數,如果全域變數沒有指定初值,其預設值是0。
6-4-2 靜態變數 • 如果在函數的程式區塊宣告靜態變數,不同於其它區域變數,在離開函數時會消失,靜態變數會配置固定的儲存位置,在重複呼叫函數時,靜態變數值都會保留。 • 在函數將區域變數宣告成靜態變數,只需在變數前加上static關鍵字,如下所示: static int step = 0;
6-4-3 暫存器變數-說明 • C語言的暫存器變數是針對那些存取十分頻繁的變數,可以直接將變數置於CPU的暫存器,以便加速程式的執行,通常是使用在迴圈的計數器變數。只需在宣告變數前加上register關鍵字,就可以宣告暫存器變數,如下所示: register int i;
6-4-3 暫存器變數-限制 • 暫存器變數在使用上有一些限制,如下所示: • 暫存器變數只可以使用在區域變數或函數的參數。 • 暫存器變數允許使用的個數需視CPU的電腦硬體而定,而且只有少數變數可以宣告成暫存器變數。 • 編譯程式對於暫存器變數並不一定處理,不過就算我們將變數宣告成register也無所謂,編譯程式會自行決定是否處理。 • 暫存器變數並不能使用「&」取址運算子取得變數的位址。
6-5 遞迴函數 • 6-5-1 遞迴的基礎 • 6-5-2 遞迴的階層函數 • 6-5-3 河內塔問題
6-5-1 遞迴的基礎 • 遞迴的觀念主要是在建立遞迴函數,其基本定義,如下所示: 一個問題的內涵是由本身所定義的話,稱之為遞迴。 • 遞迴函數是由上而下分析方法的一種特殊的情況,因為子問題本身和原來問題擁有相同的特性,只是範圍改變,範圍逐漸縮小到一個終止條件。遞迴函數的特性,如下所示: • 遞迴函數在每次呼叫時,都可以使問題範圍逐漸縮小。 • 函數需要擁有一個終止條件,以便結束遞迴函數的執行,否則遞迴函數並不會結束,而是持續的呼叫自已。
6-5-2 遞迴的階層函數-說明 • 遞迴函數最簡易的應用是數學的階層函數n!,如下所示:
6-5-2 遞迴的階層函數-過程1 • 例如:計算4!的值,從上述定義n>0,使用n!定義的第2條計算階層函數4!的值,如下所示: 4!=4*3*2*1=24 • 因為階層函數本身擁有遞迴特性。可以將4!的計算分解成子問題,如下所示: 4!=4*(4-1)!=4*3! • 現在3!的計算成為一個新的子問題,必須先計算出3!值後,才能處理上述的乘法。
6-5-2 遞迴的階層函數-過程2 • 同理將子問題3!繼續分解,如下所示: 3! = 3*(3-1)! = 3*2! 2! = 2*(2-1)! = 2*1! 1! = 1*(1-1)! = 1*0! = 1*1 = 1 • 最後在知道1!的值後,接著就可以計算出2!~4!的值,如下所示: 2! = 2*(2-1)! = 2*1! = 2 3! = 3(3-1)! = 3*2! = 3*2 = 6 4! = 4*(4-1)! = 4*3! = 24
6-5-2 遞迴的階層函數-函數 /* 函數: 計算n!的值 */ long factorial(int n) { if ( n == 1 ) /* 終止條件 */ return 1; else return n * factorial(n-1); }
6-5-3 河內塔問題-說明 • 「河內塔」(Tower of Hanoi)問題是程式語言在說明遞迴觀念時,不可錯過的實例,這是一個流傳在Brahma廟內的遊戲,廟內的僧侶相信完成這個遊戲是一件不可能的任務。河內塔問題共有三根木樁,如下圖所示:
6-5-3 河內塔問題-規則 • 共有n個盤子放置在第一根木樁,盤子的尺寸由上而下依序遞增。河內塔問題是將所有的盤子從木樁1搬移到木樁3,在搬動的過程中有三項規則,如下所示: • 每次只能移動一個盤子,而且只能從最上面的盤子搬動。 • 任何盤子可以搬到任何一根木樁。 • 必須維持盤子的大小是由上而下依序遞增。
6-5-3 河內塔問題-步驟 • 歸納出三個步驟,如下所示: • Step 1:將最上面n-1個盤子從木樁1搬移到木樁2。 • Step 2:將最後一個盤子從木樁1搬移到木樁3。 • Step 3:將木樁2的n-1個盤子搬移到木樁3。
6-5-3 河內塔問題-函數 /* 遞迴函數: 河內塔問題 */ void towerofHanoi(int dishs, int peg1, int peg2, int peg3) { if ( dishs == 1 ) /* 終止條件 */ printf("盤子從%d移到%d\n", peg1, peg3); else { /* 第二步驟 */ towerofHanoi(dishs-1, peg1, peg3, peg2); printf("盤子從%d移到%d\n", peg1, peg3); /* 第三步驟 */ towerofHanoi(dishs-1, peg2, peg1, peg3); } }
6-6 C語言的巨集 • 6-6-1 含括檔案(File Inclusion) • 6-6-2 巨集指令(Macro Substitution) • 6-6-3 條件式含括檔案或指定常數
6-6 C語言的巨集 • C語言的巨集是程式碼在編譯前透過「C的前置處理器」(The C PreProcessor)來處理,這是位在C程式檔開頭以「#」字元起頭的指令。 • 目前我們已經使用過#include和#define指令,更進一步還可以使用這些前置處理器指令建立「巨集」(Macro)。
6-6-1 引入檔案(File Inclusion) • C前置處理器的#include指令可以將其它程式檔案的內容含括到目前的程式檔案,含括的意義是將檔案內容直接複製到程式碼檔案,其指令格式如下所示: #include <檔案名稱.h> • 如果是自行定義的標頭檔案(通常是使用副檔名.h),檔案和C程式檔位在相同目錄時,可以使用引號括起檔案名稱,如下所示: #include "檔案名稱.h"