字符串輸入前的注意事項
如果想把一個字符串讀入程序,首先必須預留該字符串的空間,然后用輸入函數獲取該字符串
?這意味著必須要為字符串分配足夠的空間。
不要指望計算機在讀取字符串時順便計算它的長度,然后再分配空間(計算機不會這樣做,除非你編寫一個處理這些任務的函數)。
假設編寫了如下代碼:
char *name;
scanf("%s",name);
雖然可能會通過編譯(編譯器很可能給出警告),但是在讀入name時,name可能會擦寫掉程序中的數據或代碼,從而導致程序異常中止。
因為scanf()要把信息拷貝至參數指定的地址上,而此時該參數是個未初始化的指針,name可能會指向任何地方。大多數程序員都認為出現這種情況很搞笑,但僅限于評價別人的程序時。
?
最簡單的方法是,在聲明時顯式指明數組的大小:
?
char name[81];
scanf("%s",name);
現在name是一個已分配塊(81字節)的地址。還有一種方法是使用C庫函數來分配內存
為字符串分配內存后,便可讀入字符串。C語言提供了許多讀取字符串的函數:gets(),fgets(),gets_s()函數
gets()
C語言中的gets()
函數是一個標準庫函數,用于從標準輸入(鍵盤)讀取一行字符并存儲到指定的字符數組中。它的函數原型如下:
char *gets(char *str);
gets()
函數的參數是一個字符數組,用于存儲讀取到的字符,返回值是讀取到的字符數組的首地址。
注意:gets()
函數存在一些安全性問題,因為它無法保證讀取的字符數不超過指定的字符數組大小,可能導致緩沖區溢出。因此,在實際的程序開發中,最好使用更安全的替代函數fgets()
來代替gets()
函數。
特點
在讀取字符串時,scanf()和轉換說明%s只能讀取一個單詞。可是在程序中經常要讀取一整行輸入,而不僅僅是一個單詞。許多年前,gets()函數就用于處理這種情況。
gets()函數簡單易用,它讀取整行的輸入,直至遇到換行符,然后丟棄換行符,儲存其余字符,并在這些字符的末尾添加一個空字符使其成為一個C字符串。
使用示例:
#include <stdio.h>
int main()
{
char words[81];gets(words) ; // 典型用法printf("s\n", words);
}
結果
//輸入abcd
abcd
但是我們拿著上面這段代碼去運行的時候,就會發現編譯器報錯或者發出警告,這是為什么呢?
缺點
問題就出現在gets唯一的參數是words,它無法檢查數組是否裝得下輸入行。
因此gets()函數只知道數組的開始處(通過傳入的數組名),但是并不知道數組中有多少個元素。
如果輸入的字符串過長,會導致緩沖區溢出(buffer overflow),即多余的字符超出了指定的目標空間。
如果這些多余的字符只是占用了尚未使用的內存,就不會立即出現問題;
如果它們擦寫掉程序中的其他數據,會導致程序異常中止:或者還有其他情況。
為了讓輸入的字符串容易溢出,把程序中的STLEN設置為5,程序的輸出如下:
//輸入abcd
abcd
Segmentation fault:11
“Segmentation fault”(分段錯誤)似乎不是個好提示,的確如此。在UNIX系統中,這條消息說明該程序試圖訪問未分配的內存。
C 提供解決某些編程問題的方法可能會導致陷入另一個尷尬棘手的困境。
但是,為什么要特別提到gets()函數?
因為該函數的不安全行為造成了安全隱患。
過去,有些人通過系統編程,利用gets()插入和運行一些破壞系統安全的代碼。
不久,C編程社區的許多人都建議在編程時摒棄gets()。制定C99標準的委員會把這些建議放入了標準,承認了gets()的問題并建議不要再使用它。盡管如此,在標準中保留gets()也合情合理,因為現有程序中含有大量使用該函數的代碼。而且,只要使用得當,它的確是一個很方便的函數。
好景不長,C11標準委員會采取了更強硬的態度,直接從標準中廢除了gets()函數。然而在實際應用中,編譯器為了能兼容以前的代碼,大部分都繼續支持gets()函數。不過,VS2022就不支持了
fgets()
過去通常用fgets()來代替gets(),fgets()函數稍微復雜些,在處理輸入方面與gets()略有不同。
原型
在C語言中,fgets()
函數用于從指定的輸入流中讀取一行字符串。它接受三個參數:輸入緩沖區指針,緩沖區大小和要讀取的輸入流。
使用fgets()
函數的語法如下:
char *fgets(char *str, int size, FILE *stream);
fgets()函數的第2個參數指明了讀入字符的最大數量。
如果該參數的值是n,那么fgets()
函數從輸入流中讀取至多n?- 1個字符(因為會自動加\0),或者遇到換行符('\n')為止。
fgets()函數的第3個參數指明要讀入的文件。如果讀入從鍵盤輸入的數據,則以stdin(標準輸入)作為參數,該標識符定義在stdio.h中。
fgets()函數返回指向char的指針。如果一切進行順利,該函數返回的地址與傳入的第1個參數相同,但是,如果函數讀到文件結尾,它將返回一個特殊的指針:空指針(null pointer)。該指針保證不會指向有效的數據,所以可用于標識這種特殊情況。在代碼中,可以用數字0來代替,不過在C語言中用宏NULL來代替更常見(如果在讀入數據時出現某些錯誤,該函數也返回NULL)。
讀取規則
它將讀取到的字符逐個存儲在字符數組中,直到達到指定的大小或者遇到換行符為止。
如果沒有遇到換行符,或者輸入流中沒有更多字符可讀,fgets()
函數會在最后一個字符后面添加一個空字符('\0')(如果數組沒存滿,系統會自動添加\0直到裝滿),表示字符串的結束。
看個例子
#include<stdio.h>
int main()
{char a[10];fgets(a, 10, stdin);printf("%s", a);}
輸入1234567890(超出指定大小),結果是
123456789
?輸入1234,按enter(遇到換行符),結果是
1234
我們可以再看個例子啊
#include <stdio.h>
int main(void)
{char words[10];puts("Enter strings (empty line to quit):");while (fgets(words, 14, stdin) != NULL && (words[0] != '\n'))fputs(words, stdout);puts("Done.");
}
輸入By the way,the gets() function,?結果是
有人就會有疑問了啊,這輸入的東西不是超除了words的大小嗎?那為什么還能正常打印?
實際上它確實超過了,但是這是循環!
程序中的fgets()一次讀入 10?-1個字符(該例中為9個字符)。所以,一開始它只讀入了“By the wa”,并儲存為By the wa\0:接著fputs()打印該字符串,而且并未換行。然后while循環進入下一輪迭代,fgets()繼續從剩余的輸入中讀入數據,即讀入“y,the ge”并儲存為y,the ge\0;接著fputs()在剛才打印字符串的這一行接著打印第2次讀入的字符串。然后while 進入下一輪迭代,fgets()繼續讀取輸入、fputs()打印字符串,這一過程循環進行,直到讀入最后的“tion\n”。fgets()將其儲存為tion\n\0,fputs()打印該字符串,由于字符串中的\n,光標被移至下一行開始處。
保留換行符
需要注意的是,fgets()
函數會保留輸入流中的換行符,所以讀取到的字符串可能包含換行符。如果你希望去除換行符,可以使用strtok()
或者手動處理字符串。
系統采用緩沖的IO,這意味著用戶在按下enter鍵之前,輸入都會被存在臨時存儲區(緩沖區)。
這點與gets()不同,gets()會丟棄換行符。
我們可以先借用puts()函數的特性:自動在字符串末尾加換行符
我們可以驗證一下
#include <stdio.h>
int main(void)
{char words[14];puts("請輸入:");fgets(words, 14, stdin);printf("見證奇跡的時刻:\n");puts(words);printf("sjajj");}
結果是:?
apple pie,比fgets()讀入的整行輸入短,因此,apple pie\n\0被儲存在數組中(因為fgets()會自動存儲換行符)。當puts()顯示該字符串時又在末尾添加了換行符,調用puts()時apple pie\n\0里的換行符起作用將光標移動到下一行,但是puts自動在字符串末尾添加換行符,所以光標再次移動到下一行。因此apple pie下面有一行空行。
系統使用緩沖的I/O。這意味著用戶在按下Return鍵之前,輸入都被儲存在臨時存儲區(即,緩沖區)中。按下Enter鍵就在輸入中增加了一個換行符,并把整行輸入發送給fgets()。對于輸出,fputs()把字符發送給另一個緩沖區,當發送換行符時,緩沖區中的內容被發送至屏幕上。
fgets()儲存換行符有好處也有壞處。
壞處是你可能并不想把換行符儲存在字符串中,這樣的換行符會帶來一些麻煩。
好處是對于儲存的字符串而言,檢查末尾是否有換行符可以判斷是否讀取了一整行。如果不是一整行,要妥善處理一行中剩下的字符。
處理掉換行符
首先,如何處理掉換行符?
一個方法是在已儲存的字符串中查找換行符,并將其替換成空字符:
while (words[i] !='\n')// 假設\n在words中
i++;
words[i]='\0';
其次,如果仍有字符串留在輸入行怎么辦?
一個可行的辦法是,如果目標數組裝不下一整行輸入,就丟棄那些多出的字符
while(getchar()!='\n')
contine;
gets_s()函數
C11標準新增的gets_s()函數也可代替gets()。該函數與gets()函數更接近,而且可以替換現有代碼中的 gets()。但是,它是stdio.h.輸入/輸出函數系列中的可選擴展,所以支持C11的編譯器也不一定支持它。
在C語言中,gets_s()
函數用于讀取用戶輸入的字符串。gets_s()
函數的聲明如下:
char *gets_s(char *str, rsize_t n);
其中,str
是指向字符數組的指針,用于存儲讀取到的字符串;n
表示字符數組的大小。
gets_s()
函數會讀取用戶輸入的字符串,并將其存儲到指定的字符數組中,直到讀取到換行符或數組大小的限制。讀取到的字符串將包含換行符,且以\0字符結尾。
需要注意的是,gets_s()
函數是C11中引入的安全版本的函數,主要解決了gets()
函數的緩沖區溢出問題。
C11新增的gets_s()函數(可選)和fgets()類似,用一個參數限制讀入的字符數。
特性
- gets_s()只從標準輸入中讀取數據,所以不需要第3個參數。
- 如果gets_s()讀到換行符,會丟棄它而不是儲存它。
- 如果gets_s()讀到最大字符數都沒有讀到換行符,會執行以下幾步。首先把目標數組中的首字符設置為空字符,讀取并丟棄隨后的輸入直至讀到換行符或文件結尾,然后返回空指針。接著,調用依賴實現的“處理函數”(或你選擇的其他函數),可能會中止或退出程序。
第2個特性說明,只要輸入行未超過最大字符數,gets_s()和gets()幾乎一樣,完全可以用gets_s()換gets()。第3個特性說明,要使用這個函數還需要進一步學習。
三種輸入方式的選擇
我們來比較一下gets()、fgets()和gets_s()的適用性。
如果目標存儲區裝得下輸入行,3個函數都沒問題。但是fgets()會保留輸入末尾的換行符作為字符串的一部分,要編寫額外的代碼將其替換成字符。
如果輸入行太長會怎樣?
使用gets()不安全,它會擦寫現有數據,存在安全隱患。gets_s()函數很全,但是,如果并不希望程序中止或退出,就要知道如何編寫特殊的“處理函數”。另外,如果打算讓程繼續運行,gets_s(會丟棄該輸入行的其余字符,無論你是否需要。由此可見,當輸入太長,超過數組容納的字符數時,fgets()函數最容易使用,而且可以選擇不同的處理方式。如果要讓程序繼續使用輸中超出的字符,可以參考程序清單11.8中的處理方法。
所以,當輸入與預期不符時,gets_s()完全沒有fgets()函數方便、靈活。也許這也是gets s二民的因之一。鑒于此,fgets()通常是處理類似情況的最佳選擇。
?