gets和fgets函數及其區別,C語言gets和fgets函數詳解

2020-07-16 10:04:23
每當討論 gets 函數時,大家不由自主地就會想起 1988 年的“網際網路蠕蟲”,它在 UNIX 作業系統的 finger 後臺程式中使用一個 gets 呼叫作為它的攻擊方式之一。很顯然,對蠕蟲病毒的實現來說, gets 函數的功勞不可小視。不僅如此,GCC 也不推薦使用gets和puts函數。

那麼,究竟是什麼原因導致 gets 函數這麼不招人待見呢?

我們知道,對於 gets 函數,它的任務是從 stdin 流中讀取字串,直至接收到換行符或 EOF 時停止,並將讀取的結果存放在 buffer 指標所指向的字元陣列中。這裡需要注意的是,換行符不作為讀取串的內容,讀取的換行符被轉換為 null('') 值,並由此來結束字串。即換行符會被丟棄,然後在末尾新增 null('') 字元。其函數的原型如下:
char* gets(char* buffer);
如果讀入成功,則返回與引數 buffer 相同的指標;如果讀入過程中遇到 EOF 或發生錯誤,返回 NULL 指標。因此,在遇到返回值為 NULL 的情況,要用 ferror 或 feof 函數檢查是發生錯誤還是遇到 EOF。

函數 gets 可以無限讀取,不會判斷上限,所以程式設計師應該確保 buffer 的空間足夠大,以便在執行讀操作時不發生溢位。也就是說,gets 函數並不檢查緩衝區 buffer 的空間大小,事實上它也無法檢查緩衝區的空間。

如果函數的呼叫者提供了一個指向堆疊的指標,並且 gets 函數讀入的字元數量超過了緩衝區的空間(即發生溢位),gets 函數會將多出來的字元繼續寫入堆疊中,這樣就覆蓋了堆疊中原來的內容,破壞一個或多個不相關變數的值。如下面的範例程式碼所示:
int main(void)
{
    char buffer[11];
    gets(buffer);
    printf("輸出: %sn",buffer);
    return 0;
}
範例程式碼的執行結果為:
aaa
輸出: aaa

根據執行結果,當使用者在鍵盤上輸入的字元個數大於緩衝區 buffer 的最大界限時,gets 函數也不會對其進行任何檢查,因此我們可以將惡意程式碼多出來的資料寫入堆疊。由此可見,gets 函數是極其不安全的,可能成為病毒的入口,因為 gets 函數沒有限制輸入的字串長度。所以我們應該使用 fgets 函數來替換 gets 函數,實際上這也是大多程式設計師所推薦的做法。

相對於 gets 函數,fgets 函數最大的改進就是能夠讀取指定大小的資料,從而避免 gets 函數從 stdin 接收字串而不檢查它所複製的緩衝區空間大小導致的快取溢位問題。當然,fgets 函數主要是為檔案 I/O 而設計的(注意,不能用 fgets 函數讀取二進位制檔案,因為 fgets 函數會把二進位制檔案當成文字檔案來處理,這勢必會產生亂碼等不必要的麻煩)。其中,fgets 函數的原型如下:
char *fgets(char *buf, int bufsize, FILE *stream);
該函數的第二個引數 bufsize 用來指示最大讀入字元數。如果這個引數值為 n,那麼 fgets 函數就會讀取最多 n-1 個字元或者讀完一個換行符為止,在這兩者之中,最先滿足的那個條件用於結束輸入。

與 gets 函數不同的是,如果 fgets 函數讀到換行符,就會把它儲存到字串中,而不是像 gets 函數那樣丟棄它。即給定引數 n,fgets 函數只能讀取 n-1 個字元(包括換行符)。如果有一行超過 n-1 個字元,那麼 fgets 函數將返回一個不完整的行(唯讀取該行的前 n-1 個字元)。但是,緩衝區總是以 null('') 字元結尾,對 fgets 函數的下一次呼叫會繼續讀取該行。

也就是說,每次呼叫時,fgets 函數都會把緩衝區的最後一個字元設為 null(''),這意味著最後一個字元不能用來存放需要的資料。所以如果某一行含有 size 個字元(包括換行符),要想把這行讀入緩衝區,要把引數 n 設為 size+1,即多留一個位置儲存 null('')。

最後,它還需要第 3 個引數來說明讀取哪個檔案。如果是從鍵盤上讀入資料,可以使用 stdin 作為該引數,如下面的程式碼所示:
int main(void)
{
    char buffer[11];
    fgets(buffer,11,stdin);
    printf("輸出: %sn",buffer);
    return 0;
}
對於上面的範例程式碼,如果輸入的字串小於或等於 10 個字元,那麼程式將完整地輸出結果;如果輸入的字串大於 10 個字元,那麼程式將截斷輸入的字串,最後只輸出前 10 個字元。範例程式碼執行結果為:

aaaaaaaaaaaaaaaa
輸出: aaaaaaaaaa

除此之外,C99 還提供了 fgets 函數的寬字元版本 fgetws 函數,其函數的一般原型如下面的程式碼所示:
wchar_t *fgetws(wchar_t * restrict s, int n, FILE * restrict stream);
該函數的功能與 fgets 函數一樣。