Call by Name

相對於 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);
}

以上的範例中,x 原本是 int 型態的變數, 但是我們將 &x 傳入函式 test()。 因此,在 test() 接收到這個參數的時候, 它就已經是 int* 型態了。 因此我們宣告:
void test(int*);
而在 test() 裡面,x 是一個指標類型的變數, 因此 *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
我們看到,在 main()test() 裡面的那兩個 x 固然佔有不同的記憶體地址,但是 test() 裡面的指標 x 卻指向 main() 裡面的變數 x。 所以,在 test() 中做計算 *x = (*x)*(*x); 就真改變了 main() 裡面 x 的數值。

如果將序列變數當作函式的參數, 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() 函式的定義。 這個函式沒有函式值,而它需要一個字元型態的參數。 test() 將它的參數命名為 s。 而 s[] 表示 s 是一個序列。 但是 test() 並不指定 s 的序列維度。 這就是在一個函式的參數中定義序列的標準語法。

接著,請看 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[] 的序列維度。

習題

  1. 描述以下函式各需要幾個參數?哪些型態的參數?函式值是什麼型態?
    1. void one(float[]);
    2. two(float[]);
    3. int[] three(int, char, char[]);
    4. char[] four(void);
  2. 寫一個函式 reverse(), 使它能夠將一個 int 序列的前 n 個元素反過來排列。 再寫一個 main() 函式來示範您的 reverse 函式。
  3. 寫一個以下規格的函式
    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 單維彰

shann@math.ncu.edu.tw