scanf的高級用法


在解決一些限定輸入的問題時總是力不從心,直到發現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 個字符。

請看下面的例子:

  1. #include <stdio.h>
  2. int main(){
  3. int n;
  4. float f;
  5. char str[23];
  6. scanf("%2d"&n);
  7. scanf("%*[^\n]"); scanf("%*c"); //清空緩沖區
  8. scanf("%5f"&f);
  9. scanf("%*[^\n]"); scanf("%*c"); //清空緩沖區
  10. scanf("%22s", str);
  11. printf("n=%d, f=%g, str=%s\n", n, f, str);
  12. return 0;
  13. }

輸入示例 ①:

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 等。

請看下面的代碼:

  1. #include <stdio.h>
  2. int main(){
  3. char str[30];
  4. scanf("%[abcd]", str);
  5. printf("%s\n", str);
  6. return 0;
  7. }

輸入示例 ①:

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]表示讀取十六進制數字。


請看下面的演示:

  1. #include <stdio.h>
  2. int main(){
  3. char str[30];
  4. scanf("%[a-zA-Z]", str); //只讀取字母
  5. printf("%s\n", str);
  6. return 0;
  7. }

輸入示例:

abcXYZ123↙
abcXYZ

不匹配某些字符

假如現在有一種需求,就是讀取換行符以外的所有字符,或者讀取 0~9 以外的所有字符,該怎么實現呢?總不能把剩下的字符都羅列出來吧,一是麻煩,二是不現實。

C語言的開發者們早就考慮到這個問題了,scanf() 允許我們在%[ ]中直接指定某些不能匹配的字符,具體方法就是在不匹配的字符前面加上^,例如:

  • %[^\n]表示匹配除換行符以外的所有字符,遇到換行符就停止讀取;
  • %[^0-9]表示匹配除十進制數字以外的所有字符,遇到十進制數字就停止讀取。


請看下面的例子:

  1. #include <stdio.h>
  2. int main(){
  3. char str1[30], str2[30];
  4. scanf("%[^0-9]", str1);
  5. scanf("%*[^\n]"); scanf("%*c"); //清空緩沖區
  6. scanf("%[^\n]", str2);
  7. printf("str1=%s \nstr2=%s\n", str1, str2);
  8. return 0;
  9. }

輸入示例:

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:

  1. #include <stdio.h>
  2. int main(){
  3. char str[31];
  4. scanf("%30[^0-9\n]", str);
  5. printf("str=%s\n", str);
  6. return 0;
  7. }

輸入示例 ①:

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]表示將換行符以外的字符全部丟棄。


請看下面的代碼演示:

  1. #include <stdio.h>
  2. int main(){
  3. int n;
  4. char str[30];
  5. scanf("%*d %d"&n);
  6. scanf("%*[a-z]");
  7. scanf("%[^\n]", str);
  8. printf("n=%d, str=%s\n", n, str);
  9. return 0;
  10. }

輸入示例:

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表示最大讀取寬度,可有可無。
    • *表示丟棄讀取到的數據,可有可無。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM