C語言字串輸入完整教學|scanf、fgets與getline的安全用法與範例

目次

1. 前言

C語言是學習程式設計基礎時非常重要的語言。其中,「字串輸入」是在接收使用者輸入資料時不可或缺的功能。本文將詳細解說C語言中的字串輸入方法,並介紹安全處理的技巧與注意事項。

特別是對初學者來說,在處理輸入的字串時,常常會遇到錯誤或安全性風險而感到困惑。因此,本文將從基本函式到進階程式碼範例廣泛涵蓋,目標是幫助讀者掌握實用技能。

只要正確理解並能安全地實作C語言的字串輸入,就能邁出撰寫更高階程式的第一步。那麼,接下來就進入具體內容吧。

2. 什麼是C語言的字串輸入?基本概念解析

什麼是字串?

在C語言中,字串以字元陣列的形式表示。字串的最後必須附加「 」這個終止符號,用來表示字串的結束。透過這個特性,C語言在處理字串時不需要明確指定長度。

字串與陣列的關係

在C語言中,字串實際上就是字元型別(char型)的陣列。例如,可以這樣宣告字串:

char str[10];  // 準備一個可容納10個字元的字串緩衝區

在這個例子中,會分配一塊記憶體來存放最多10個字元的字串。但其中有1個字元要保留給終止符號「 」,因此實際可存放的字元數為9個。

字串常值的範例

字串常值是用雙引號(”)括起來的字串。以下是使用字串常值的範例:

char greeting[] = "Hello";

在這種情況下,greeting會自動被視為大小為6(”Hello”+終止符號 )的陣列。

為什麼需要字串輸入?

在程式中,經常需要接收使用者輸入的資料。例如姓名或地址的登錄、搜尋關鍵字的輸入等,字串輸入是應用程式介面中不可缺少的功能。因此,理解如何安全且有效率地處理字串非常重要。

3. 字串輸入的基本函式與使用範例

3-1. scanf函式

scanf函式的基本用法

scanf函式用於從標準輸入(鍵盤)讀取資料。在輸入字串時,會使用%s這個格式指定子。

程式碼範例:

#include <stdio.h>

int main() {
    char str[50];  // 準備一個最多可存放50字元的緩衝區
    printf("請輸入字串: ");
    scanf("%s", str);  // 從標準輸入讀取字串
    printf("輸入的字串: %s\n", str);
    return 0;
}

這個程式會接收使用者輸入的字串,並將其輸出到螢幕上。

scanf函式的注意事項

  1. 無法處理空白字元:
    scanf會將空格、Tab、換行等視為分隔符號,因此輸入含有空白的字串時會被截斷。

範例:
輸入:

Hello World

輸出:

Hello
  1. 緩衝區溢位風險:
    如果輸入字串超過緩衝區大小,可能會破壞記憶體區域,導致程式當掉或產生安全漏洞。

對策:
建議使用更安全的函式(如後面介紹的fgets)。

3-2. fgets函式

fgets函式的基本用法

fgets函式可以安全地輸入指定大小的字串,並包含換行字元,因此不會發生緩衝區溢位問題。

程式碼範例:

#include <stdio.h>

int main() {
    char str[50];  // 準備一個最多可存放50字元的緩衝區
    printf("請輸入字串: ");
    fgets(str, sizeof(str), stdin);  // 安全輸入字串
    printf("輸入的字串: %s", str);
    return 0;
}

這個程式會安全地接收最多50字元的輸入並輸出。

fgets函式的優點

  1. 避免緩衝區溢位: 可透過指定大小避免溢位。
  2. 可處理含空白的字串: 輸入的字串中可包含空格與Tab。

fgets函式的注意事項

  1. 換行字元處理: 輸入的最後會包含換行字元,可能導致輸出時多出一行。

刪除換行字元範例:

str[strcspn(str, "\n")] = '\0';
  1. 標準輸入緩衝殘留: 使用fgets後再進行其他輸入時,緩衝中可能殘留多餘資料。可透過fflush(stdin)getchar()清除。

3-3. 該使用哪一個?

