專欄導航
本節文章分別屬于《Win32 學習筆記》和《MFC 學習筆記》兩個專欄,故劃分為兩個專欄導航。讀者可以自行選擇前往哪個專欄。
(一)WIn32 專欄導航
上一篇:編程技能:格式化打印03,printf
回到目錄
下一篇:無
(二)MFC 專欄導航
上一篇:編程技能:格式化打印03,printf
回到目錄
下一篇:無
本節前言
在上一節,我們講解了 printf 的實現代碼。
本節,我們來講解與之相近的 sprintf 的實現代碼。
對于本節的講解,是基于 vsprintf 的知識的。
如果你尚未學習過 vsprintf 的知識,請參考下述鏈接所示的文章。
編程技能:格式化打印02,vsprintf-CSDN博客
確保你理解了 vsprintf 的大致邏輯之后,接下來,我們來學習 printf 的實現代碼。
在本節里面,我會用到三個術語。他們分別是【棧指針】,【格式字符串】和【參數列表】。這三個術語,均可以從 vsprintf 文章鏈接中找到。如果你不懂這些術語,請你先去上面的 vsprintf 文章鏈接中,學習好 vsprintf ,然后再進行本節的學習。
我們開始本節的講解。
一.? ? sprintf 函數的實現代碼
代碼如下。
extern int sprintf(char * str, const char *fmt, ...)
{va_list args;int i;va_start(args, fmt);i = vsprintf(str, fmt, args);va_end(args);return i;
}
這段代碼,改編自 Linux 0.12 內核。絕大部分的代碼,都與 0.12 內核一樣。但是呢,我也的確是作出了一點小小的改動。
二.? ? 調用 sprintf 函數的三個案例
下面是案例1 的代碼。
static char buf01[300];
void func01(void)
{int i, a, c;float b;a = 90;b = 3.14;c = 1024;i = sprintf(buf01, "a = %d,b = %f,c = %x", a, b, c);return;
}
以上為案例1 的代碼。
下面是案例2 的代碼。
static char buf02[300];
void func02(void)
{int i;i = sprintf(buf02, "小雪的兜里有 %d 塊錢", 100);return;
}
以上為案例 2 的代碼。
下面是案例3 的代碼
static char buf03[300];
void func03(void)
{int i;i = sprintf(buf03, "I have a pen");return;
}
以上為案例3 的代碼。
這幾個代碼,我們在下面的講解中,隨時會引用。大家需要瀏覽一下這幾個代碼。一會兒,你需要隨時來查閱對照這幾個案例代碼。
三.? ? sprintf 的函數頭部
sprintf 的函數頭部如下。
extern int sprintf(char * str, const char *fmt, ...)
對于此函數的返回值,我們先不去講。我們來看看它的各個參數。
(一)緩沖區
第一個參數,是字符指針 str 。這個字符指針,是由調用 sprintf 的函數提供的。
第二分節的案例1 代碼里,包含有下述兩行代碼。
static char buf01[300];
i = sprintf(buf01,?"a = %d,b = %f,c = %x",?a, b, c);
在案例1 里面,我們聲明了一個大的字符數組,里面包含 300 個元素。數組名為 buf01 。數組元素為 char 型,所以,數組名為字符指針。而在案例1 的調用 sprintf 函數的代碼中,傳入的第一個參數,為字符數組名 buf01 。
在案例2 和案例3 里面,也有類似的代碼邏輯。也就是,申請了一個大的字符數組。同時,在調用 sprintf 函數的時候,給 sprintf 傳入的第一個參數,是這個字符數組的數組名。
一般地,我們平時在申請數組的時候,不會讓數組包含著這么多的元素個數。然而,在本節的第二分節的三個案例代碼里面,我們的確是分別申請了一個大的字符數組。
申請這么大的數組干嘛?
一般來講,當我們選擇申請一個尺寸很大的數組的時候,我們是想要用它來臨時地保存某些數據。這種臨時地保存某些數據的,尺寸很大的數組,我們可以給它換一個名字,緩沖區。
在 Linux 0.12 內核里面,為了實現 printf 函數,它是申請了包含 1024 個 char 型元素的字符數組,作為緩沖區,來給 printf 函數使用。
關于緩沖區的概念,其實,大家在基礎的 C 語言學習中,大家也聽說過的。那就是鍵盤緩沖區。
在 Linux 0.12 內核里面,就包含有一個鍵盤緩沖區,它也是用包含著 1024 個元素的大數組,來作為鍵盤緩沖區的。
buf,常常用作【緩沖區】的含義,它是 buffer 的簡寫。
我們接著往下講。
(二)格式字符串
在 sprintf 的函數頭部里面,第二個參數,為【const char *fmt】。從這里可以看出,本參數用來接收一個字符串,并且,由于 const 關鍵字的存在,我們不可以通過字符指針 fmt 對這個接收的字符串進行任何改動,而只是可以使用其值。
在這里,我將 fmt 接收的字符串,稱作格式字符串。
所謂的格式字符串,是說,它里面,可能會含有 %d,%c,%f,%s 等等的格式控制符。
在代碼【printf("a = %d,b = %f,c = %x", a, b, c);】中,【"a = %d,b = %f,c = %x"】是格式字符串。類似地,在本文的的第二分節的案例1 代碼里面,在代碼【i = sprintf(buf01, "a = %d,b = %f,c = %x", a, b, c);】中,?【"a = %d,b = %f,c = %x"】也是格式字符串,由sprintf 的第二個形參 fmt 來接收。
在代碼【printf("小雪的兜里有 %d 塊錢", 100);】里面,【"小雪的兜里有 %d 塊錢"】是格式字符串。類似地,在本文的的第二分節的案例2?代碼里面,在代碼【i = sprintf(buf02, "小雪的兜里有 %d 塊錢", 100);】中,?【"小雪的兜里有 %d 塊錢"】也是格式字符串,由sprintf 的第二個形參 fmt 來接收。
在代碼【printf("I have a pen");】里面,【"I have a pen"】是格式字符串。類似地,在本文的的第二分節的案例3?代碼里面,在代碼【i = sprintf(buf03, "I have a pen");】中,?【"I have a pen"】也是格式字符串,由sprintf 的第二個形參 fmt 來接收。
格式字符串,里面可以包含有 %d 等等的格式控制符,也可以不包含。
到了這里,sprintf 的第二個形參我們就講完了。我們接著講。
(三)可變參數
在 sprintf 函數頭部里面,在 fmt 形參的右邊,是【...】,這個東西,不是省略號。你也可以在你自己的代碼里面包含這樣的東西。不過呢,使用的時候,是三個英文句點,不可以是中文的三個句點。在數量上,必須是三個,多一個不行,少一個也不行。
這個【...】,有一個專有名字,叫做可變參數。
可變參數,是說,它的數目是可變的,每一個參數的數據類型也是可變的,沒有固定的范式。在數目方面,可變參數中,可以包含有一個參數,兩個參數,或者多個參數,也可以不包含參數。
在代碼【printf("a = %d,b = %f,c = %x", a, b, c);】中,【a, b, c】的部分,便是可變參數部分。類似地,在本文的的第二分節的案例1 代碼里面,在代碼【i = sprintf(buf01, "a = %d,b = %f,c = %x", a, b, c);】中,【a, b, c】的部分,也是可變參數部分。在此代碼示例中,可變參數部分含有三個參數。
在代碼【printf("小雪的兜里有 %d 塊錢", 100);】中,【100】的部分,為可變參數部分。類似地,在本文的的第二分節的案例2?代碼里面,在代碼【i = sprintf(buf02, "小雪的兜里有 %d 塊錢", 100);】中,【100】的部分,也是可變參數部分。在此代碼示例中,可變參數部分含有一個參數。
在代碼【printf("I have a pen");】里面,可變參數部分無參數。類似地,在本文的的第二分節的案例3?代碼里面,在代碼【i = sprintf(buf03, "I have a pen");】中,可變參數部分也是沒有參數的。
在大家平時寫代碼的時候,應該不太會去使用可變參數的。不過,這次,我們要來學習 printf,vsprintf,sprintf 等等的打印函數,那就需要來接觸可變參數了。
我盡力去講,希望大家能夠學好可變參數。
到了這里,sprintf 的函數頭部,我們就先進行到這里。返回值,后面會有講解。
四.? ? va_list
我們來看下圖的紅色框線部分的代碼。

