文章程式碼顯示

2017年11月13日 星期一

《筆記》C語言 - 05:函式基本、傳值呼叫(call by value)


函式

C語言 裡面的模組成為函式或函數(function) 。 C 程式的撰寫通常是將程式設計師所寫的函式與位於 C 標準函式庫 ( C standard library ) 中 「先寫好的函式」結合起來構成一個程式。

我們在先前的程式中使用 #include "studio.h" 程式碼,目的就是告訴編譯器我們將要使用位於 C 標準函式庫內的函式。

在標準函式庫裡已經包含了很多常用的函式,例如字串處理、字元處理、輸入/輸出 ... 等。你可以把某個將在程式中許多位置用到的工作,撰寫成一個函式,這種函式稱為自訂函式( programmer-defined functions) 。

函式經由函式呼叫(function call) 的方式調用(invoked) 。

函式呼叫(或稱呼叫函式)指明了欲調用之函知名稱以及所需的資訊(引數, 或稱參數)

有三個動機誘使我們將常用的程式進行「函式化」

    1. 各個擊破的方法使得程式發展更容易管理

    2. 軟體可以重複性使用(software reusability)

    3. 避免程式中重複地撰寫相同的程式碼

在進行函式撰寫時我們應該要遵守「每一個函式只限制執行一項定義明確的工作」,而且函式的名稱應能夠充分反映出它所執行的工作。如果你無法選用一個恰當的名稱來代表函式在做些什麼,那麼可能是這個函式所被賦予的工作太多樣化。最好能將這種函式再切割成數個更小的函式。

接下來我們將學到如何建立一個簡單的函式

#include "stdio.h"

int square( int y ); /* square function prototype 函式原型*/

int main(void) {
setvbuf(stdout,NULL,_IONBF,0);

 int x;
 int result;

 printf( "Enter your number : " );
 scanf( "%d", &x );

 result = square( x ); /* 函式呼叫 */
 printf( "The answer is %d\n", result );


 return 0;
}

/* square function 函式標頭 */
int square ( int y ){
 return y * y;
}


在此程式中 square 函式在 main 裡面第 14 行被調用(invoked) 或稱被呼叫(called),稱為"呼叫函式"。

函式 square 用它的參數(parameter) y 接收了一份來自呼叫函式的複製品 x  ( 第 14行的 x 會複製到第 22 行) 。然後 square 函式會進行 y * y 的計算( 23行 ),並且將計算的結果 return (傳回)給呼叫函式 (第 14 行) 。

我們通常將第 22 行稱為 "函式標頭(function header)"

第 3 行稱為"函式原型(function prototype)"括號內的 int 告訴編譯器square 希望接收一個整數的參數(引數)。而函式名稱之前的 int 則告訴編譯器, square 會回傳一個整數值給呼叫函式

編譯器會根據"函式原型"檢查呼叫函式,看它是否使用正確的回傳型別、引數的數目是否正確、引數的型別是否正確、引數的排列順序是否正確

另外一點,函式原型的主要作用在於告訴編譯器,它所指定的函式可能 "定義" 在本檔案稍後的位置,或定義在不同的檔案中

並且注意,函式原型與函式標頭(第 3 與 22 行) 在此例中不應該被包含在 main 函式的大括號內,而是獨立的

我們用另一個例子來做練習

#include "stdio.h"

int multiplier( int num1, int num2 ); /* multiplier function prototype */

int main(void) {
setvbuf(stdout,NULL,_IONBF,0);

 int x1;
 int x2;
 int result;

 printf( "Enter your first number : " );
 scanf( "%d", &x1 );
 printf( "Enter your second number : " );
 scanf( "%d", &x2 );

 result = multiplier( x1, x2 );
 printf( "The answer is %d\n", result );

 return 0;
}

/* multiplier function */
int multiplier ( int num1, int num2 ){
 return num1 * num2;
}


由第 3 行的函式原型(或可由第 24 行的函式標頭) 看出 multiplier 是一個具有兩個輸入參數的函式,且兩個輸入參數都應該是整數型態的變數,且回傳的值也是一個整數型態。

函式的大括號內構成了函式的本體(function body) ,或稱區塊(block)。

函式原型、函式呼叫、函式標頭,這三個部分對於引數參數的數目、型別、和順序,以及回傳值型別應該要一致。

接下來我們談談當我們進入一個副函式(函式呼叫)的時候,在什麼狀況下才會跳回原本函式呼叫的地方?

