使用者自定外部函式

在這一節裡面,我們要示範如何自己寫一個函式,並且將它獨立出來, 使得它可以被許多不同的函式呼叫。 我們先看一個例子:s2i()。 它將一個字串 (string) 型態的資料,轉換為一個 int 型態的資料。

將以下原始碼獨立寫在一個檔案裡面,譬如叫 s2i.c


#define WHITE(c) (c == ' ' || c == '\t')
#define SIGN(c) (c == '+' || c == '-')
#define DIGIT(c) (c <= '9' && c >= '0')

/* s2i: convert string s to int (return value) */
int s2i(char s[]) {
    int i, n, sign;
    for (i=0; WHITE(s[i]); ++i) ;  /* skip leading spaces and tabs */
    sign = (s[i] == '-') ? -1 : 1;
    if (SIGN(s[i]))
        ++i;
    for (n=0; DIGIT(s[i]); ++i)
        n = 10*n + s[i] - '0';
    return sign*n;
}

這個函式定義了 3 個巨集,分別檢查字元是否為空白 (空格或跳格)、 是否為正負號、是否為數目字。 s2i() 從原函式獲得一個字串 s[]。首先,用指令
    for (i=0; WHITE(s[i]); ++i) ;  /* skip leading spaces and tabs */
把字串開頭部分的所有空白都略過。此時 s[i] 的值是一個非空白字元, 包括了 '\0''\n' 的可能性 (s[] 是空字串)。 然後,用
    sign = (s[i] == '-') ? -1 : 1;
檢查 s[i] 是不是負號。如果是的話,定義 sign = -1, 否則就定義 sign = -1。 所以不管此時的 s[i] 是什麼 (總之不是空白), sign 是有定義的了。 注意,直到此刻,i 都還未改變。 現在,才以
    if (SIGN(s[i]))
        ++i;
檢查 s[i] 是否為正負號,如果是,就略過,否則 i 還是不動。 接著,就以
    for (n=0; DIGIT(s[i]); ++i)
        n = 10*n + s[i] - '0';
來獲取數值了。一直重複執行到第一個非數目字的字元為止。 此時的 n 是那串表示十進制數字的值,不含正負號。 最後,用
    return sign*n;
將所求的答案返回原函式。

注意,s2i(s) 之中,即使 s[] 全是空白、或是空字串、 或者裡面只有一個正負號,則 signn 還是有定義, 分別是 1 和 0。所以,返回的值就是 0。 以上所說的,就是極端狀況。可見 s2i(s) 可以處理極端狀況, 至於處理是否得當,就不知道了。

再注意,s2i(s) 還可以接受更糟的輸入。 例如,輸入 "abcd" 則返回 0,輸入 "3.14" 則返回 3。 這些特色都是從原始碼之中可以觀察的。 將 "3.14" 返回 3,似乎是正確的反應。 但是,將 "abcd" 返回 0,就難以斷言是否正確了。 不過,這些問題都應該留給使用者自己去注意, 或者由原函式來注意。這個 s2i(s) 只負責做好它分內的工作。

s2i.c 存檔之後,讓我們寫個簡單的程式來測試它。 這個程式的原始碼如下,也將它獨立儲存在一個檔案內,譬如叫做 test_s2i.c


#include <stdio.h>
#include <string.h> 
int s2i(char[]);

/* Test of s2i()   (test_s2i.c) */
main() {
    char s[20]="2000";
    printf("%s\t%d\n", s, s2i(s));
    strcpy(s, "-2000");
    printf("%s\t%d\n", s, s2i(s));
    strcpy(s, "-");
    printf("%s\t%d\n", s, s2i(s));
    strcpy(s, "3.14");
    printf("%s\t%d\n", s, s2i(s));
    strcpy(s, "abcd");
    printf("%s\t%d\n", s, s2i(s));
    strcpy(s, "");
    printf("%s\t%d\n", s, s2i(s));
}

讀者應該看得出來,這個程式沒幹什麼,只是以各種狀況來測試 s2i() 而已。

現在我們示範,如何處理這些原始碼檔案。 首先,我們做個錯誤的示範

shell% gcc test_s2i.c
    In function `main':
    undefined reference to `s2i'
    more undefined references to `s2i' follow
    collect2: ld returned 1 exit status 
您看到的錯誤訊息或多或少像這個樣子。 這表示,C 不知道 s2i() 是什麼東西。 雖然在 test_s2i.c 裡面,我們以
int s2i(char[]);
宣告s2i(),但那只是告訴 C 說 s2i() 的規格是什麼, 卻沒有說明 s2i() 做什麼事? 換句話說,C 只知道 s2i() 的規格,不知道它的內容。

正確的做法是,先將 s2i() 的內容編譯成機器碼, 並將其機器碼獨立儲存在一個檔案裡面。如下

shell% gcc -c s2i.c
shell% ls
    ...     s2i.c    s2i.o    ...
在執行成功之後,您會發現多出來一個檔案,叫做 s2i.o, 那個 o 是 Object code 的意思,我們就稱它為 s2i.c 的機器碼。 除非它的原始碼 (s2i.c) 經過修改, 或是想要把它複製到不同的作業系統上執行, 否則機器碼只需製造一次,就可以重複使用。

譬如說,一台以 Pentium II 或 Pentium III 甚至 AMD 為 CPU 的個人電腦, 如果都執行 Windows 98,則被視為同一類型的電腦。 但是,同樣一台個人電腦,執行了 Windows NT 或 Linux 不同的作業系統, 就被視為不同類型的電腦。 但是,同樣是 UNIX 類型的作業系統, 若細分各種品牌,例如 Linux、Sun 的 Solaris 或 SunOS、IBM 的 AIX, 卻都是不同類型的。 總之,如果發現機器碼不能執行,重新編譯就對了。

好了,假使 s2i.o 已經存在,現在說

shell% gcc test_s2i.c s2i.o
就會產生新的 a.out 檔案,這時候,就可以執行它了:
shell% a.out
2000    2000
-2000   -2000
-       0
3.14    3
abcd    0
        0

如果一個函式的原始碼經過修改,那麼就要重新製造它的機器碼。 然後,所有用到它的機器碼的原函式,也都要重新編譯,才能讀入新的函式機器碼。 譬如說,如果因為任何理由修改了 s2i.c (加註解不算), 就應該要重新執行

shell% gcc -c s2i.c
shell% gcc test_s2i.c s2i.o
shell% a.out

習題

  1. 寫一個程式,從 stdin 讀入一個純文字檔案, 假設讀入的每一列是一個代表十進制整數的字串。 輸出這些整數之中的最大值和最小值。
  2. 寫一個程式,從 stdin 讀入一個純文字檔案, 假設讀入的每一列是一個代表十進制整數的字串。 輸出 (1) 總共有幾個數,(2) 它們的平均值,輸出到小數點下第二位。

[ 前一節 ]‧[ 後一節 ]‧[ 回目錄 ]



注意:此處所有文件均為原著,個別的版權宣告日後會一一公布, 整體版面設計亦尚未完成。但仍請勿抄襲文字與圖片,以免觸犯著作權法。

Created: May 18, 2000
Last Revised: May 18, 2000
© Copyright 2000 Wei-Chang Shann 單維彰

shann@math.ncu.edu.tw