本節分析一下printf的機理,通過編製一個自己的myprintf打印函數,進一步加深對打印輸出函數的理解,用好這個函數。
20.4.1 具有可變參數的函數
printf函數的原型聲明如下:
int printf (const char *format ,... )
按照這個格式,聲明如下的test函數。
int test (const char *format ,... )
在主函數中聲明整型變量a和b,並把它們的地址&a和&b打印出來,參照printf函數的調用方式,寫出如下對test函數的調用方法。
test ("%d ,%d\n" ,a ,b );
由此可以寫出如下主函數。
#include <stdio.h> int test (const char *format ,... ); // 聲明test 函數 int main () { int a=100 , b=-100 ; printf (" 變量a 和b 的地址:%p ,%p\n" ,&a ,&b ); // 輸出變量a 和b 的地址 test ("\n" ,a ,b ); // 調用test 函數 return 0 ; }
在test函數中再次輸出傳遞參數a和b的地址,這是函數test內的臨時變量,所以它們的地址與主函數里的地址並不相同。聲明指針p並用format初始化。因為format是字符常量指針,所以使用int強制轉換。
為了簡單。test函數內並不處理字符串,所以可以隨便賦值,這裡用一個換行符。根據對函數test的要求,編寫如下實現程序。在程序裡移動指針p,看看會帶來什麼結果。
int test (const char *format , int a , int b ) { int *p ; printf ("test 內變量a 和b 的地址:%p ,%p\n" ,&a ,&b ); p= (int* )&format ; // 指向format 地址 printf ("format :%p\n" ,p ); // 輸出format 地址 p++ ; //p 現在指向format + 1 的地址 printf ("%p ,%d\n" ,p ,*p ); // 輸出當前p 的指向地址和地址裡的內容 p++ ; // p 現在指向format + 2 的地址 printf ("%p ,%d\n" ,p ,*p ); // 輸出當前p 的指向地址和地址裡的內容 return 0 ; }
程序運行結果如下:
變量a 和b 的地址:0012FF7C ,0012FF78 test 內變量a 和b 的地址:0012FF24 ,0012FF28 format :0012FF20 0012FF24 ,100 0012FF28 ,-100
傳輸給函數test的參數在函數里將作為臨時變量被重新分配地址。format是test函數的第1個參數,被分配的地址是0012FF20,參數a為0012FF24,b為0012FF28。如果再有一個參數,將依次分配地址。這就是test內的參數地址分配規律。
因為分配給參數的地址是連續的,所以根據formart的地址就可以利用指針找到後面的參數了。在test函數里,正是利用指針依次打印出a和b的值。
為了演示變量a和b在test內分配的地址與format的關係,將它設計成只有兩個參數的函數。下面將它設計為可變參數並能將一個整數按10進制和16進制打印出來。為了分析方便,添加測試用的打印信息。
處理10進制和16進制的字符串使用標準的「%d」和「%x」,它們將作為字符串常量傳給test函數,在test函數內,將根據是「%d」還是「%x」借用printf函數輸出。
【例20.16】設計可變參數程序的例子。
#include <stdio.h> int test (const char *format ,... ); // 聲明可變參數test 函數 int main () { int a=100 ; test ("%d%x%d 結束!\n" ,a ,a ,-200 ); return 0 ; } int test (const char *format ,... ) { int *p ; char c ; int value ; p= (int* )&format ; p++ ; // 先做p++ ,使p 指向字符串常量後面的第1 個參數 while ((c = *format++ ) != '\0' ) // 循環到常量字符串結束標誌 { if (c != '%' ) // 如果不是格式字符則直接輸出 { putchar (c ); continue ; } else // 處理格式字符 { c = *format++ ; // 取% 後面的字符 if (c=='d' ) { value=*p++ ; // 將參數值賦給value ,加1 指向下一個參數 printf ("10 進制:%d\n" ,value ); // 借用測試 } if (c=='x' ) { value=*p++ ; // 將參數值賦給value ,加1 指向下一個參數 printf ("16 進制:%x\n" ,value ); // 借用測試 } } } return 0 ; }
測試時有意使用"%d%x%d結束!\n"字符串,以便演示判斷語句的正確性。程序中的註釋已經很清楚,不再贅述,下面給出程序的運行結果。
10 進制:100 16 進制:64 10 進制:-200 結束!
20.4.2 設計簡單的打印函數
test函數已經初具雛形,但它的輸出是借用了printf函數。為了設計自己的myprint函數,現在不再借用printf函數,而是設計自己的函數完成打印。
【例20.17】設計實現printf簡單功能的myprintf可變參數函數的例子。
設計自己的打印函數myprintf,實現最簡單的「%d」和「%x」功能。函數原型如下:
int myprintf (const char *format ,... );
要把數值轉換成倒序的字符串,再把字符串反序即得到正確的字符串。設計一個根據進制轉換相應的字符串函數,最後一個參數為要轉換的進制。其原型如下:
void itoa (int , char * , int );
在itoa函數里,先把數字按進制轉換為數字字符串,這是一個與給定數字逆序的字符串,直接在程序裡面設計一個宏SWAP,通過交換實現字符串反轉,得到與給定數字相同的字符串供輸出。
在調用itoa之前,還需要判斷數字的正負,如果是負整數,需要變成正整數,待轉換後再在它的前面輸出負的符號位。
因為puts函數自動在尾部實現換行,這不符合輸出要求(會多一個換行)。設計一個去掉換行的函數myputs。其原型如下:
void myputs (char *buf )
為了驗證程序,除了正負整數,也需要打印0以及與格式字符一起的其他字符。曾經提到過,對於一個字符串s,「printf(s);」與「printf("%s",s);」是不等效的,通過這個演示,將能進一步證明這一點。
// 完整的程序 #include <stdio.h> int myprintf (const char *format ,... ); // 聲明打印函數的函數原型 void myputs (char * ); // 聲明輸出字符串函數的函數原型 void itoa (int , char * , int ); // 聲明數制轉換函數的函數原型 int main ( ) { int a=100 ; char s="OK !" ; myprintf ("10 進制:%d\n16 進制:%x\n10 進制:%d 零%d\n" ,a ,a ,-100 ,0 ); myprintf (s ); myprintf (" 原來如此!\n" ); myprintf ("here !%s\n" ,s ); return 0 ; } //puts 有換行符,必須去掉,設計myputs 替代它 void myputs (char *buf ) { while (*buf ) putchar (*buf++ ); return ; } // 數制轉換函數內部使用宏定義SWAP void itoa (int num , char *buf , int base ) { char *hex= "0123456789ABCDEF" ; int i=0 ,j=0 ; do { int rest ; rest = num % base ; buf[i++]=hex[rest] ; num/=base ; }while (num !=0 ); buf[i]='\0' ; printf ("\n 逆序:%s\n" ,buf ); // 驗證信息 // 定義交換宏實現反轉 #define SWAP (a ,b ) do{a= (a )+ (b ); \ b= (a )- (b ); \ a= (a )- (b ); \ }while (0 ) // 反轉 for (j=0 ; j<i/2 ; j++ ) { SWAP (buf[j] ,buf[i-1-j] ); } printf ("\n 正序:%s\n" ,buf ); // 驗證信息 return ; } // 可變參數輸出函數 int myprintf (const char *format ,... ) { int *p ; char c ; char buf[32] ; int value ; p= (int* )&format ; p++ ; while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); // 輸出字符串中的非格式字符 continue ; } else { c = *format++ ; // 取% 後面的字符 if (c=='d' ) // 處理10 進制 { value=*p++ ; if (value<0 ) // 處理負整數 { value=-value ; itoa (value ,buf ,10 ); putchar ('-' ); myputs (buf ); } else // 處理正整數 { itoa (value ,buf ,10 ); myputs (buf ); } } if (c=='x' ) // 將10 進制正整數按16 進制處理 { value=*p++ ; itoa (value ,buf ,16 ); myputs (buf ); } } } return 0 ; }
程序輸出結果如下:
10 進制: 逆序:001 正序:100 100 16 進制: 逆序:46 正序:64 64 10 進制: 逆序:001 正序:100 -100 零 逆序:0 正序:0 0 OK !原來如此! here !
程序對0的處理正確。語句
myprintf (" 原來如此!\n" );
是由「putchar(c);」語句輸出。語句
myprintf (s );
中的字符串「OK」,也是由「putchar(c);」語句輸出。因為沒有設計「%s」的功能,所以語句
myprintf ("here !%s\n" ,s );
只是通過「putchar(c);」語句輸出「here!」,而不輸出s的內容。如果設計了「%s」的功能,則將s的內容作為字符串輸出,如果字符串裡有「%」號,它也不會處理,只會原樣輸出。對於printf函數而言,如果字符串不是自己預先設計的,而是程序運行的中間產物,都應盡可能地使用格式「%s」輸出,以免發生錯誤。
【例20.18】為myprintf函數增加處理字符和字符串的功能。
增加「%c」和「%s」的功能也很容易,為了簡潔,將調試信息去掉。下面是它的源程序。為了對照主程序的輸出結果,將主程序放在最後,其他函數按先後順序排列,所以就不需要先聲明它們的函數原型了。
#include <stdio.h> void myputs (char *buf ) { while (*buf ) putchar (*buf++ ); return ; } void itoa (int num , char *buf , int base ) { char *hex= "0123456789ABCDEF" ; int i=0 ,j=0 ; do { int rest ; rest = num % base ; buf[i++]=hex[rest] ; num/=base ; }while (num !=0 ); buf[i]='\0' ; // 定義交換宏 #define SWAP (a ,b ) do{a= (a )+ (b ); \ b= (a )- (b ); \ a= (a )- (b ); \ }while (0 ) // 反轉 for (j=0 ; j<i/2 ; j++ ) { SWAP (buf[j] ,buf[i-1-j] ); } return ; } int myprintf (const char *format ,... ) { int *p ; char c ; char buf[32] ; int value ; p= (int* )&format ; p++ ; while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); continue ; } else { c = *format++ ; // 取% 後面的字符 if (c=='c' ) { value=*p++ ; putchar (value ); } if (c=='s' ) { value=*p++ ; myputs ((char* )value ); } if (c=='d' ) { value=*p++ ; if (value<0 ) { value=-value ; itoa (value ,buf ,10 ); putchar ('-' ); myputs (buf ); } else { itoa (value ,buf ,10 ); myputs (buf ); } } if (c=='x' ) { value=*p++ ; itoa (value ,buf ,16 ); myputs (buf ); } } } return 0 ; } int main () { char c1='H' ; char c2="How are you ?" ; myprintf ("%d ,%d ,%d ,%x ,%x\n" ,100 ,0 ,-100 ,100 ,0 ); //1 驗證%d 和%x myprintf ("%c ,%s\n" ,c1 ,c2 ); //2 驗證%c 和%s myprintf ("%c ,%s\n" ,'H' ,"Fine !" ); //3 帶格式使用字符常量 myprintf ("How are you ?\n" ); //4 直接用字符串常量 myprintf (c2 ); //5 直接用字符串名字 myprintf ("%s\n" ,c2 ); //6 標準格式 myprintf ("\n" ,c2 ); //7 使用有誤,只輸出換行,不處理c2 myprintf ("How are%s" ,"you ?\n" ); //8 格式正確 return 0 ; }
主程序使用6條驗證語句,注意它們執行路徑的區別。第4條和第5條是在判別格式字符的時候直接一個字一個字地輸出。第7條有誤,但編譯系統無法識別錯誤。第8條的參數是字符常量,經由「%s」的路徑輸出。顯然,字符串作為整體輸出時的速度會快些,字符串愈長,差別愈顯著。比較下面的運行結果,仔細體會不同語句的區別。
100 ,0 ,-100 ,64 ,0 H ,How are you ? H ,Fine ! How are you ? How are you ?How are you ? How areyou ?
20.4.3 利用宏改進打印函數
標準庫實現printf函數用到了va_開頭的三個有參數宏va_start、va_arg和va_end。這些宏定義在頭文件stdarg.h中。利用這些宏可以大大簡化設計,為了看看它們的作用,設計一個不處理10進制,僅輸出參考信息的myprintf函數。va_list用來聲明一個供宏使用的指針類型的變量。
【例20.19】研究如何使用宏來簡化設計的例子。
#include <stdio.h> #include <stdarg.h> int myprintf (const char *format ,... ) { int *p ,i=101 ; va_list va_p ; //1 char c ; char buf[32]={'\0'} ; int value=0 ; p= (int* )&format ; printf ("format 的地址=%x\n" ,(int )p ); // 打印對照 p++ ; // 先做p++ ,使兩者相等,後面程序也變化 printf ("p+1 後的變量%d 的地址=%x\n" ,i ,(int )p ); // 打印對照 va_start (va_p ,format ); //2 printf ("va_p=%x\n" ,(int )va_p ); // 打印對照 while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); continue ; } else { c = *format++ ; if (c=='d' ) { printf (" 變量%d 的va_p=%x\n" , i ,(int )va_p ); // 打印對照 value=va_arg (va_p ,int ); printf (" 執行va_arg (va_p ,int )後的va_p=%x\n" , (int )va_p ); // 打印對照 i++ ; printf (" 變量%d 的va_p=%x\n" , i ,(int )va_p ); // 打印對照 printf ("%d" ,value ); } } } printf (" 結束後的va_p=%x\n" , (int )va_p ); // 打印對照 va_end (va_p ); printf (" 執行va_end (va_p )後的va_p=%x\n" , (int )va_p ); // 打印對照 return 0 ; } int main () { myprintf ("%d\n%d\n%d\n" ,101 ,102 ,103 ); return 0 ; }
程序輸出結果如下:
format 的地址=12ff24 p+1 後的變量101 的地址=12ff28 va_p=12ff28 變量101 的va_p=12ff28 執行va_arg (va_p ,int )後的va_p=12ff2c 變量102 的va_p=12ff2c 101 變量102 的va_p=12ff2c 執行va_arg (va_p ,int )後的va_p=12ff30 變量103 的va_p=12ff30 102 變量103 的va_p=12ff30 執行va_arg (va_p ,int )後的va_p=12ff34 變量104 的va_p=12ff34 103 結束後的va_p=12ff34 執行va_end (va_p )後的va_p=0
對照分析輸出結果,執行語句
va_start (va_p ,format );
的作用首先是把format地址賦給va_p,然後執行加1,這時va_p就變成第1個變量101的地址。原來的程序要執行p++才能取得變量101的地址,這就可以不需要執行+1操作了。
執行value=va_arg(va_p,int)語句,將整數值賦給value的同時,也對va_p執行加1操作,使va_p指向下一個變量102的地址12ff2c,這就可以直接取得變量102的value值。原來利用指針p時,需要執行p+1操作。改用宏,宏內執行了這一操作,所以簡化了指令。
程序循環結束後的va_p=12ff34(程序指示是變量104,其實是越界的地址),所以要求調用一個用於釋放空間的宏va_end,執行va_end(va_p)後的va_p=0。
下面的例題是使用宏完成簡單打印函數的完整程序,程序中還改用異或定義交換宏,異或運行快(加法要有進位操作),提高程序性能。
【例20.20】使用宏優化簡單打印函數的例子。
#include <stdio.h> #include <stdarg.h> void myputs (char *buf ) { while (*buf ) putchar (*buf++ ); return ; } void itoa (int num , char *buf , int base ) { char *hex= "0123456789ABCDEF" ; int i=0 ,j=0 ; do { int rest ; rest = num % base ; buf[i++]=hex[rest] ; num/=base ; }while (num !=0 ); buf[i]='\0' ; // 使用異或定義交換宏,異或運行快(加法要有進位操作) #define SWAP (a ,b ) do{a= (a )^ (b ); \ b= (a )^ (b ); \ a= (a )^ (b ); \ }while (0 ) // 反轉 for (j=0 ; j<i/2 ; j++ ) { SWAP (buf[j] ,buf[i-1-j] ); } return ; } int myprintf (const char *format ,... ) { va_list ap ; char c ; char buf[32] ; int value ; va_start (ap ,format ); while ((c = *format++ ) != '\0' ) { if (c != '%' ) { putchar (c ); continue ; } else { c = *format++ ; // 取% 後面的字符 if (c=='c' ) { putchar (va_arg (ap ,char )); } if (c=='s' ) { myputs (va_arg (ap ,char * )); } if (c=='d' ) { value=va_arg (ap ,int ); if (value<0 ) { value=-value ; itoa (value ,buf ,10 ); putchar ('-' ); myputs (buf ); } else { itoa (value ,buf ,10 ); myputs (buf ); } } if (c=='x' ) { value=va_arg (ap ,int ); itoa (value ,buf ,16 ); myputs (buf ); } } } va_end (ap ); return 0 ; } int main () { char c1='H' ; char c2="How are you ?" ; myprintf ("%d ,%d ,%d ,%x ,%x\n" ,100 ,0 ,-100 ,100 ,0 ); //1 驗證%d 和%x myprintf ("%c ,%s\n" ,c1 ,c2 ); //2 驗證%c 和%s myprintf ("%c ,%s\n" ,'H' ,"Fine !" ); //3 帶格式使用用字符常量 myprintf ("How are you ?\n" ); //4 直接用字符串常量 myprintf (c2 ); //5 直接用字符串名字 myprintf ("%s\n" ,c2 ); //6 標準格式 myprintf ("\n" ,c2 ); //7 使用有誤,只輸出換行,不處理c2 myprintf ("How are%s" ,"you ?\n" ); //8 格式正確 return 0 ; }
這是改寫例20.18的程序,主程序一樣,所以運行結果也相同。
注意程序中有一條語句
putchar (va_arg (ap , char ));
是可以正確執行的,這是因為直接作為putchar的參數。其實,va_arg宏的第2個參數不能被指定為char、short或float類型。因為char和short類型的參數會被轉換為int類型,而float類型會被轉換成double類型。如果指定錯誤,將會引起麻煩。語句
c = va_arg (ap , char );
肯定是不對的,因為無法傳遞一個char類型參數,如果傳遞了,它會被自動轉換為int類型。應該將它寫為如下語句:
c = va_arg (ap , int );
如果cp是一個字符指針,而程序中又需要一個字符指針類型的參數,則下面的寫法是正確的。
cp = va_arg (ap , char * );
當作為參數時,指針並不會轉換,只有char、short或float類型的數值才會被轉換。
【例20.21】分析下面程序的輸出結果。
#include <stdio.h> #include <string.h> int main () { int i=0 ,len=0 ; char str="Look !" ; len=strlen (str ); for (i=0 ; i<len ;i++ ) printf ("%s\n" ,str+i ); }
【解答】「printf("%s\n",str+i);」語句不是把str作為首地址,而是str+i做地址。由自行設計myprintf函數中可以知道,str+i等效於&str[i]。它與下面程序的輸出結果一樣。
#include <stdio.h> #include <string.h> int main () { int i=0 ,len=0 ; char str="Look !" ; len=strlen (str ); for (i=0 ; i<len ;i++ ) printf ("%s\n" ,&str[i] ); }
程序每循環一次,輸出字符就從左邊減少一個字符。輸出結果如下:
Look ! ook ! ok ! k ! !