有三種方法可以將控制權返回調用函式(函式呼叫)的位置

  1. 如果函式並沒有回傳值,直接使用 return; 取回控制權

  2. 如果函式並沒有回傳值,則當到達函式終止的右大括號 } 時,控制權便自動回到函式呼叫的位置。也就是說在沒有回傳值的狀況下 return; 可以被忽略不寫

  3. 如果函式有回傳值的話,則使用 return expression; 將 expression 的值傳回給函式呼叫。

expression 可以是一個運算式(例如第 25 行);也可以不是一個運算式,例如我們可以在函式的本體中將計算過程保留在一個變數裡,最後再用 return 的方式將這個變數傳回給函式呼叫。


函式原型

  如前面所說的,函式原型會告訴編譯器有關函式傳回值的資料型別、函式希望接收到的參數個數、參數的型別、參數的排列順序。編譯器會利用函式原型來驗證函式呼叫。

我們應該為所有的函式建立函式原型,以便使用編譯器之型別檢查功能。

  當我們具有多個函式時,就可以自己建立標頭檔(.h),裡頭存放著函式原型,並寫上一些註釋來幫助使用者知道該如何使用。將函式標頭及主體,也就是這個函式內部演算法的部分存放在 .cpp 檔之中,這就是我們一般所說的 "建立自己的函式庫" 。

  函式原型的另一項功能是強制的引數型別轉換(coercion of arguments),亦即強迫引數變成適當的型別。我們在撰寫函式時為了通用性,可能在函式呼叫中並不會使用規定的引數型態,但我們前面又有說過,在函式呼叫時的引數應該要符合當初設計該函數時所規定的型態。此時我們可以在函式原型的地方置入強制型別轉換,以符合格式的需求。

但型別的轉換具有一定的限制,胡亂使用可能導致資料的錯誤(詳細可查詢 C的提升規則 promotion rules),我們可以掌握一個大原則如下

  1. 整數能轉換成浮點數,但浮點數最好別轉換成整數(會因小數點捨去造成數值計算上的誤差)

  2. 較少位元長度的資料型態可以轉換成較多位元長度的資料型態(例如 int 可以轉換成 unsigned int )

傳值呼叫(call by value)

#include "stdio.h"

void call_by_value(int number);

int main(void) {

 int number = 10; /* local */

 printf("Number in main %d\n",number);

 call_by_value(number);

 printf("Now,Number in main %d\n",number);

 return 0;
}

void call_by_value(int number){
 number = 20;
 printf("Number in call_by_value %d\n",number);
}



在前面的例子中,我們的函式都具有 "引入參數" ,這種具有引入參數的函式呼叫就稱為傳值呼叫(call by value)

call_by_value 需具有引入參數,回傳則可有可無。

當我們在 main 函式裡面定義一個變數 number 為 10 ,接著我們在 main 函式裡面以 call_by_value 的方式呼叫函式 。(第 11 行)

此時,在電腦的記憶體內部會暫時生出一個記憶體位置(假設就叫 '老王家' 好了),並將位在 main 函式裡面的 number 複製一份副本存放在這裡,然後 call_by_value 函式再從老王家調用這個副本,因為 call_by_value 函式裡面的 number 是一個複製品,所以當我們在 call_by_value 函式內對 number 進行任何的操作運算,在老王它家亂搞一通,最終都不會影響到原來在 main 函式內的 number 。

順帶一提,如果 call_by_value 具有回傳值,則當 call_by_value 運算完畢後,main 函式會從老王家調用這個值(複製品,副本),但是在 main 裡面的 number 還是保持不變。

綜合上述我們知道

若 call_by_value 沒有回傳値,則 main 函式在 call_by_value 函式結束後,不會自動去調用副本的値來覆蓋原有的値;

若call_by_value 有回傳値, 我們必然會用一個變量來去接收這個函數的回傳,而這個回傳的値會從副本裡面來提取。


#include "stdio.h"

int call_by_value(int number);

int main(void) {

 int number = 10; /* local */
 int number1;

 printf("Number in main %d\n",number);

 number1 = call_by_value(number);

 printf("Now,Number in main %d\n",number);
 printf("Now,Number1 in main %d\n",number1);

 return 0;
}

int call_by_value(int number){
 number = 20;
 printf("Number in call_by_value %d\n",number);
 return number;
}






↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引