程序是人機(jī)交互的媒介,有輸出必然也有輸入,第三章我們講解了如何將數(shù)據(jù)輸出到顯示器上,本章我們開始講解如何從鍵盤輸入數(shù)據(jù)。在C語言中,有多個(gè)函數(shù)可以從鍵盤獲得用戶輸入:
scanf():和 printf() 類似,scanf() 可以輸入多種類型的數(shù)據(jù)。
getchar()、getche()、getch():這三個(gè)函數(shù)都用于輸入單個(gè)字符。
gets():獲取一行數(shù)據(jù),并作為字符串處理。
scanf() 是最靈活、最復(fù)雜、最常用的輸入函數(shù),但它不能完全取代其他函數(shù),大家都要有所了解。
本節(jié)我們只講解 scanf(),其它的輸入函數(shù)將在下節(jié)講解。
scanf()函數(shù)
scanf 是 scan format 的縮寫,意思是格式化掃描,也就是從鍵盤獲得用戶輸入,和 printf 的功能正好相反。
我們先來看一個(gè)例子:
#include <stdio.h>
int main()
{
int a = 0, b = 0, c = 0, d = 0;
scanf("%d", &a); //輸入整數(shù)并賦值給變量a
scanf("%d", &b); //輸入整數(shù)并賦值給變量b
printf("a+b=%d\n", a+b); //計(jì)算a+b的值并輸出
scanf("%d %d", &c, &d); //輸入兩個(gè)整數(shù)并分別賦值給c、d
printf("c*d=%d\n", c*d); //計(jì)算c*d的值并輸出
return 0;
}
運(yùn)行結(jié)果:
12↙
60↙
a+b=72
10 23↙
c*d=230
↙表示按下回車鍵。
從鍵盤輸入12,按下回車鍵,scanf() 就會(huì)讀取輸入數(shù)據(jù)并賦值給變量 a;本次輸入結(jié)束,接著執(zhí)行下一個(gè) scanf() 函數(shù),再從鍵盤輸入 60,按下回車鍵,就會(huì)將 60 賦值給變量 b,都是同樣的道理。
第 8 行代碼中,scanf() 有兩個(gè)以空格分隔的%d,后面還跟著兩個(gè)變量,這要求我們一次性輸入兩個(gè)整數(shù),并分別賦值給 c 和 d。注意"%d %d"之間是有空格的,所以輸入數(shù)據(jù)時(shí)也要有空格。對(duì)于 scanf(),輸入數(shù)據(jù)的格式要和控制字符串的格式保持一致。
其實(shí) scanf 和 printf 非常相似,只是功能相反罷了:
scanf("%d %d", &a, &b); // 獲取用戶輸入的兩個(gè)整數(shù),分別賦值給變量 a 和 b
printf("%d %d", a, b); // 將變量 a 和 b 的值在顯示器上輸出
它們都有格式控制字符串,都有變量列表。不同的是,scanf 的變量前要帶一個(gè)&符號(hào)。&稱為取地址符,也就是獲取變量在內(nèi)存中的地址。
在《數(shù)據(jù)在內(nèi)存中的存儲(chǔ)》一節(jié)中講到,數(shù)據(jù)是以二進(jìn)制的形式保存在內(nèi)存中的,字節(jié)(Byte)是最小的可操作單位。為了便于管理,我們給每個(gè)字節(jié)分配了一個(gè)編號(hào),使用該字節(jié)時(shí),只要知道編號(hào)就可以,就像每個(gè)學(xué)生都有學(xué)號(hào),老師會(huì)隨機(jī)抽取學(xué)號(hào)來讓學(xué)生回答問題。字節(jié)的編號(hào)是有順序的,從 0 開始,接下來是 1、2、3……
下圖是 4G 內(nèi)存中每個(gè)字節(jié)的編號(hào)(以十六進(jìn)制表示):
?

這個(gè)編號(hào),就叫做地址(Address)。int a;會(huì)在內(nèi)存中分配四個(gè)字節(jié)的空間,我們將第一個(gè)字節(jié)的地址稱為變量 a 的地址,也就是&a的值。對(duì)于前面講到的整數(shù)、浮點(diǎn)數(shù)、字符,都要使用 & 獲取它們的地址,scanf 會(huì)根據(jù)地址把讀取到的數(shù)據(jù)寫入內(nèi)存。
我們不妨將變量的地址輸出看一下:
#include <stdio.h>
int main()
{
int a='F';
int b=12;
int c=452;
printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);
return 0;
}
輸出結(jié)果:
&a=0x18ff48, &b=0x18ff44, &c=0x18ff40
%p是一個(gè)新的格式控制符,它表示以十六進(jìn)制的形式(帶小寫的前綴)輸出數(shù)據(jù)的地址。如果寫作%P,那么十六進(jìn)制的前綴也將變成大寫形式。
?

