如何在 C 語言中安全地讀取用戶輸入
在 C 語言中讀取字符串是一件非常危險的事情。當讀取用戶輸入時,程序員可能會嘗試使用 C 標準庫中的 gets
函數。它的用法非常簡單:
char *gets(char *string);
gets()
從標準輸入讀取數據,然后將結果存儲在一個字符串變量中。它會返回一個指向字符串的指針,如果沒有讀取到內容,返回 NULL
值。
舉一個簡單的例子,我們可能會問用戶一個問題,然后將結果讀入字符串中:
#include <stdio.h>
#include <string.h>
int main()
{
char city[10]; // 例如 "Chicago"
// 這種方法很糟糕 .. 不要使用 gets
puts("Where do you live?");
gets(city);
printf("<%s> is length %ld\n", city, strlen(city));
return 0;
}
輸入一個相對較短的值就可以:
Where do you live?
Chicago
<Chicago> is length 7
然而,gets()
函數非常簡單,它會天真地讀取數據,直到它認為用戶完成為止。但是它不會檢查字符串是否足夠容納用戶的輸入。輸入一個非常長的值會導致 gets()
存儲的數據超出字符串變量長度,從而導致覆蓋其他部分內存。
Where do you live?
Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch
<Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch> is length 58
Segmentation fault (core dumped)
最好的情況是,覆蓋部分只會破壞程序。最壞的情況是,這會引入一個嚴重的安全漏洞,惡意用戶可以通過你的程序將任意數據插入計算機的內存中。
這就是為什么在程序中使用 gets()
函數是危險的。使用 gets()
,你無法控制程序嘗試從用戶讀取多少數據,這通常會導致緩沖區溢出。
安全的方法
fgets()
函數歷來是安全讀取字符串的推薦方法。此版本的 gets()
提供了一個安全檢查,通過僅讀取作為函數參數傳遞的特定數量的字符:
char *fgets(char *string, int size, FILE *stream);
fgets()
函數會從文件指針讀取數據,然后將數據存儲到字符串變量中,但最多只能達到 size
指定的長度。我們可以更新示例程序來測試這一點,使用 fgets()
而不是 gets()
:
#include <stdio.h>
#include <string.h>
int main()
{
char city[10]; // 例如 "Chicago"
puts("Where do you live?");
// fgets 雖好但是并不完美
fgets(city, 10, stdin);
printf("<%s> is length %ld\n", city, strlen(city));
return 0;
}
如果編譯運行,你可以在提示符后輸入任意長的城市名稱。但是,程序只會讀取 size
= 10 數據存儲到字符串變量中。因為 C 語言在字符串末尾會添加一個空(\0
)字符,這意味著 fgets()
只會讀取 9 個字符到字符串中。
Where do you live?
Minneapolis
<Minneapol> is length 9
雖然這肯定比 fgets()
讀取用戶輸入更安全,但代價是如果用戶輸入過長,它會“切斷”用戶輸入。
新的安全方法
更靈活的解決方案是,如果用戶輸入的數據比變量可能容納的數據多,則允許字符串讀取函數為字符串分配更多內存。根據需要調整字符串變量大小,確保程序始終有足夠的空間來存儲用戶輸入。
getline()
函數正是這樣。它從輸入流讀取輸入,例如鍵盤或文件,然后將數據存儲在字符串變量中。但與 fgets()
和 gets()
不同,getline()
使用 realloc()
調整字符串大小,確保有足夠的內存來存儲完整輸入。
ssize_t getline(char **pstring, size_t *size, FILE *stream);
getline()
實際上是一個名為 getdelim()
的類似函數的裝飾器,它會讀取數據一直到特殊分隔符停止。本例中,getline()
使用換行符(\n
)作為分隔符,因為當從鍵盤或文件讀取用戶輸入時,數據行由換行符分隔。
結果證明這是一種更安全的方法讀取任意數據,一次一行。要使用 getline()
,首先定義一個字符串指針并將其設置為 NULL
,表示還沒有預留內存,再定義一個 size_t
類型的“字符串大小” 的變量,并給它一個零值。當你調用 getline()
時,你需要傳入字符串和字符串大小變量的指針,以及從何處讀取數據。對于示例程序,我們可以從標準輸入中讀取:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *string = NULL;
size_t size = 0;
ssize_t chars_read;
// 使用 getline 讀取長字符串
puts("Enter a really long string:");
chars_read = getline(&string, &size, stdin);
printf("getline returned %ld\n", chars_read);
// 檢查錯誤
if (chars_read < 0) {
puts("couldn't read the input");
free(string);
return 1;
}
// 打印字符串
printf("<%s> is length %ld\n", string, strlen(string));
// 釋放字符串使用的內存
free(string);
return 0;
}
使用 getline()
讀取數據時,它將根據需要自動為字符串變量重新分配內存。當函數讀取一行的所有數據時,它通過指針更新字符串的大小,并返回讀取的字符數,包括分隔符。
Enter a really long string:
Supercalifragilisticexpialidocious
getline returned 35
<Supercalifragilisticexpialidocious
> is length 35
注意,字符串包含分隔符。對于 getline()
,分隔符是換行符,這就是為什么輸出中有換行符的原因。 如果你不想在字符串值中使用分隔符,可以使用另一個函數將字符串中的分隔符更改為空字符。
通過 getline()
,程序員可以安全地避免 C 編程的一個常見陷阱:你永遠無法知道用戶可能會輸入哪些數據。這就是為什么使用 gets()
不安全,而 fgets()
又太笨拙的原因。相反,getline()
提供了一種更靈活的方法,可以在不破壞系統的情況下將用戶數據讀入程序。