一次讀一列字元的函式:getline()

printf() 是一個能夠處理「字串」這種隱喻資料型態的函式。 只要依照「以零字元結束」這個要領,使用者也可以自行設計處理字串的函式。 在以下範例中,我們設計一個函式 getline(), 從標準輸入裝置 (stdin) 中讀取一列文字。


/* 從 stdin 讀取字元,到第一個 '\n' (含) 為止,放進 s[] 序列,
   返回讀進 s[] 的字元個數。但 s[] 的維度是 N,故一列的長度上限是 N-1 個字元
   (因為還要保留一個位置給 '\0')。
   若超過此限,截斷。
*/
getline(char s[], int N) {
    int c, i;

    for (i=0; i<N-1 && (c=getchar()) != EOF && c != '\n'; ++i)
        s[i] = c;
    if (c == '\n') {
        s[i] = c;
        ++i;
    }
    s[i] = '\0';
    return i;
}

注意,我們並沒有在 getline() 中宣告 s[]。 我們假設這是原函式的責任。 我們只知道 s[] 的維度是 N, 因此最多只能記錄 N 個字元。 因為要包含零字元,所以其實只能記錄輸入的 N-1 個字元。

檢視 getline() 的設計邏輯。 函式中的 for 迴圈在三種情形停止:

i == N-1
此時,c=getchar() 根本不會執行, 因為 i<N-1 是 FALSE,整個邏輯語句已經可以確定是 FALSE, 因此後面兩個語句都不會執行。 也就是說,我們不會輸入下一個字元。 而此時的 c 仍是前一個字元。 既然如此,那麼 c 必定不是 EOF 也不是 LF。
      在此情況下,輸入的一列文字長度超過了容許的字串長度, 我們將它截斷,逕自將 s[] 的最後一個元素: s[N-1] 設定成零字元。
(c=getchar()) == EOF
輸入之檔案結束。此時 s[0]s[i-1] 已經儲存了輸入的字元,只要將 s[i] 設定成零字元即可。
c == '\n'
一列結束。此時 s[0]s[i-1] 已經儲存了這一列字元的普通文字部分。 但我們要將 '\n' 也放進 s[], 所以將 s[i] 設定成 '\n',而 s[i+1] 設定成零字元。 因為 i<N-1 是 TRUE,所以 i 最大可能是 N-2, 因此 s[i+1] 不會超過預設的序列維度。
按照上面的邏輯,當此函式的指令結束時, i 正好是輸入字元中,含 '\n' 但是不含零字元的字元個數。 所以將這個值返回原函式。

注意,那三個用 && 合成的邏輯語句,它們的順序很重要。 因為,只要第一個是 FALSE,後面的 getchar() 就不會執行。 如果把 getchar() 寫在第一個,那麼可能會漏掉一個字元。

注意,根據以上的邏輯,當 getline() 讀到一個空行的時候, 返回的值是 1。因此即使空行也有一個 linefeed 字元。 由於 UNIX 純文字檔的 EOF 之前必定有一個 linefeed, 所以,當讀到 EOF 的時候,返回的值是 0。 因此,當原函式發現返回值是 0 的時候,就知道檔案結束了。

現在,我們寫一個簡單的 main() 來測試 getline() 函式。 我們設定一列不能超過 1024 個字元 (含 LF 但不含零字元)。 這個 main() 函式印出每列不含 LF 的字元數, 並印出那一列的字元。


#include <stdio.h>
#define BUFSIZE 1025

int getline(char[], int);

/* 輸出每一列的字元數 (不含 LF),以及那一列本身 (cline.c) */
main() {
    char buf[BUFSIZE];   /* 新讀入字元的暫存佇列 (buffer) */
    int n;
    
    while ((n = getline(buf, BUFSIZE)) > 0)
        printf("%6d: %s", n-1, buf);
    return 0;
}

getline(char s[], int N) {
    int c, i;

    for (i=0; i<N-1 && (c=getchar()) != EOF && c != '\n'; ++i)
        s[i] = c;
    if (c == '\n') {
        s[i] = c;
        ++i;
    }
    s[i] = '\0';
    return i;
}

在此程式中,我們設計一個序列來暫時儲存輸入的資料。 當資料被處理了之後,此序列就被用來儲存下一筆輸入的資料。 像這樣的設計,稱之為暫存佇列 (buffer)。 就好像鍵盤小廝把掃瞄碼送到電腦的傳達室時,也是先儲存在一個佇列中。 通常,佇列都有固定的大小,稱之為 buffer size。

習題

  1. 如何利用 UNIX 指令以及 cline.c 程式, 將一個純文字檔按照每列文字的長度排列? 從短的排到長的。
  2. getline() 的原函式中,如何判別它遇到了過長的一列?
  3. 解釋,為何以下函式,不論 BUFSIZE 是多少 (只要 >=2), 結果都相同?
    main() {
        char buf[BUFSIZE];
        int n;
       
        while ((n = getline(buf, BUFSIZE)) > 0)
            printf("%s", buf);
        return 0;
    }
    

  4. 改寫 cline.c,令 BUFSIZE=11 並且將 getline()for 條件改為
    for (i=0; (c=getchar()) != EOF && i<N-1 && c != '\n'; ++i)
    
    餵給它以下輸入檔案
    0123456789
    01234567890123456789
    012345678901234567890123456789
    

    觀察輸出會少掉一些字元,解釋為什麼會這樣?
  5. 寫一個程式,輸出一個純文字檔有哪些列的長度超過了 80 字元 (不含 linefeed)。 列的編號從 1 開始。
  6. 寫一個程式,輸入一個純文字檔,不輸出每一列最後面的連續空格或 TAB, 但所有其他字元照原樣輸出。假設輸入檔案中的每一列不超過 1024 個字元。 設計一個純文字檔案來測試您的程式。
  7. 寫一個程式,將一個純文字檔案內的段落,變成一列字串。 所謂段落,是以空列隔開的字元。若有空格或跳格,不算是空列。 在輸出的字串中,應該把原檔案內的 LF 都換成 \n, 雙引號 " 都換成 \",反斜線 \ 都換成 \\。 輸出的文字中,沒有空列。 參考:[ 測試用的輸入檔案 ]‧ [ 測試用的輸出結果 ]‧

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



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

Created: Apr 4, 2000
Last Revised: Apr 20, 2000, June 10
© Copyright 2000 Wei-Chang Shann 單維彰

shann@math.ncu.edu.tw