第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-2 建立C語言的函數-呼叫過程
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 函數的參數呼叫方式
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!繼續分解,如下所示: 最後在知道1!的值後,接著就可以計算出2!~4!的值,如下所示: 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。
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); /* 第三步驟 */ 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"
6-6-2 巨集指令(Macro Substitution)-語法 C前置處理器的#define指令除了定義常數,事實上,#define指令是巨集指令,可以定義新關鍵字或使用參數建立巨集函數。 #define巨集指令的格式,如下所示: #define 名稱 替換的內容
6-6-2 巨集指令(Macro Substitution)-範例 #define FOREVER for(;;) FOREVER是一個無窮的for迴圈。 巨集指令還可以加上參數建立巨集函數,如下所示: #define swap(x, y) { int _z; \ _z = y; \ y = x; \ x = _z; }
6-6-2 巨集指令(Macro Substitution)-執行過程 swap(x, y); swap()事實上是巨集函數,C前置處理器會將它展開成程式區塊,如下所示: { int _z; _z = y; y = x; x = _z; }
6-6-3 條件式含括檔案或指定常數-#if/#else/#endif條件 #if INC == 10 #define EDGE 2 #endif 當常數INC為10時,就指定常數EDGE。如果是二選一的條件,如下所示: #else #define EDGE 0
6-6-3 條件式含括檔案或指定常數-#elif條件 #if INC == 10 #define EDGE 2 #elif INC == 5 #define EDGE 1 #else #define EDGE 0 #endif
6-6-3 條件式含括檔案或指定常數-#ifdef或#ifndef條件 #ifdef OFFSET #define INC 5 #else #define INC 10 #endif 如果沒有定義就含括指定的標頭檔,如下所示: #ifndef PI #include "Ch6-6-1.h"
6-7 C語言的標準函式庫 6-7-1 亂數函數 6-7-2 數學函數
6-7 C語言的標準函式庫 ANSI-C語言定義的標準函式庫一共擁有十幾個函式庫,函數的原型宣告分別定義在這十幾標頭檔,每一個標頭檔的函數擁有特定功能,常用函式庫的簡介,如下表所示:
6-7-1 亂數函數 「亂數」(Random Numbers)是使用一個整數的種子數(Seed),然後使用數學公式所產生的一系列隨機數值,在<stdlib.h>標頭檔一共定義2個亂數函數,如下表所示:
6-7-2 數學函數-1 在<math.h>標頭檔提供各種三角函數(Trigonometic)、指數(Exponential)和對數(Logarithmic)的數學函數,所有函數的傳回值是double,其相關函數如下表所示:
6-7-2 數學函數-2
6-7-2 數學函數-3