圖1 中的紅色框線部分,是變量聲明。【int i;】,這個簡單,聲明了一個整型變量 i 。問題在于【va_list args;】這一部分,需要去講解一下的,是 va_list 。va_list,是【char *】的意思。
請看下面的參考代碼。
typedef char *va_list;
va_list,是【char *】的別名。這樣一來,由 va_list 聲明的 args 變量,便是一個【char *】類型的變量。
在這里,我來講一講 va_list 的含義。va_list 中的 va,是【variable argument】的意思,翻譯過來,就是可變參數的意思。list,是列表的含義。所以呢,va_list,整個的意思就是【可變參數列表】。用【可變參數列表】類型 va_list 聲明的變量為 args,它是 arguments 的簡寫,就是【各個參數】的意思。
我們接著往下看。
五.? ? va_start
我們來看下圖中的紅色框線部分的代碼。

va_start,直接翻譯,就是【可變參數開始】的意思。其實,它是用來對可變參數變量 args 進行初始化的宏函數。
va_start,從它的使用方法上看,似乎是一個函數。實際上,它是一個宏。對于這種,實際上為宏,而外形為函數的東西,我將其稱作宏函數。別的地方咋叫我記不清了。反正我就管它叫宏函數。
va_start,它的宏代碼,具體是什么,在這里,我不展開。原因在于,想要徹底理解其宏代碼,你需要具備匯編語言基礎。在設計本專欄教程的時候,我的一個基本的假定,就是,各位讀者并不具備匯編語言基礎。我希望在各位并不具備匯編語言基礎的情況下,也能夠看懂本專欄教程。
由于假定各位不懂匯編語言,所以,va_start 的宏代碼,我就不去細講了。但是呢,它的功能,我還得來說一說的。
va_start(args, fmt) 的意思是說,將可變參數變量 args,賦值為?fmt 右邊的可變參數列表中第一個變量的棧指針。
棧指針又是什么?
我們在講解 vsprintf 的時候,有去詳細講它。
在講解 vsprintf 的時候,我們所給出的棧指針的大致含義如下。
A 函數調用 B 函數的時候,會將 B 函數所需要的參數,也就是傳遞給 B 函數的實參,壓入棧中。完成了參數入棧的工作以后,CPU 才會前往執行 B 函數的代碼。那么,A 函數所傳遞的某一個參數在棧中的位置,就是這個實參的棧指針。
在這里,如果你需要進一步理解棧指針的含義,那么,你可以參考 vsprintf 的講解。講解 vsprintf 的文章鏈接如下所示。
編程技能:格式化打印02,vsprintf-CSDN博客
不過,說實話,我在講解 vsprintf 的時候,對棧指針的講解,做不到讓你徹底地理解棧指針的含義。因為,想要徹底地理解棧指針的概念,你需要具備匯編語言基礎。此處,你只要能夠模糊地,大致地理解棧指針的概念就可以了。
我們接著往下講。
為了輔助大家理解 va_start 的含義,我們來舉幾個例子。
在案例1 中的代碼【i = sprintf(buf01, "a = %d,b = %f,c = %x", a, b, c);】中,在調用了 sprintf 函數以后,在 sprintf 函數內部,代碼【va_start(args, fmt);】的執行,會將?args 賦值為格式字符串【"a = %d,b =?%f,c = %x"】右邊的可變參數列表中的第一個參數,a 的棧指針。
在案例2?中的代碼【i = sprintf(buf02, "小雪的兜里有 %d 塊錢", 100);】里面,在調用了 sprintf 函數以后,在 sprintf 函數內部,代碼【va_start(args, fmt);】的執行,會將?args 賦值為格式字符串【"小雪的兜里有 %d 塊錢"】右邊的可變參數列表中的第一個參數,【100】的棧指針。
在案例3?中的代碼【i = sprintf(buf03, "I have a pen");】里面,可變參數部分無參數。在這種情況下,代碼【va_start(args, fmt);】的執行也會令 args 指向一個東西,不過,在可變參數部分無參數的情況下,args 究竟是指向啥,我們就不必關心了。我們暫時只關心可變參數列表中至少含有一個參數的情形,并且將可變參數列表中包含至少一個參數的情形視為典型。
再次重復一下,va_start(args, fmt) 的意思是說,將可變參數變量 args,賦值為?fmt 所指向的格式字符串右邊的可變參數列表中的第一個變量的棧指針。
正是在 va_start 宏函數的作用之下,可變參數列表中的第一個參數的棧指針,被提取出來了。
不知道,你理解得是否費勁兒。反正,此刻,我是覺得,講得挺費勁兒的。
接著來吧。
六.? ? 調用 vsprintf
我們接著看下圖的紅色框線所示的代碼。

