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