圖:a、b、c 的內(nèi)存地址
注意:這里看到的地址都是假的,是虛擬地址,并不等于數(shù)據(jù)在物理內(nèi)存中的地址。虛擬地址是現(xiàn)代計(jì)算機(jī)因內(nèi)存管理的需要才提出的概念,我們將在《C語言內(nèi)存精講》專題中詳細(xì)講解。
再來看一個(gè) scanf 的例子:
#include <stdio.h>
int main()
{
int a, b, c;
scanf("%d %d", &a, &b);
printf("a+b=%d\n", a+b);
scanf("%d %d", &a, &b);
printf("a+b=%d\n", a+b);
scanf("%d, %d, %d", &a, &b, &c);
printf("a+b+c=%d\n", a+b+c);
scanf("%d is bigger than %d", &a, &b);
printf("a-b=%d\n", a-b);
return 0;
}
運(yùn)行結(jié)果:
10? ? 20↙
a+b=30
100 200↙
a+b=300
56,45,78↙
a+b+c=179
25 is bigger than 11↙
a-b=14
第一個(gè) scanf() 的格式控制字符串為"%d %d",中間有一個(gè)空格,而我們卻輸入了10??? 20,中間有多個(gè)空格。第二個(gè) scanf() 的格式控制字符串為"%d?? %d",中間有多個(gè)空格,而我們卻輸入了100 200,中間只有一個(gè)空格。這說明 scanf() 對(duì)輸入數(shù)據(jù)之間的空格的處理比較寬松,并不要求空格數(shù)嚴(yán)格對(duì)應(yīng),多幾個(gè)少幾個(gè)無所謂,只要有空格就行。
第三個(gè) scanf() 的控制字符串為"%d, %d, %d",中間以逗號(hào)分隔,所以輸入的整數(shù)也要以逗號(hào)分隔。
第四個(gè) scanf() 要求整數(shù)之間以is bigger than分隔。
用戶每次按下回車鍵,程序就會(huì)認(rèn)為完成了一次輸入操作,scanf() 開始讀取用戶輸入的內(nèi)容,并根據(jù)格式控制字符串從中提取有效數(shù)據(jù),只要用戶輸入的內(nèi)容和格式控制字符串匹配,就能夠正確提取。
本質(zhì)上講,用戶輸入的內(nèi)容都是字符串,scanf() 完成的是從字符串中提取有效數(shù)據(jù)的過程。
連續(xù)輸入
在本節(jié)第一段示例代碼中,我們一個(gè)一個(gè)地輸入變量 a、b、c、d 的值,每輸入一個(gè)值就按一次回車鍵?,F(xiàn)在我們改變輸入方式,將四個(gè)變量的值一次性輸入,如下所示:
12 60 10 23↙
a+b=72
c*d=230
可以發(fā)現(xiàn),兩個(gè) scanf() 都能正確讀取。合情合理的猜測是,第一個(gè) scanf() 讀取完畢后沒有拋棄多余的值,而是將它們保存在了某個(gè)地方,下次接著使用。
如果我們多輸入一個(gè)整數(shù),會(huì)怎樣呢?
12 60 10 23 99↙
a+b=72
c*d=230
這次我們多輸入了一個(gè) 99,發(fā)現(xiàn) scanf() 仍然能夠正確讀取,只是 99 沒用罷了。
如果我們少輸入一個(gè)整數(shù),又會(huì)怎樣呢?
12 60 10↙
a+b=72
23↙
c*d=230
輸入三個(gè)整數(shù)后,前兩個(gè) scanf() 把前兩個(gè)整數(shù)給讀取了,剩下一個(gè)整數(shù) 10,而第三個(gè) scanf() 要求輸入兩個(gè)整數(shù),一個(gè)單獨(dú)的 10 并不能滿足要求,所以我們還得繼續(xù)輸入,湊夠兩個(gè)整數(shù)以后,第三個(gè) scanf() 才能讀取完畢。
從本質(zhì)上講,我們從鍵盤輸入的數(shù)據(jù)并沒有直接交給 scanf(),而是放入了緩沖區(qū)中,直到我們按下回車鍵,scanf() 才到緩沖區(qū)中讀取數(shù)據(jù)。如果緩沖區(qū)中的數(shù)據(jù)符合 scanf() 的要求,那么就讀取結(jié)束;如果不符合要求,那么就繼續(xù)等待用戶輸入,或者干脆讀取失敗。我們將在本章的《進(jìn)入緩沖區(qū)(緩存)的世界,破解一切與輸入輸出有關(guān)的疑難雜癥》《結(jié)合C語言緩沖區(qū)談scanf函數(shù)》兩節(jié)中詳細(xì)講解緩沖區(qū)。
注意,如果緩沖區(qū)中的數(shù)據(jù)不符合 scanf() 的要求,要么繼續(xù)等待用戶輸入,要么就干脆讀取失敗,上面我們演示了“繼續(xù)等待用戶輸入”的情形,下面我們對(duì)代碼稍作修改,演示“讀取失敗”的情形。
#include <stdio.h>
int main()
{
int a = 1, b = 2, c = 3, d = 4; //修改處:給變量賦予不同的初始值
scanf("%d", &a);
scanf("%d", &b);
printf("a=%d, b=%d\n", a, b);
scanf("%d %d", &c, &d);
printf("c=%d, d=%d\n", c, d);
return 0;
}
運(yùn)行結(jié)果:
12 60 a10↙
a=12, b=60
c=3, d=4
前兩個(gè)整數(shù)被正確讀取后,剩下了 a10,而第三個(gè) scanf() 要求輸入兩個(gè)十進(jìn)制的整數(shù),a10 無論如何也不符合要求,所以只能讀取失敗。輸出結(jié)果也證明了這一點(diǎn),c 和 d 的值并沒有被改變。
這說明 scanf() 不會(huì)跳過不符合要求的數(shù)據(jù),遇到不符合要求的數(shù)據(jù)會(huì)讀取失敗,而不是再繼續(xù)等待用戶輸入。
總而言之,正是由于緩沖區(qū)的存在,才使得我們能夠多輸入一些數(shù)據(jù),或者一次性輸入所有數(shù)據(jù),這可以認(rèn)為是緩沖區(qū)的一點(diǎn)優(yōu)勢。然而,緩沖區(qū)也帶來了一定的負(fù)面影響,甚至?xí)?dǎo)致很奇怪的行為,請(qǐng)看下面的代碼:
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
輸入示例:
a=99↙
a=99, b=2
輸入a=99,按下回車鍵,程序竟然運(yùn)行結(jié)束了,只有第一個(gè) scanf() 成功讀取了數(shù)據(jù),第二個(gè) scanf() 仿佛沒有執(zhí)行一樣,根本沒有給用戶任何機(jī)會(huì)去輸入數(shù)據(jù)。
如果我們換一種輸入方式呢?
a=99b=200↙
a=99, b=200
這樣 a 和 b 都能夠正確讀取了。注意,a=99b=200中間是沒有任何空格的。
肯定有好奇的小伙伴又問了,如果a=99b=200兩個(gè)數(shù)據(jù)之間有空格又會(huì)怎么樣呢?我們不妨親試一下:
a=99 b=200↙
a=99, b=2
你看,第二個(gè) scanf() 又讀取失敗了!在前面的例子中,輸入的兩份數(shù)據(jù)之前都是有空格的呀,為什么這里不能帶空格呢,真是匪夷所思。好吧,這個(gè)其實(shí)還是跟緩沖區(qū)有關(guān)系,我將在《結(jié)合C語言緩沖區(qū)談scanf()函數(shù)》中深入講解。
要想破解 scanf() 輸入的問題,一定要學(xué)習(xí)緩沖區(qū),它能使你對(duì)輸入輸出的認(rèn)識(shí)上升到一個(gè)更高的層次,以后不管遇到什么疑難雜癥,都能迎刃而解??梢哉f,輸入輸出的“命門”就在于緩沖區(qū)。
輸入其它數(shù)據(jù)
除了輸入整數(shù),scanf() 還可以輸入單個(gè)字符、字符串、小數(shù)等,請(qǐng)看下面的演示:
#include <stdio.h>
int main()
{
char letter;
int age;
char url[30];
float price;
scanf("%c", &letter);
scanf("%d", &age);
scanf("%s", url); //可以加&也可以不加&
scanf("%f", &price);
printf("26個(gè)英文字母的最后一個(gè)是 %c。\n", letter);
printf("C語言中文網(wǎng)已經(jīng)成立%d年了,網(wǎng)址是 %s,開通VIP會(huì)員的價(jià)格是%g。\n", age, url, price);
return 0;
}
運(yùn)行示例:
z↙
6↙
http://c.biancheng.net↙
159.9↙
26個(gè)英文字母的最后一個(gè)是 z。
C語言中文網(wǎng)已經(jīng)成立6年了,網(wǎng)址是 http://c.biancheng.net,開通VIP會(huì)員的價(jià)格是159.9。
scanf() 和 printf() 雖然功能相反,但是格式控制符是一樣的,單個(gè)字符、整數(shù)、小數(shù)、字符串對(duì)應(yīng)的格式控制符分別是 %c、%d、%f、%s。
對(duì)讀取字符串的說明
在《在C語言中使用英文字符》一節(jié)中,我們談到了字符串的兩種定義形式,它們分別是:
char str1[] = "http://c.biancheng.net";
char *str2 = "C語言中文網(wǎng)";
這兩種形式其實(shí)是有區(qū)別的,第一種形式的字符串所在的內(nèi)存既有讀取權(quán)限又有寫入權(quán)限,第二種形式的字符串所在的內(nèi)存只有讀取權(quán)限,沒有寫入權(quán)限。printf()、puts() 等字符串輸出函數(shù)只要求字符串有讀取權(quán)限,而 scanf()、gets() 等字符串輸入函數(shù)要求字符串有寫入權(quán)限,所以,第一種形式的字符串既可以用于輸出函數(shù)又可以用于輸入函數(shù),而第二種形式的字符串只能用于輸出函數(shù)。
另外,對(duì)于第一種形式的字符串,在[ ]里面要指明字符串的最大長度,如果不指明,也可以根據(jù)=后面的字符串來自動(dòng)推算,此處,就是根據(jù)"http://c.biancheng.net"的長度來推算的。但是在前一個(gè)例子中,開始我們只是定義了一個(gè)字符串,并沒有立即給它賦值,所以沒法自動(dòng)推算,只能手動(dòng)指明最大長度,這也就是為什么一定要寫作char url[30],而不能寫作char url[]的原因。
讀者還要注意第 11 行代碼,這行代碼用來輸入字符串。上面我們說過,scanf() 讀取數(shù)據(jù)時(shí)需要的是數(shù)據(jù)的地址,整數(shù)、小數(shù)、單個(gè)字符都要加&取地址符,這很容易理解;但是對(duì)于此處的 url 字符串,我們并沒有加 &,這是因?yàn)?,字符串的名字?huì)自動(dòng)轉(zhuǎn)換為字符串的地址,所以不用再多此一舉加 & 了。當(dāng)然,你也可以加上,這樣雖然不會(huì)導(dǎo)致錯(cuò)誤,但是編譯器會(huì)產(chǎn)生警告,至于為什么,我們將會(huì)在《數(shù)組和指針絕不等價(jià),數(shù)組是另外一種類型》《數(shù)組到底在什么時(shí)候會(huì)轉(zhuǎn)換為指針》中講解。
關(guān)于字符串,后續(xù)章節(jié)我們還會(huì)專門講解,這里只要求大家會(huì)模仿,不要徹底理解,也沒法徹底理解。
最后需要注意的一點(diǎn)是,scanf() 讀取字符串時(shí)以空格為分隔,遇到空格就認(rèn)為當(dāng)前字符串結(jié)束了,所以無法讀取含有空格的字符串,請(qǐng)看下面的例子:
#include <stdio.h>
int main()
{
char author[30], lang[30], url[30];
scanf("%s %s", author, lang);
printf("author:%s \nlang: %s\n", author, lang);
scanf("%s", url);
printf("url: %s\n", url);
return 0;
}
運(yùn)行示例:
YanChangSheng C-Language↙
author:YanChangSheng
lang: C-Language
http://c.biancheng.net http://biancheng.net↙
url: http://c.biancheng.net
對(duì)于第一個(gè) scanf(),它將空格前邊的字符串賦值給 author,將空格后邊的字符串賦值給 lang;很顯然,第一個(gè)字符串遇到空格就結(jié)束了,第二個(gè)字符串到了本行的末尾結(jié)束了。
或許第二個(gè) scanf() 更能說明問題,我們輸入了兩個(gè)網(wǎng)址,但是 scanf() 只讀取了一個(gè),就是因?yàn)檫@兩個(gè)網(wǎng)址以空格為分隔,scanf() 遇到空格就認(rèn)為字符串結(jié)束了,不再繼續(xù)讀取了。
scanf() 格式控制符匯總
格式控制符說明
%c讀取一個(gè)單一的字符
%hd、%d、%ld讀取一個(gè)十進(jìn)制整數(shù),并分別賦值給 short、int、long 類型
%ho、%o、%lo讀取一個(gè)八進(jìn)制整數(shù)(可帶前綴也可不帶),并分別賦值給 short、int、long 類型
%hx、%x、%lx讀取一個(gè)十六進(jìn)制整數(shù)(可帶前綴也可不帶),并分別賦值給 short、int、long 類型
%hu、%u、%lu讀取一個(gè)無符號(hào)整數(shù),并分別賦值給 unsigned short、unsigned int、unsigned long 類型
%f、%lf讀取一個(gè)十進(jìn)制形式的小數(shù),并分別賦值給 float、double 類型
%e、%le讀取一個(gè)指數(shù)形式的小數(shù),并分別賦值給 float、double 類型
%g、%lg既可以讀取一個(gè)十進(jìn)制形式的小數(shù),也可以讀取一個(gè)指數(shù)形式的小數(shù),并分別賦值給 float、double 類型
%s讀取一個(gè)字符串(以空白符為結(jié)束)