在圖3 的紅色框線部分,我們調用了 vsprintf 函數,傳給它的三個參數,分別是由上一級的調用函數傳過來的字符緩沖區指針【str】,格式字符串的指針【fmt】,還有可變參數列表中第一個參數的棧指針【args】。
vsprintf 函數的功能,是根據 fmt 與 args,對 fmt 所指向的格式字符串進行格式化轉換,轉換結果放在字符緩沖區指針 str 所指向的字符數組里面。格式化轉換的工作完成以后,str 中的字符串的不含 NUL 結束符的有效字符長度,會作為返回值,予以返回。在圖3 里面,這個返回值由 int 型變量 i 來接收了。
vsprintf 是如何進行格式化轉換的,請大家參考 vsprintf 文章鏈接,鏈接如下。
編程技能:格式化打印02,vsprintf-CSDN博客
不過呢,在這里,雖說我不想細講 vsprintf 的執行過程。不過呢,我還是想要舉幾個例子。
例如,在執行本文第二分節的案例1 中的代碼【i = sprintf(buf01, "a = %d,b = %f,c = %x", a, b, c);】以后,sprintf 會在內部調用 vsprintf 。 vsprintf 會根據格式字符串【"a = %d,b =?%f,c = %x"】與可變參數列表中第一個參數的棧指針,對格式字符串進行格式化轉換。對于案例1 而言,格式化轉換的結果為【"a = 90,b = 3.14,c = 400"】,這個轉換結果會被存放在 str 緩沖區里面,或者說是存放在案例1 中的 buf01 緩沖區里面。十進制 1024?的十六進制值為 0x400?。我在格式字符串里面,沒有給十六進制值加上十六進制前綴,你可以自己添加啊。這里,我偷個懶。
再比如,在執行本文第二分節的案例2?中的代碼【i = sprintf(buf02, "小雪的兜里有 %d 塊錢", 100);】以后,sprintf 會在內部調用 vsprintf 。 vsprintf 會根據格式字符串【"小雪的兜里有 %d 塊錢"】與可變參數列表中第一個參數的棧指針,對格式字符串進行格式化轉換。格式化轉換的結果為【"小雪的兜里有 100?塊錢"】,這個轉換結果會被存放在 str 緩沖區里面,或者說是存放在案例2 中的 buf02 緩沖區里面。
再比如,在執行本文第二分節的案例3?中的代碼【i = sprintf(buf03, "I have a pen");】以后,sprintf 會在內部調用 vsprintf 。 vsprintf 會根據格式字符串【"I have a pen"】與可變參數列表中第一個參數的棧指針,對格式字符串進行格式化轉換。在此例子代碼中,由于格式字符串里面不含有格式控制符,可變參數列表里面也不含有參數,因此,vsprintf 的工作,是直接把字符串【"I have a pen"】放在 str?緩沖區中,或者說是存放在案例3 中的 buf03 緩沖區中,而并不需要專門的格式化轉換工作。
我們接著往下看。
七.? ? va_end
我們來看下圖的紅色框線部分所示的代碼。

