C語言中文網 目錄

printf函數和scanf函數,C語言printf函數和scanf函數詳解

對于 printf 函數,相信大家并不陌生。之所以稱它為格式化輸出函數,關鍵就是該函數可以按用戶指定的格式,把指定的數據顯示到顯示器屏幕上。該函數原型的一般格式如下:

int printf(const char * format, ... );

很顯然,與其他庫函數不同的是,printf 函數是一個“可變參數函數”(即函數參數的個數是可變的)。確切地說,是其輸出參數的個數是可變的,且每一個輸出參數的輸出格式都有對應的格式說明符與之對應,從格式串的左端第 1 個格式說明符對應第 1 個輸出參數,第 2 個格式說明符對應第 2 個輸出參數,第 3 個格式說明符對應第 3 個輸出參數,以此類推。其中,格式說明符的一般形式如下(方括號 [] 中的項為可選項):

%[flags][width][.prec][length] type_char
/*用中文標識如下:*/
%[標志符][寬度][精度][長度]類型符

1) 類型符(type_char)

它用以表示輸出數據的類型,如表 1 所示。

表 1 常見的類型符及其說明
符 號 類 型 說 明 示 例 結 果
% 輸出字符“%”本身 pnntf("%%"); %
d、i int 以整型輸出 printf("%i,%d", 100,100); 100,100
u unsigned int 以無符號整型輸出 printf( "%u,%u",100u,100); 100,100
o unsigned int 以八進制無符號整S輸出 printf( "%o”,100); 144
x unsigned int 以十六進制小寫輸出 printf("%x",11); b
X unsigned int 以十六制大寫輸出 printf("%X",11); B

除表 1 所示的類型符之外,還有一個比較特殊與另類的類型符“%n”,當在格式化字符串中碰到“%n”時,在“%n”之前輸出的字符個數會保存到下一個參數里。例如,下面的示例代碼演示了如何獲取在兩個格式化的數字之間空間的偏量:
int main(void)
{
    int pos=0;
    int x = 123;
    int y = 456;
    printf("%d%n%d\n", x, &pos, y);
    printf("pos=%d\n", pos);
    return 0;
}
很顯然,上面代碼中的 pos 將輸出 3,即“123”的長度,運行結果為:
123456
pos=3

這里需要特別注意,“%n”返回的是應該被輸出的字符數目,而不是實際輸出的字符數目。當把一個字符串格式化輸出到一個定長緩沖區內時,輸出字符串可能被截短。不考慮截短的影響,“%n”格式表示如果不被截短的偏量值(輸出字符數目)。看下面的示例代碼:
int main(void)
{
    char buf[20];
    int pos=0;
    int x = 0;
    snprintf(buf, sizeof(buf), "%.100d%n", x, &pos);
    printf("pos=%d\n", pos);
    return 0;
}
很顯然,上面的代碼會輸出 100,而不是 20。

由此可見,相對于“%d”“%x”“%s”等,“%n”的顯著不同之處就是“%n”會改變變量的值,這也就是格式化字符串攻擊的爆破點,如下面的示例代碼所示:
char daddr[16];
int main(void)
{
    char buf[100];
    int x=1;
    memset(daddr,'/0',16);
    printf("前X: %d/%#x (%p)\n", x, x, &x);
    strncpy(daddr,"AAAAAAA%n",9);
    snprintf(buf,sizeof(buf),daddr);
    buf[sizeof(buf) - 1] = 0;
    printf("后X: %d/%#x (%p)\n",x, x, &x);
    return 0;
}
在上面的代碼中,x 將被從 1 修改成 7,其運行結果為:
前X: 1/0x1 (0061FEA8)
后X: 7/0x7 (0061FEA8)

之所以會出現這樣的結果,是因為程序在調用 snprintf 函數之前,首先調用了 printf 函數,而這時 printf 函數的 &x 參數在 main 函數的堆棧內存中留下了 &x 的內存殘像。當調用 snprintf 時,系統本來只給 snprintf 準備了 3 個參數,但是由于格式化字符串攻擊原因,使得 snprinf 認為應該有 4 個參數傳給它,這樣 snprintf 就私自把 &x 的內存殘像作為第 4 個參數讀走,而 snprintf 所謂的第 4 個參數對應的就是“%n”,于是 snprintf 就成功修改了變量 x 的值。這也就是最常見的使用 Linux 函數調用時的內存殘像來實現格式化字符串攻擊的方法之一,所以在使用的時候一定要注意。

2) 標志符(flags)

它用于規定輸出格式,如表 2 所示。

