在解決一些限定輸入的問題時總是力不從心,直到發現scanf的高級用法,才發現原來可以這么簡單。
文章轉載於:https://blog.csdn.net/C1664510416/article/details/80869470
在前面幾節中,我們演示了如何使用 scanf() 來讀取各種各樣的數據,匯總了 scanf() 可以使用的格式控制符,然后還講解了緩沖區,從根本上消除了 scanf() 的那些奇怪行為,至此,很多初學者就認為自己已經完全掌握了 scanf()。
其實,這只是 scanf() 的基本用法,每個C語言程序員都應該掌握,如果你想讓自己的輸入更加炫酷、更加個性化、更加安全,那么還需要學習 scanf() 的高級用法,這才是大神和菜鳥的分水嶺。
好了,言歸正傳,我們分三個方面講解 scanf() 的高級用法。
1) 指定讀取長度
還記得在 printf() 中可以指定最小輸出寬度嗎?就是在格式控制符的中間加上一個數字,例如,%10d
表示輸出的整數至少占用 10 個字符的位置:
- 如果整數的寬度不足 10,那么在左邊以空格補齊;
- 如果整數的寬度超過了 10,那么以整數本身的寬度來輸出,10 不再起作用。
其實,scanf() 也有類似的用法,也可以在格式控制符的中間加一個數字,用來表示讀取數據的最大長度,例如:%2d
表示最多讀取兩位整數;%10s
表示讀取的字符串的最大長度為 10,或者說,最多讀取 10 個字符。
請看下面的例子:
#include <stdio.h>
int main(){
int n;
float f;
char str[23];
scanf("%2d", &n);
scanf("%*[^\n]"); scanf("%*c"); //清空緩沖區
scanf("%5f", &f);
scanf("%*[^\n]"); scanf("%*c"); //清空緩沖區
scanf("%22s", str);
printf("n=%d, f=%g, str=%s\n", n, f, str);
return 0;
}
輸入示例 ①:
20↙
100.5↙
http://c.biancheng.net↙
n=20, f=100.5, str=http://c.biancheng.net
輸入示例 ②:
8920↙
10.2579↙
http://data.biancheng.net↙
n=89, f=10.25, str=http://data.biancheng.
這段代碼使用了多個 scanf() 函數連續讀取數據,為了避免受到緩沖區中遺留數據的影響,每次讀取結束我們都使用
scanf("%*[^\n]"); scanf("%*c");
來清空緩沖區。
限制讀取數據的長度在實際開發中非常有用,最典型的一個例子就是讀取字符串:我們為字符串分配的內存是有限的,用戶輸入的字符串過長就存放不了了,就會沖刷掉其它的數據,從而導致程序出錯甚至崩潰;如果被黑客發現了這個漏洞,就可以構造棧溢出攻擊,改變程序的執行流程,甚至執行自己的惡意代碼,這對服務器來說簡直是滅頂之災。
在用 gets() 函數讀取字符串的時候,有一些編譯器會提示不安全,建議替換為 gets_s() 函數,就是因為 gets() 不能控制讀取到的字符串的長度,風險極高。
就目前學到的知識而言,雖然 scanf() 可以控制字符串的長度,但是字符串中卻不能包含空白符,這是硬傷,所以 scanf() 暫時還無法替代 gets()。不過大家也不要着急,稍后我還會補充 scanf() 的高級用法,屆時 scanf() 就可以完全替代 gets(),並且比 gets() 更加智能。
2) 匹配特定的字符
%s 控制符會匹配除空白符以外的所有字符,它有兩個缺點:
- %s 不能讀取特定的字符,比如只想讀取小寫字母,或者十進制數字等,%s 就無能為力;
- %s 讀取到的字符串中不能包含空白符,有些情況會比較尷尬,例如,無法將多個單詞存放到一個字符串中,因為單詞之間就是以空格為分隔的,%s 遇到空格就讀取結束了。
要想解決以上問題,可以使用 scanf() 的另外一種字符匹配方式,就是%[xxx]
,[ ]
包圍起來的是需要讀取的字符集合。例如,%[abcd]
表示只讀取字符abcd
,遇到其它的字符就讀取結束;注意,這里並不強調字符的順序,只要字符在 abcd 范圍內都可以匹配成功,所以你可以輸入 abcd、dcba、ccdc、bdcca 等。
請看下面的代碼:
- #include <stdio.h>
- int main(){
- char str[30];
- scanf("%[abcd]", str);
- printf("%s\n", str);
- return 0;
- }
輸入示例 ①:
abcdefgh↙
abcd
輸入示例 ②:
baccbaxyz↙
baccba
使用連接符
為了簡化字符集合的寫法,scanf() 支持使用連字符-
來表示一個范圍內的字符,例如 %[a-z]、%[0-9] 等。
連字符左邊的字符對應一個 ASCII 碼,連字符右邊的字符也對應一個 ASCII 碼,位於這兩個 ASCII 碼范圍以內的字符就是要讀取的字符。注意,連字符左邊的 ASCII 碼要小於右邊的,如果反過來,那么它的行為是未定義的。
常用的連字符舉例:
%[a-z]
表示讀取 abc...xyz 范圍內的字符,也即小寫字母;%[A-Z]
表示讀取 ABC...XYZ 范圍內的字符,也即大寫字母;%[0-9]
表示讀取 012...789 范圍內的字符,也即十進制數字。
你也可以將它們合並起來,例如:
%[a-zA-Z]
表示讀取大寫字母和小寫字母,也即所有英文字母;%[a-z-A-Z0-9]
表示讀取所有的英文字母和十進制數字;%[0-9a-f]
表示讀取十六進制數字。
請看下面的演示:
#include <stdio.h>
int main(){
char str[30];
scanf("%[a-zA-Z]", str); //只讀取字母
printf("%s\n", str);
return 0;
}
輸入示例:
abcXYZ123↙
abcXYZ
不匹配某些字符
假如現在有一種需求,就是讀取換行符以外的所有字符,或者讀取 0~9 以外的所有字符,該怎么實現呢?總不能把剩下的字符都羅列出來吧,一是麻煩,二是不現實。
C語言的開發者們早就考慮到這個問題了,scanf() 允許我們在%[ ]
中直接指定某些不能匹配的字符,具體方法就是在不匹配的字符前面加上^
,例如:
%[^\n]
表示匹配除換行符以外的所有字符,遇到換行符就停止讀取;%[^0-9]
表示匹配除十進制數字以外的所有字符,遇到十進制數字就停止讀取。
請看下面的例子:
- #include <stdio.h>
- int main(){
- char str1[30], str2[30];
- scanf("%[^0-9]", str1);
- scanf("%*[^\n]"); scanf("%*c"); //清空緩沖區
- scanf("%[^\n]", str2);
- printf("str1=%s \nstr2=%s\n", str1, str2);
- return 0;
- }
輸入示例:
abcXYZ@#87edf↙
c c++ java python go javascript↙
str1=abcXYZ@#
str2=c c++ java python go javascript
請注意第 6 行代碼,它的作用是讀取一行字符串,和 gets() 的功能一模一樣。你看,scanf() 也能讀取帶空格的字符串呀,誰說 scanf() 不能完全取代 gets(),這明顯是錯誤的說法。
另外,scanf() 還可以指定字符串的最大長度,指定字符串中不能包含哪些字符,這是 gets() 不具備的功能。
例如,讀取一行不能包含十進制數字的字符串,並且長度不能超過 30:
#include <stdio.h>
int main(){
char str[31];
scanf("%30[^0-9\n]", str);
printf("str=%s\n", str);
return 0;
}
輸入示例 ①:
http://c.biancheng.net http://biancheng.net↙
str=http://c.biancheng.net http://
輸入示例 ②:
I have been programming for 8 years.↙
str=I have been programming for
總之,scanf() 不僅可以完全替代 gets(),並且比 gets() 的功能更加強大。
3) 丟棄讀取到的字符
在前面的代碼中,每個格式控制符都要對應一個變量,把讀取到的數據放入對應的變量中。其實你也可以不這樣做,scanf() 允許把讀取到的數據直接丟棄,不往變量中存放,具體方法就是在 % 后面加一個
*
,例如:
%*d
表示讀取一個整數並丟棄;%*[a-z]
表示讀取小寫字母並丟棄;%*[^\n]
表示將換行符以外的字符全部丟棄。
請看下面的代碼演示:
- #include <stdio.h>
- int main(){
- int n;
- char str[30];
- scanf("%*d %d", &n);
- scanf("%*[a-z]");
- scanf("%[^\n]", str);
- printf("n=%d, str=%s\n", n, str);
- return 0;
- }
輸入示例:
100 999abcxyzABCXYZ↙
n=999, str=ABCXYZ
對結果的分析:整數 100 被第一個 scanf() 中的%*d
讀取后丟棄了,整數 999 被第%d
讀取到,並賦值給 n。此時緩沖區中剩下 abcxyzABCXYZ,第二個 scanf() 將 abcxyz 讀取並丟棄,剩下的 ABCXYZ 被最后一個 scanf() 讀取到並賦值給 str。
大家有沒有意識到,將讀取到的字符直接丟棄,這就是在清空輸入緩沖區呀,雖然有點蹩腳,但是行之有效。在《清空(刷新)緩沖區,從根本上消除那些奇怪的行為》一節中,我們已經給出了使用 scanf() 清空緩沖區的方案,就是:
scanf("%*[^\n]"); scanf("%*c");
下面我們就來解釋一下。
首先需要明白的是,等到需要清空緩沖區的時候,緩沖區中的最后一個字符一定是換行符\n
,因為輸入緩沖區是行緩沖模式,用戶按下回車鍵會產生換行符,結束本次輸入,然后輸入函數開始讀取。scanf("%*[^\n]");
將換行符前面的所有字符清空,scanf("%*c");
將最后剩下的換行符清空。
有些網友將這兩條語句合並起來,寫作:
scanf("%*[^\n]%*c");
這是錯誤的。合並以后的語句不能清空單個換行符,因為該語句要求換行符前邊至少要有一個其它的字符,單個換行符會導致匹配失敗。
總結
scanf() 控制字符串的完整寫法為:
%{*} {width} type
其中,{ } 表示可有可無。各個部分的具體含義是:
type
表示讀取什么類型的數據,例如 %d、%s、%[a-z]、%[^\n] 等;type 必須有。width
表示最大讀取寬度,可有可無。*
表示丟棄讀取到的數據,可有可無。