va_end,依然是一個宏函數。
va_end(args),它的作用,是將 args 置空。由于 args 是 va_list 類型的,也就是【char *】類型,所以呢,【va_end(args);】的作用,相當于執行代碼【args = NULL;】。就這么簡單。在這里,我將 va_end 的宏代碼給展示一下。
#define va_end(AP) (AP = NULL)
這個宏代碼,應該是不難吧?
如果不理解的話,請查閱 C 語言基礎知識教材。
在 va_start 宏函數里面,初始化了 args 變量。在調用 vsprintf 的時候,使用了 args 變量。最后呢,在 va_end 宏函數里面,清理了 args 變量。
我們接著往下看。
八.? ? 打印輸出與返回
請先看下圖的紅色框線所示的代碼。

圖5 中的紅色框線部分,是返回語句【return i】。
關于變量 i,在本文的第六節中,講解 vsprintf 函數調用的時候,我談到了,vsprintf 會將完成了格式化轉換工作以后,保存在 str 緩沖區中的字符串的不含有 NUL 結束符的有效字符長度作為返回值,予以返回。這個返回值,被賦值給 int 型變量 i 了。
所以呢,此時,變量 i 里面所保存的,是 str 緩沖區中的字符串的不含有 NUL結束符的有效字符長度。
而在 sprintf 函數實現代碼的末尾,這個有效字符長度,再一次作為返回值,予以返回了。
在我的學習經歷中,這個返回值,還是有用的。至于究竟是如何來使用,我們在以后的 Windows 編程教程中,會見到的。
WIndows 編程,我打算分為 Win32 與 MFC 兩塊來講。在 MFC 專欄里面,可能對 sprintf 用得不多。然而,在 Win32 專欄里面,我們用 sprintf 會多一些。當然了,我們不是直接用 sprintf ,而是用它的 WIndows 版本,wsprintf 函數。
九.? ? sprintf 函數總結
我們對 sprintf 的函數功能進行一個小小的總結。
我們還是先來看一下 sprintf 的函數頭部。
extern int sprintf(char * str,?const char *fmt, ...);
sprintf 函數的功能,根據 fmt 所指向的格式字符串與可變參數列表中第一個參數的棧指針,對格式字符串進行格式化轉換工作。轉換好的字符串,會被存放在 str 緩沖區里面。函數的返回值,為格式化轉換工作完成以后,存放在?str 緩沖區中的字符串的不含有 NUL 結束符的有效字符長度。
對函數返回值部分,我特意用橄欖色的粗體字來標識了。我還是再來重復一下。簡單地來講,sprintf 函數的返回值,為有效字符長度。
到了這里,本節的講課任務就都完成了。
結束語
相比上一節講解 printf 的時候來講,本節的寫作,算是輕松了一些,因為大部分都是復制粘貼。然而,也還有許多東西是需要去修改的。
sprintf,我認為是比較重要的函數。希望大家能夠學習好它。
其實,到了這里,主體任務,vsprintf,printf,sprintf,這三大格式化打印函數,我們就都講完了。到了這里,格式化打印部分,可以說是完成了。
不過,我還想要補充一點東西。那就是,對于格式控制符,它還有一些個東西,我想要去補充補充。也許,這個補充是不必要的。不過,我還是想要補充一下。
下一節,將會是格式化打印板塊的最后一節。同時,Windows 編程的預備知識部分,也將在下一節畫上句號。
本節結束。
專欄導航
本節文章分別屬于《Win32 學習筆記》和《MFC 學習筆記》兩個專欄,故劃分為兩個專欄導航。讀者可以自行選擇前往哪個專欄。
(一)WIn32 專欄導航
上一篇:編程技能:格式化打印03,printf
回到目錄
下一篇:無
(二)MFC 專欄導航
上一篇:編程技能:格式化打印03,printf
回到目錄
下一篇:無