表 2 標志符及其說明
符號 說 明
(空白) 右對齊,左邊填充 0 和空格
(空格) 輸出值為正時加上空格,為負時加上負號
- 輸出結果為左對齊(默認為右對齊),邊填空格(如果存在表格最后一行介紹的0,那么將忽略0)
+ 在數字前增加符號“+”(正號)或“-”(負號)
# 類塑符是o、x、X吋,增加前綴0、0x、0X;類型符是e、E、f、F、g、G時,一定要使用小數點;類型符是g、G時,尾部的 0 保留
0 參數的前面用0填充,直到占滿指定列寬為止(如果同時存在“-”,將被“-”覆蓋,導致 0 被忽略

3) 寬度(width)

它用于控制顯示數值的寬度,如表 3 所示。

表 3 寬度及其說明
符號 說 明
n 至少輸出 n 個字符(n 是一個正整數)。如果輸出少于 n 個字符,則用空格填滿余下的位置(如果標識符為“-”,則在右側填,否則在左端填)
0n 至少輸出 n 個字符(n 是一個正整數)。如果輸出值少于 n 個字符,則在左側填滿 0
* 輸出字符個數由下一個輸出參數指定(其必須為一個整形量)

4) 精度(.prec)

它用于控制顯示數值的精度。如果輸出的是數字,則表示小數的位數;如果輸出的是字符,則表示輸出字符的個數;若實際位數大于所定義的精度數,則截去超過的部分。如表 4 所示。
表 4 精度及其說明
符號 說 明
系統默認精度
.0 對于 d、i、o、u、x、X等整形類型符,采用系統默認精度;對于f、F、e、E等浮點類型符,不輸出小數部分
.n 1) 對于d、i、o、u、x、X類型符,至少輸出 n 位數字,且:
  • 如果對應的輸出參數少于 n 位數字,則在其左端用零(0)填充
  • 如果對應的輸出參數多于 n 位數字,則輸出時不對其進行截斷
2) 對于f、F、e、E類型符,輸出結果保留 n 位小數。如果小數部分多于 n 位,則對其四舍五入
3) 對于 g 和 G 類型符,最多輸出 n 位有效數字
4) 對于 s 類型符,如果對應的輸出串的長度不超過 n 個字符,則將其原樣輸出,否則輸出其頭 n 個寧符
* 輸出精度由下一個輸出參數指定(其必須為一個整型量)

5) 長度(length)

它用于控制顯示數值的長度,如表 5 所示。

表 5 長度及其說明
符號 說 明
hh 與d、i 一起使用,表示一個signed char 類型的值;與o、u、x、X—起使用,表示一個unsigned char 類型的值;與 n 一起使用,表示相應的變元是指向 signed char 型變量的指針(c99 )
h 與d、i、o、u、x、X 或 n 一起使用,表示一個short int 或 unsigned short int 類型的值
l 與d、i、o、u、x、X 或 n 一起使用,表示一個 long int 或者 unsigned long int 類型的值
ll 與 d、i、o、u、x、X 或 n —起使用,表示相應的變元是 long long int 或 unsigned long long int 類型的值(c99 )
j 與 d、i、o、u、x、X 或 n —起使用,表示匹配的變元是 intmax_t 或 uintmax_t 類型,這些類型在“stdint. h”中聲明(c99 )
z 與 d、i、o、u、x、X 或 n —起使用,表示匹配的變元是指向 size_t 類型對象的指針,該類型在“stddef. h”中聲明(c99 )
t 與d、i、o、u、x、X 或 n —起使用,表示匹配的變元是指向 ptrdiff_t 類型對象的指針,該類型在“stddef. h”中聲明(c99 )
L 和a、A、e、E、f、F、g、G—起使用,表示一個long double類型的值

最后,在使用 printf 函數時還必須注意,盡量不要在 printf 語句中改變輸出變量的值,因為可能會造成輸出結果的不確定性。如下面的示例代碼所示:
int k=8;
printf("%d,%d\n",k,++k);
對于上面的代碼,表面上看起來輸出的結果應該是“8,9”。但實際情況并非如此,在調用printf函數時,其參數是從右至左進行處理的,即將先進行 ++k 運算,所以最后的結果是“9,9”。由此可見,千萬不要在 printf 語句中試圖改變輸出變量的值,如果確實需要改變,可以按照下面的示例代碼形式來處理:
printf("%d\n",k);
printf("%d\n",++k);
這樣處理之后,其結果就是我們所需要的“8,9”了。

除此之外,每一個輸出參數的輸出格式都必須有對應的格式說明符與之一一對應,并且類型必須匹配。若二者不能夠一一對應匹配,則不能夠正確輸出,而且編譯時可能不會報錯。同時,若格式說明符個數少于輸出項個數,則多余的輸出項將不予輸出;若格式說明符個數多于輸出項個數,則可能會輸出一些毫無意義的數字亂碼。

scanf 函數