函式名稱用途注意事項
scanf簡單字串輸入(不含空白的短字串)需注意空白處理與緩衝區溢位。
fgets安全,適合含空白的字串輸入需處理換行字元與緩衝清理。

在初學者或實用程式中,建議優先使用fgets以確保安全性。

4. 為了安全字串輸入的實用技巧

4-1. 緩衝區溢位對策

什麼是緩衝區溢位?

緩衝區溢位是指輸入的資料超過預先分配的記憶體空間(緩衝區),導致資料寫入其他記憶體區域的問題。這可能造成程式當機,甚至產生安全漏洞。

範例: 以下的程式碼是不安全的使用方式。

char str[10];
scanf("%s", str);  // 沒有限制輸入大小

若輸入超過10個字元,就會發生緩衝區溢位。

對策1: 使用fgets

fgets函式可以限制輸入大小,因此更安全。

安全範例:

#include <stdio.h>
#include <string.h>

int main() {
    char str[10];
    printf("請輸入字串: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';  // 移除換行符號
    printf("輸入的字串: %s\n", str);
    return 0;
}

這段程式會限制輸入不超過10個字元,並適當處理換行字元。

對策2: 驗證輸入長度

當使用者輸入過長時,可以顯示警告並中止處理。

範例:

#include <stdio.h>
#include <string.h>

int main() {
    char str[10];
    printf("請輸入字串 (最多9個字元): ");
    fgets(str, sizeof(str), stdin);
    if (strlen(str) >= sizeof(str) - 1 && str[strlen(str) - 1] != '\n') {
        printf("輸入過長。\n");
        return 1;  // 錯誤結束
    }
    str[strcspn(str, "\n")] = '\0';  // 移除換行符號
    printf("輸入的字串: %s\n", str);
    return 0;
}

這個範例會檢查輸入長度,若過長就會提示錯誤並結束程式。

4-2. 錯誤處理的實作

錯誤處理的重要性

錯誤處理對於提升程式的穩定性至關重要。特別是在處理使用者輸入時,必須能應對無效或意料之外的輸入。

對策1: 重試輸入

當使用者輸入無效時,要求重新輸入。

範例:

#include <stdio.h>
#include <string.h>

int main() {
    char str[10];
    int valid = 0;

    while (!valid) {
        printf("請輸入字串 (最多9個字元): ");
        fgets(str, sizeof(str), stdin);

        // 驗證輸入長度
        if (strlen(str) >= sizeof(str) - 1 && str[strlen(str) - 1] != '\n') {
            printf("輸入過長,請再試一次。\n");
            while (getchar() != '\n');  // 清除多餘輸入
        } else {
            str[strcspn(str, "\n")] = '\0';  // 移除換行符號
            valid = 1;  // 有效輸入
        }
    }

    printf("輸入的字串: %s\n", str);
    return 0;
}

這個程式會在輸入錯誤時提示使用者重新輸入。

對策2: 輸入過濾

可以設定條件(如僅允許英數字),過濾不合法輸入。

範例:

#include <stdio.h>
#include <ctype.h>
#include <string.h>

int isValidInput(const char *str) {
    for (int i = 0; str[i] != '\0'; i++) {
        if (!isalnum(str[i])) {  // 僅允許英數字
            return 0;
        }
    }
    return 1;
}

int main() {
    char str[50];
    printf("請輸入僅包含英數字的字串: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';

    if (isValidInput(str)) {
        printf("輸入的字串: %s\n", str);
    } else {
        printf("輸入無效。\n");
    }

    return 0;
}

這個程式會檢查輸入是否僅包含英數字,若有其他字元則視為無效。

5. 非建議使用的函式與替代方案

5-1. gets函式的危險性

什麼是gets函式?

gets函式用於從使用者輸入讀取字串。基本用法如下:

程式碼範例:

char str[50];
gets(str);  // 從標準輸入讀取字串

這段程式看似簡單,但其實存在致命問題。

gets函式的問題點

  1. 緩衝區溢位風險:
    gets沒有提供限制輸入長度的機制,如果輸入超過陣列大小,就會破壞記憶體,導致程式錯誤甚至安全漏洞。

範例:

char str[10];
gets(str);  // 沒有輸入大小限制

如果輸入20個字元,程式可能當掉或行為異常。

  1. 安全風險:
    攻擊者可能利用緩衝區溢位進行惡意攻擊(如Buffer Overflow Attack),造成系統被入侵。
  2. 標準規格廢止:
    在C99標準中已經標註為「不建議使用」,C11更是完全刪除了。現代編譯器通常會對gets顯示警告或錯誤。

5-2. 安全的替代函式

使用fgets函式

取代gets最常見的方法是使用fgets,它能避免緩衝區溢位。

安全程式碼範例:

#include <stdio.h>
#include <string.h>

int main() {
    char str[50];
    printf("請輸入字串: ");
    fgets(str, sizeof(str), stdin);  // 安全輸入
    str[strcspn(str, "\n")] = '\0';  // 移除換行符號
    printf("輸入的字串: %s\n", str);
    return 0;
}

與scanf函式比較

scanf也能用來輸入字串,但如前所述,它無法處理空白字元,且有緩衝區溢位風險。因此,如果需要處理複雜字串或更高安全性,建議使用fgets

使用getline函式

在支援POSIX標準的系統中,也可以使用getline。它會動態配置記憶體,因此不必擔心緩衝區大小不足的問題。

範例:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *line = NULL;
    size_t len = 0;
    ssize_t read;

    printf("請輸入字串: ");
    read = getline(&line, &len, stdin);  // 動態分配記憶體接收輸入

    if (read != -1) {
        printf("輸入的字串: %s", line);
    }

    free(line);  // 釋放記憶體
    return 0;
}

避免使用不安全函式的重要性

在C語言中,部分舊有函式因安全性問題已經被棄用。特別是涉及外部輸入時,應盡量避免使用像gets這類危險函式,並改用安全的替代方案。

不建議使用的函式替代函式用途與優點
getsfgets安全、支援固定大小的字串輸入。
getsgetline支援動態記憶體,適合長字串輸入。
scanf("%s")fgets可安全處理含空白的字串。

6. 實踐範例與應用篇|多行字串輸入與處理

6-1. 接收多行字串輸入

多行輸入的概要

在程式中,有時需要讓使用者輸入多行字串並一次性處理。例如,像記事本這類應用程式,多行輸入是必須的。

範例1: 接收3行輸入的程式

#include <stdio.h>
#include <string.h>

#define MAX_LINES 3     // 輸入行數
#define MAX_LENGTH 100  // 每行的最大字元數

int main() {
    char lines[MAX_LINES][MAX_LENGTH];  // 用於儲存字串的二維陣列

    printf("請輸入 %d 行字串:\n", MAX_LINES);

    for (int i = 0; i < MAX_LINES; i++) {
        printf("第 %d 行: ", i + 1);
        fgets(lines[i], sizeof(lines[i]), stdin);
        lines[i][strcspn(lines[i], "\n")] = '\0';  // 移除換行字元
    }

    printf("\n您輸入的字串:\n");
    for (int i = 0; i < MAX_LINES; i++) {
        printf("第 %d 行: %s\n", i + 1, lines[i]);
    }

    return 0;
}

重點解析:

  1. 使用二維陣列: 每行輸入分別儲存在獨立的陣列中。
  2. 利用fgets安全輸入: 限制最大字元數並刪除換行符號。
  3. 透過for迴圈處理: 使用迴圈來簡化多行輸入的處理。

執行範例:

請輸入 3 行字串:
第 1 行: Hello
第 2 行: World
第 3 行: C Language

您輸入的字串:
第 1 行: Hello
第 2 行: World
第 3 行: C Language

6-2. 處理包含空白或特殊字元的字串

包含空白的輸入範例

當需要安全地處理包含空白的字串時,應使用fgets

範例2: 處理含有空格的輸入

#include <stdio.h>
#include <string.h>

int main() {
    char str[100];  // 準備一個最多可存放100字元的緩衝區

    printf("請輸入一段文字: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';  // 移除換行字元

    printf("您輸入的文字: %s\n", str);
    return 0;
}

重點:

  • 能安全接收包含空格或Tab的輸入。
  • 刪除換行符號可避免多餘的換行輸出。

執行範例:

請輸入一段文字: Hello World with spaces
您輸入的文字: Hello World with spaces

6-3. 特殊字元與跳脫字元處理

特殊字元輸入範例

在C語言中,也需要處理包含跳脫字元或符號的輸入。

範例3: 偵測特殊字元的程式

#include <stdio.h>
#include <string.h>
#include <ctype.h>

int main() {
    char str[100];
    int specialCharCount = 0;

    printf("請輸入字串: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';  // 移除換行符號

    for (int i = 0; str[i] != '\0'; i++) {
        if (!isalnum(str[i]) && !isspace(str[i])) {  // 非英數字且非空白
            specialCharCount++;
        }
    }

    printf("特殊字元數量: %d\n", specialCharCount);
    return 0;
}

重點:

  • 使用isalnum判斷是否為英數字。
  • 使用isspace判斷是否為空白。
  • 其餘字元皆視為「特殊字元」。

執行範例:

請輸入字串: Hello, World! 123
特殊字元數量: 2

6-4. 應用範例:簡易記事本程式

最後,我們來看一個結合多行輸入與檔案輸出的簡易記事本程式。

範例4: 記事本程式

#include <stdio.h>
#include <string.h>

#define MAX_LINES 5
#define MAX_LENGTH 100

int main() {
    char lines[MAX_LINES][MAX_LENGTH];

    printf("您最多可以輸入 %d 行備忘錄。\n", MAX_LINES);
    for (int i = 0; i < MAX_LINES; i++) {
        printf("第 %d 行: ", i + 1);
        fgets(lines[i], sizeof(lines[i]), stdin);
        lines[i][strcspn(lines[i], "\n")] = '\0';
    }

    FILE *file = fopen("memo.txt", "w");
    if (file == NULL) {
        printf("無法開啟檔案。\n");
        return 1;
    }

    for (int i = 0; i < MAX_LINES; i++) {
        fprintf(file, "%s\n", lines[i]);
    }

    fclose(file);
    printf("備忘錄已儲存。\n");
    return 0;
}

重點:

  • 將使用者輸入儲存到檔案中。
  • 能學習檔案操作的基礎應用。

7. 常見問題(Q&A形式)

Q1: 為什麼不建議使用gets函式?

A:
gets函式讀取輸入時不會限制字串長度,因此存在緩衝區溢位的風險。這種漏洞會造成嚴重的安全問題,因此在C11之後已被標準移除。建議改用更安全的fgets

範例(安全寫法):

char str[50];
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';  // 移除換行符號

Q2: 為什麼scanf無法處理包含空白的輸入?

A:
因為scanf預設會將空白字元(空格、Tab、換行)視為分隔符號,所以輸入含有空格的字串會被截斷。

範例:
輸入:

Hello World

輸出:

Hello

解決方法:
若要處理含空格的字串,請使用fgets

char str[100];
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';
printf("輸入的字串: %s\n", str);

Q3: 如果輸入超過fgets指定大小會怎麼辦?

A:
fgets只會讀取到指定大小,過長的輸入會被截斷。

解決方法:

  1. 當輸入過長時,顯示錯誤訊息或要求重新輸入。
  2. 在POSIX環境中可以使用getline動態分配記憶體,處理更長的字串。

範例(使用getline):

#include <stdio.h>
#include <stdlib.h>

int main() {
    char *line = NULL;
    size_t len = 0;
    printf("請輸入字串: ");
    getline(&line, &len, stdin);  // 動態配置記憶體
    printf("輸入的字串: %s", line);
    free(line);  // 釋放記憶體
    return 0;
}

Q4: fgets會保留換行字元,該如何處理?

A:
因為fgets會將換行字元一併讀入,輸出時可能會多出一行。

解決方法:
手動刪除換行字元。

char str[100];
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';  // 移除換行符號
printf("輸入的字串: %s\n", str);

Q5: 如果fgets後輸入緩衝區還有殘留資料怎麼辦?

A:
當輸入過長時,未讀取的資料會殘留在緩衝區。這會影響下一次輸入。

解決方法:
使用getchar()清除緩衝區。

char str[10];
fgets(str, sizeof(str), stdin);

// 如果沒有讀到換行字元,清空緩衝區
if (strchr(str, '\n') == NULL) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

printf("輸入的字串: %s\n", str);

Q6: 想只允許英數字輸入,該怎麼做?

A:
可以在程式中加入檢查,過濾掉非英數字的輸入。

範例:

#include <stdio.h>
#include <ctype.h>
#include <string.h>

int isValidInput(const char *str) {
    for (int i = 0; str[i] != '\0'; i++) {
        if (!isalnum(str[i])) {  // 若不是英數字則回傳0
            return 0;
        }
    }
    return 1;
}

int main() {
    char str[50];
    printf("請輸入僅包含英數字的字串: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';

    if (isValidInput(str)) {
        printf("有效輸入: %s\n", str);
    } else {
        printf("輸入無效。\n");
    }
    return 0;
}

Q7: 如果要處理超過緩衝區大小的長字串怎麼辦?

A:
fgets可以分段讀取,或者在POSIX環境中使用getline,動態分配記憶體以處理長字串。

範例:
使用getline能應對大量輸入,前面已有示範。

8. 總結

本文從C語言中「字串輸入」的基礎到進階應用進行了系統性的解說。讀者應該已經學會如何透過適當的函式與技巧,來安全且有效率地處理字串輸入。以下整理重點:

1. 理解字串輸入的基礎

在C語言中,字串是以字元陣列表示,並由終止符號\0結束。理解這個基本概念能幫助正確進行字串操作。

2. 輸入函式的特性與選擇

  • scanf函式:
    無法處理空白,且存在緩衝區溢位風險,需小心使用。
  • fgets函式:
    能指定輸入大小,安全性高,但需注意換行字元處理。
  • getline函式:
    可動態配置記憶體,適合大量字串輸入(POSIX環境限定)。

3. 實踐安全的字串輸入

為了提升安全性,本文介紹了以下要點:

  1. 緩衝區大小管理: 避免過長輸入破壞記憶體。
  2. 換行符號處理: 刪除不必要的換行符,保持輸入乾淨。
  3. 輸入驗證: 過濾不合法字元,並實作錯誤處理或重試機制。

4. 透過應用範例強化實力

透過多行輸入與檔案輸出的應用範例,學會如何將基礎技巧延伸至更實用的程式設計。例如,使用二維陣列與動態記憶體管理,可應用於更進階的專案。

5. 學會解決常見疑問

Q&A部分針對初學者常遇到的困難提供具體解答,例如:

  • gets函式的危險性,以及fgets/getline的替代方案。
  • 處理空白與特殊字元的方法。
  • 面對過長輸入或換行符號問題的對策。

6. 下一步的學習方向

掌握字串輸入的基礎後,可以進一步挑戰以下主題:

  1. 字串處理函式庫的活用:
    strlen, strcmp, strcpy等標準函式。
  2. 動態記憶體管理:
    利用mallocrealloc彈性處理字串。
  3. 檔案輸入輸出:
    設計能處理大量資料的程式。
  4. 資料結構與演算法:
    例如字串搜尋、排序演算法等。

7. 實踐練習題

為了鞏固學習效果,可以挑戰以下練習:

  1. 練習1: 名單管理系統
    利用多行輸入建立姓名、年齡等資料的管理程式。
  2. 練習2: 關鍵字搜尋程式
    建立搜尋字串的程式,判斷是否包含特定關鍵字。
  3. 練習3: 檔案紀錄系統
    將輸入資料儲存到檔案中,並可事後讀取。

總結中的總結

本文全面涵蓋了C語言字串輸入的基礎與應用。
最重要的重點:

  • 安全性: 使用fgetsgetline等安全函式。
  • 緩衝區與錯誤處理: 防止溢位並處理異常狀況。
  • 實務應用: 結合多行輸入與檔案操作,開發更進階的程式。

只要理解並實踐這些重點,就能大幅提升C語言字串處理的能力,為後續更高階的程式設計奠定基礎。