相對於 call by value,另外一種傳遞函式參數的機制, 稱作 call by name (名呼叫) 或是 call by reference。 採用這種機制時,在被呼叫之函式內改變參數值, 就真的在原函式 (caller, 或稱為「呼叫者」) 內改變了相對的變數值。
C 語言其實只提供 Call by Value 的參數傳遞機制。 但是對初學者而言,也許還不能在此時瞭解。 因此,我們就技術的表面來說,有兩種 Call by Name 的機制。 一種是利用指標,另一種是透過序列。 先簡單地介紹指標的一種用法 (還有多種用法,但不在這裡多說)。
#include <stdio.h> /* 示範 call by name (test-callname-ref.c) */ void test(int*); main() { int x=2; printf("x of main() before = %d\n", x); printf("x of main() at %u\n", &x); test(&x); printf("x of main() after = %d\n", x); } void test(int *x) { printf("\tx of test() at %u\n", &x); printf("\tx of test() points to %u\n", x); *x = (*x)*(*x); }
void test(int*);而在 test() 裡面,x 是一個指標類型的變數, 因此 *x 就是所指的數值。
執行上述程式,得到結果
我們看到,在 main() 和 test() 裡面的那兩個 x 固然佔有不同的記憶體地址,但是 test() 裡面的指標 x 卻指向 main() 裡面的變數 x。 所以,在 test() 中做計算 *x = (*x)*(*x); 就真改變了 main() 裡面 x 的數值。x of main() before = 2 x of main() at 4026530500 x of test() at 4026530468 x of test() points to 4026530500 x of main() after = 4
如果將序列變數當作函式的參數, C 就自動採用了 call by name 的機制。 以下是一個沒有什麼實用價值的例子, 它有兩個目的:示範字元序列、示範序列變數在函式之間的傳遞。
#include <stdio.h> /* 示範 call by name (test-callname-arr.c) */ void test(char[]); main() { char i, h[14]; h[0]='h'; h[1]='e'; h[2]=h[3]=h[10]='l'; h[4]=h[8]='o'; h[5]=','; h[6]=' '; h[7]='w'; h[9]='r'; h[11]='d'; h[12]='.'; h[13]='\n'; for (i=0; i<14; ++i) putchar(h[i]); test(h); for (i=0; i<14; ++i) putchar(h[i]); return 0; } void test(char s[]) { s[0]='H'; s[12]='!'; }
接著,請看 test() 的宣告指令:
void test(char[]);這個宣告的意思是: test() 是一個函式,它沒有函式值, 而它需要一個參數,此參數是 char 型態的序列。 那個參數的名字,此時還不必說。同理,
int[] 是宣告函式需要一個 int 型態的序列作為參數
float[] 是宣告函式需要一個 float 型態的序列作為參數
double[] 是宣告函式需要一個 double 型態的序列作為參數
以上的 main() 函式,宣告一個維度 14 的 char 序列。 然後定義序列中每個字元的值。 第一次用 for 迴圈輸出 h[] 的結果應該是
hello, world.但是我們將 h[] 送進 test() 函式之後, 第二次用 for 迴圈輸出 h[] 的結果就成了
Hello, world!可見 test() 函式內對序列參數 s[] 所做的修改, 直接影響了原函式 main() 中 h[] 的值。
即使前述兩個序列變數的名字不同: 在 main() 裡面叫做 h[], 在 test() 裡面叫做 s[]。 但是,當我們採用 call by name 的機制傳送參數的時候, 這兩個名字不同的序列其實是同一個序列。 就好像 h[] 整個被搬移到 test() 去, 換個名字叫 s[],執行完了再整個搬移回來。
讓我們做個簡短的結論: 這種將數值 從一個函式搬移到另一個函式 去執行的作法, 稱為名呼叫 (call by name)。
C 函式的參數,凡是序列變數,都採用名呼叫。
這裡,我們用比較技術的方式再解釋一遍。 當 main() 宣告
char h[14];並且開始定義 h[] 的值的時候, 作業系統為這個程式在記憶體中保留了一段連續 14 個 bytes 的位址。 比如說,它們是從 1024000 開始 (含) 的連續 14 個 bytes。 例如 h[7] 就是使用 1024007 這個位址的記憶體來儲存資料。 在前面的範例中, main() 在 1024007 號位址放了小寫字母 w 的 ASCII 二進制數,也就是 01110111。 當 main() 將 h[] 傳遞給 test() 的時候,採用了 call by name 的機制, 而此處所謂的 name 就是 h[] 在記憶體中的位址。 在計算機裡面,一個變數的位址,才是它真正的名字。 所以,在 test() 裡面的 s[], 也同樣地代表了從 1024000 開始 (含) 的連續 14 個 bytes。 因此,當 test() 將 s[0] 設定成 'H' 的時候, 就真的將 1024000 這個位址的資料改變了。 等到 main() 第二次要輸出 h[0] 的時候, 就會印出 H。
最後,我們要提醒讀者注意, test() 不知道也不理會 s[] 的序列維度。 這是寫程式的人應該要自行負責的事。 test() 只管把 s[0] 的值定義成 H 字元, 把 s[12] 的值定義成 ! 字元。
如果想要 test() 知道 s[] 的序列維度, 必須要使用另一個參數來傳達這個消息。例如說
void test(char s[], int N)而 N 就是 s[] 的序列維度。
習題
void printia(int s[], int N, int m)取得一個 int 型態的序列 s[],它的維度是 N, 以每列 m 個元素將它們列印出來, 最後一列可以不滿 m 個,但是要記得輸出 LF。 直行不必對齊。 例如 printia(s[], 7, 5); 會輸出
s[0] s[1] s[2] s[3] s[4] s[5] s[6]並且寫一個簡單的主函式來測試您的 printia() 函式。
注意:此處所有文件均為原著,個別的版權宣告日後會一一公布, 整體版面設計亦尚未完成。但仍請勿抄襲文字與圖片,以免觸犯著作權法。
Created: Apr 1, 2000
Last Revised: Feb 28, 2001
© Copyright 2001 Wei-Chang Shann 單維彰