相對于 printf 函數,scanf 函數就簡單得多。scanf 函數的功能與 printf 函數正好相反,執行格式化輸入功能。即 scanf 函數從格式串的最左端開始,每遇到一個字符便將其與下一個輸入字符進行“匹配”,如果二者匹配(相同)則繼續,否則結束對后面輸入的處理。而每遇到一個格式說明符,便按該格式說明符所描述的格式對其后的輸入值進行轉換,然后將其存于與其對應的輸入地址中。以此類推,直到格式串結束為止。該函數原型的一般格式如下:
int scanf (const char *format, ...);
從函數原型可以看出,同 printf 函數相似,scanf 函數也是一個“可變參數函數”。同時,scanf 函數的第一個參數 format 也必須是一個格式化串。除此格式化串之外,scanf 函數還可以有若干個輸入地址,且對于每一個輸入地址,在格式串中都必須有一個格式說明符與之一一對應。即從格式串的左端第 1 個格式說明符對應第 1 個輸入地址,第 2 個格式說明符對應第 2 個輸入地址,第 3 個格式說明符對應第 3 個輸入地址,以此類推。

也就是說,除第 1 個格式化串參數之外,其他參數的個數是可變的,且每一個輸入地址必須指向一個合法的存儲空間,以便能正確地接受相應的輸入值。每個輸入值的轉換格式都由格式說明符決定。格式說明符的一般形式如下(方括號 [] 中的項為可選項):

%[*][width][length] type_char
/*用中文標識如下:*/
%[*][寬度][長度]類型符

在使用 scanf 函數的時候,需要特別注意的就是緩沖區問題。對 scanf 函數來說,估計最容易出錯、最令人捉摸不透的問題應該是緩沖區問題了。

下面先來看一段示例代碼:
int main(void)
{
    char c[5];
    int i=0;
    printf("輸入數據(hello):\n");
    for(i = 0;i < 5; ++i)
    {
        scanf("%c", &c[i]);
    }
    printf("輸出數據:\n");
    printf("%s\n", c);
    return 0;
}
對于上面這段示例代碼,我們希望在“c[5]”字符數組中能夠存儲“hello”字符串,并在最后輸出到屏幕上。從表面上看,這段程序沒有任何問題,但實際情況并非如此。當我們依次輸入“h(回車)”“e(回車)”,然后再輸入“l”時,問題發生了。此時,程序不僅中斷輸入操作,而且會打印出字符數組 c 中的內容,其運行結果為:
輸入數據(hello):
h
e
l
輸出數據:
h
e
l

很顯然,字符數組“c[5]”是完全能夠存儲“hello”字符串的,但為什么輸入到“l”就結束了呢?

其實原因很簡單,在我們輸入“h”和第一個回車后,“h”和這個回車符“\n”都保留在緩沖區中。第 1 個 scanf 讀取了“h”,但是輸入緩沖區里面還留有一個“\n”,于是第 2 個 scanf 讀取這個“\n”,然后輸入“e”和第 2 個回車符“\n”。同理,第 3 個 scanf 讀取了“e”,第 4 個 scanf 讀取了第 2 個回車符“\n”,第 5 個 scanf讀取了“l”。因此,程序并沒有提前結束,而是完整地循環了5次scanf語句,只不過有兩次scanf都讀取到回車符“\n”而已。

由此可見,在使用 scanf 函數時,如果不及時刷新輸入緩沖區,有時會出現莫名其妙的錯誤。對于這類問題,其實解決辦法有許多,比如可以使用“fflush(stdin);”語句來刷新輸入緩沖區。但不得不說明的是,fflush 函數在可移植性上并不是很好。當然,也可以通過自己編寫代碼來解決,如下面的示例代碼所示:
#include <stdio.h>
void flush()
{
    char c;
    while ((c=getchar()) != '\n'&&c!=EOF);
}
int main(void)
{
    char c[5];
    int i=0;
    printf("輸入數據(hello):\n");
    for(i = 0; i < 5; ++i)
    {
        scanf("%c", &c[i]);
        flush();
    }
    printf("輸出數據:\n");
    printf("%s\n", c);
    return 0;
}
這樣,就從根本上解決了輸入緩沖區問題,其運行結果為:
輸入數據(hello):
h
e
l
l
o
輸出數據:
hello

除此之外,還應該注意 scanf 中的空白符(這里所指的空白符包括空格、制表符、換行符、回車符和換頁符)帶來的問題,如下面的代碼所示:
int main(void)
{
    int a=0;
    printf("輸入數據:\n");
    /*請注意,這里多了一個回車符\n*/
    scanf("%d\n",&a);
    printf("輸出數據:\n",a);
    printf("%d\n",a);
    return 0;
}
在上面的代碼中,因為在“scanf("%d\n",&a);”語句中多加了一個回車符“\n”,導致的結果就是要輸入兩個數,程序才會正常結束,而不是我們所期望的一個數。運行結果為:
輸入數據:
22
11
輸出數據:
22

原因就是在用空白符結尾時,scanf 會跳過空白符去讀下一個字符,所以必須再輸入一個數。因此在編寫程序時一定要多注意這類手誤導致的錯誤。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo