C語言學習中的變參處理

weixin_34377065發表於2010-05-12
在C語言中,我們都知道給函式傳參,有傳址呼叫和傳值呼叫的差別。但是,很少有書籍、文章專門論述到,C語言的函式傳參,還有另外一大類應用,就是變參處理。舉個例子,我們最常用的printf函式,就是典型的變參函式,它的引數不固定,可以使用格式化字元控制輸出格式。這個大家可能都很熟悉。
變參函式用途很多,其通過設計,對外提供變參介面,允許上層業務層自由地通過格式化字串來實現對自己輸出行為的控制,這在很多debug和syslog日誌輸出場合很有用,我的書《0bug-C/C++商用工程之道》裡面,第五章開篇就在講這個設計方法。這也是幾乎所有C底層庫進行格式化輸出的最基本手段。
關於如何使用C語言變參函式,實現有效的字串格式化處理,我想大家可能很早就學會了,但是,近期幾個朋友問我問題,我才發現,很多人還是不瞭解如何設計變參函式。正好,近期我優化我的工程庫,特別重新設計的變參函式的處理方法。我這裡就share一下,供大家參考。
還是那句話哈,一家之言,歡迎拍磚。
由於前期我很多博文,我的書《0bug-C/C++商用工程之道》,都大量講過變參處理辦法,我這裡就不細講了,大家有興趣,可以看看我的SafePrintf這個函式,這在過去很多博文中都出現過了,呵呵,算是“程式碼明星”了。
Code:
  1. int SafePrintf(char* szBuf,int nMaxLength,char *szFormat, ...)   
  2. {   
  3.     int nListCount=0;   
  4.     va_list pArgList;   
  5.   
  6.     if (!szBuf) goto SafePrintf_END_PROCESS;   
  7.     va_start (pArgList,szFormat);   
  8.     nListCount+=Linux_Win_vsnprintf(szBuf+nListCount,   
  9.         nMaxLength-nListCount,szFormat,pArgList);   
  10.     va_end(pArgList);   
  11.     if(nListCount>(nMaxLength-1)) nListCount=nMaxLength-1;   
  12.     *(szBuf+nListCount)='\0';   
  13.   
  14. SafePrintf_END_PROCESS:   
  15.     return nListCount;   
  16. }  
不過,這裡面有個潛在的問題,我一直沒有解決好,就是說,雖然我提供了一個SafePrintf函式來處理變參,但如果另外一個函式,也提供變參介面,這時候,很不好把自己的變參引數傳遞給SafePrintf來處理。如下例:
Code:
  1. void Func(char* szFormat,...)   
  2. {   
  3.     char szBuf[256];   
  4.     SafePrintf(szBuf,256,...);    //???   
  5. }   
這樣直接傳遞...是肯定錯誤的,根據ANSI C99的定義,此時要傳遞變參,必須使用void va_copy(va_list dst, va_list src); 這個巨集來處理,以va_list這種隱式資料結構的顯式拷貝動作,來把Func這個函式的變參,傳遞給SafePrintf。並且,由va_copy初始化的va_list在使用結束時必須使用va_end來“釋放”。
這顯然太麻煩了,我以前就一直很抵制這種又是顯式,又是隱式,變來變去的介面方式。所以,我在《0bug-C/C++商用工程之道》這本書的庫程式碼中,一直是把中間處理變參這段程式碼拷來拷去使用,哪個函式處理變參,就在哪個函式一開始的地方,來上這麼一段,把變參先處理成定參,再向下傳遞。
不過,這也有問題,我的習慣,同樣邏輯的程式碼只寫一次,以後都是呼叫,避免無謂的筆誤和程式碼冗餘。這顯然不符合我的習慣,所以,我也一直在想怎麼優化這一塊。
近期我想了一下,決定採用函式型巨集來處理這個問題,這雖然像inline一樣,並不能真實地減少程式碼,但是,它使程式變得很簡潔,程式設計師看起來清清爽爽,同時,由於函式型巨集可以固化操作,不會再出現筆誤問題,算是個比較好的折中方案。嗯,抵制使用巨集的C++er們注意了哈,這是一個inline無法替代巨集的例項了。呵呵。
當然,在討論字串處理的前面,首先要給大家一些include的標頭檔案,以及一些基本的定義,我呢,懶得一一分辨了,就把《0bug-C/C++商用工程之道》的總跨平臺include表列出來,大家直接用哈。當然,由於這些定義,下面的程式碼必然是跨平臺的。
Code:
  1. #include <stdio.h>   
  2. #include <stdlib.h>   
  3. #include <stdarg.h>   
  4. #include <time.h>   
  5. #include <fcntl.h>   
  6. #include <signal.h>   
  7.   
  8. #ifdef WIN32   
  9.     #include <conio.h>   
  10.     #include <windows.h>   
  11.     #include <process.h>   
  12.     #include <winsock.h>   
  13. #else // not WIN32    
  14.     #include <unistd.h>   
  15.     #include <errno.h>   
  16.     #include <pthread.h>   
  17.     #include <fcntl.h>   
  18.     #include <unistd.h>    
  19.     #include <netinet/in.h>   
  20.     #include <string.h>   
  21.     #include <sys/time.h>   
  22.     #include <arpa/inet.h>   
  23.     #include <errno.h>   
  24.     #include <termios.h>   
  25.     #include <netdb.h>   
  26.     #include <getopt.h>   
  27.     #include <netinet/in.h>   
  28.     #include <arpa/inet.h>   
  29.     #include <unistd.h>   
  30. #endif   
  31. ////////////////////////////////////////////////////////////////////   
  32. #ifdef WIN32   
  33.     #pragma warning (disable : 4800)    
  34.     #pragma warning (disable : 4996)    
  35.     #pragma warning (disable : 4200)    
  36.     #pragma warning (disable : 4244)    
  37.     #pragma warning (disable : 4010)    
  38.     #define Linux_Win_vsnprintf _vsnprintf   
  39. #else // not WIN32    
  40.     #define Linux_Win_vsnprintf vsnprintf   
  41. #endif   
  42. #ifndef null    
  43.     #define null 0   
  44. #endif  
開始做事,我首先作了如下函式型巨集程式碼:
Code:
  1. #define TONY_FORMAT(nPrintLength,szBuf,nBufferSize,szFormat) \   
  2. { \   
  3.     va_list pArgList; \   
  4.     va_start (pArgList,szFormat); \   
  5.     nPrintLength+=Linux_Win_vsnprintf(szBuf+nPrintLength, \   
  6.         nBufferSize-nPrintLength,szFormat,pArgList); \   
  7.     va_end(pArgList); \   
  8.     if(nPrintLength>(nBufferSize-1)) nPrintLength=nBufferSize-1; \   
  9.     *(szBuf+nPrintLength)='\0'; \   
  10. }  
這個巨集有4個引數,我解釋一下:
nPrintLength:這個很重要,C的規約,處理變參的函式一般要返回一個int,表示變參展開後,真實的位元組數,注意,這裡沒有包括字串這個'\0'的位寬,即僅僅是strlen的長度。很多時候,C語言程式設計師習慣於要採納這個值參與後續計算,嗯,我們後面就有這個例子,所以,外部傳進來一個變數nPrintLength,就是求這個值。
這也看出來,函式型巨集,全部相當於傳址呼叫,可以直接修改外部的變數的值的。
szBuf,nBufferSize:就是希望把變參展開,填充到的緩衝區和緩衝區長度,我強調0bug程式設計,很多時候,外部傳入一個緩衝區要求函式填充的時候,都必須給一個邊界,避免記憶體寫出界導致崩潰,這個nBufferSize就是幹這個的,內部的設計會保證不超過這個邊界。
szFormat:精華了哈,前面說那麼麻煩的va_list傳遞變參模式,在此簡化為直接把szFormat傳進來就好了。我認為這是這個設計最漂亮的一點,大大簡化了呼叫者的程式行為,再也不麻煩了,呵呵。
ok,有了這個巨集,我們來改寫一下前面經典的SafePrintf看看:
Code:
  1. //安全的變參列印函式   
  2. inline int SafePrintf(char* szBuf,int nBufSize,char* szFormat, ...)   
  3. {   
  4.     if(!szBuf) return 0;   
  5.     if(!nBufSize) return 0;   
  6.     if(!szFormat) return 0;   
  7.     int nRet=0;   
  8.     TONY_FORMAT(nRet,szBuf,nBufSize,szFormat);   
  9.     return nRet;   
  10. }  
大家注意到什麼沒有?SafePrintf裡面複雜的邏輯不見了,全部被整合成為TONY_FORMAT這個函式巨集的呼叫。
嗯,考慮到很多時候,我們做Debug或者日誌輸出,需要列印的時候自動加上一個時間戳,因此,我又做了變參處理巨集的時間戳版本: 
Code:
  1. #define TONY_FORMAT_WITH_TIMESTAMP(nPrintLength,szBuf,nBufferSize,szFormat) \   
  2. { \   
  3.     time_t t; \   
  4.     struct tm *pTM=NULL; \   
  5.     time(&t); \   
  6.     pTM = localtime(&t); \   
  7.     nPrintLength+=SafePrintf(szBuf,nBufferSize,"[%s",asctime(pTM)); \   
  8.     szBuf[nPrintLength-1]='\0'; \   
  9.     nPrintLength--; \   
  10.     nPrintLength+=SafePrintf(szBuf+nPrintLength,nBufferSize-nPrintLength,"] "); \   
  11.     TONY_FORMAT(nPrintLength,szBuf,nBufferSize,szFormat); \   
  12. }  
大家注意沒,這裡面,TONY_FORMAT_WITH_TIMESTAMP馬上就在呼叫前面的SafePrintf,以及TONY_FORMAT。這是我做程式的習慣,每個模組寫出來就是要給人用的,自己往往就是第一個使用者,函式介面,api設計得好不好,自己一用就知道,不好用就調整,調整到自己爽為止。把自己站在使用者的立場上,把程式調整到自己用起來都“爽”,你的程式就能獲得使用者的好評。
我一直說,“程式設計師的使用者,不僅僅是終端使用者,還包括和你自己一樣的,甚至就是你自己,程式設計師。”就是這個意思,大家能理解嗎?
這裡面有個細節大家注意一下,asctime這個系統函式很討厭,它格式化的字串,最後自動帶著一個回車,這會打亂我的輸出順序,所以我用了 szBuf[nPrintLength-1]='\0'; 這句話來回退,消滅這個多餘的回車。
當然,有了這個時間戳巨集,我們也可以很輕鬆寫出SafePrintf的時間戳版本:
Code:
  1. inline int SafePrintfWithTimestamp(char* szBuf,int nBufSize,char* szFormat, ...)   
  2. {   
  3.     if(!szBuf) return 0;   
  4.     if(!nBufSize) return 0;   
  5.     if(!szFormat) return 0;   
  6.     int nRet=0;   
  7.     TONY_FORMAT_WITH_TIMESTAMP(nRet,szBuf,nBufSize,szFormat);   
  8.     return nRet;   
  9. }  
還是要給個測試嘛:
Code:
  1. inline void Test_TONY_FORMAT(void)   
  2. {   
  3.     char szBuf[256];   
  4.     int nLength=0;   
  5.     nLength=SafePrintf(szBuf,256,"Test: %d",100);   
  6.     printf("[%d] %s\n",nLength,szBuf);   
  7.     nLength=SafePrintfWithTimestamp(szBuf,256,"Test: %d",100);   
  8.     printf("[%d] %s\n",nLength,szBuf);   
  9. }   
  10. 結果:   
  11. [9] Test: 100   
  12. [36] [Wed May 12 10:10:32 2010] Test: 100  
不過,為了仔細甄別,我還是單獨寫了兩個變參處理函式來驗證這個變參傳遞情況,第一個模擬printf,第二個模擬fprintf,大家可以看看程式碼。
這是printf版本:
Code:
  1. #define TONY_LINE_MAX 1024      //最大一行輸出的字元數   
  2. //輸出到控制檯   
  3. inline int TonyPrintf(bool bWithTimestamp,      //是否帶時間戳標誌   
  4.                       char* szFormat, ...)      //格式化字串   
  5. {   
  6.     if(!szFormat) return 0;   
  7.     char szBuf[TONY_LINE_MAX];   
  8.     int nLength=0;   
  9.     if(!bWithTimestamp)            
  10.     {   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  11.         TONY_FORMAT(nLength,szBuf,TONY_LINE_MAX,szFormat);   
  12.     }   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  13.     else  
  14.     {   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  15.         TONY_FORMAT_WITH_TIMESTAMP(nLength,szBuf,TONY_LINE_MAX,szFormat);   
  16.     }   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  17.     return printf(szBuf);   
  18. }   
  19. inline void TestTonyPrintf(void)   
  20. {   
  21.     int i=0;   
  22.     double dTest=123.456;   
  23.     unsigned int unTest=0xAABBCC;   
  24.     for(i='A';i<='E';i++)   
  25.     {   
  26.         TonyPrintf(0,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);   
  27.     }   
  28.     for(i='A';i<='E';i++)   
  29.     {   
  30.         TonyPrintf(1,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);   
  31.     }   
  32. }   
  33. 執行結果:   
  34. [65]: 123.46, A, 0x00AABBCC   
  35. [66]: 123.46, B, 0x00AABBCC   
  36. [67]: 123.46, C, 0x00AABBCC   
  37. [68]: 123.46, D, 0x00AABBCC   
  38. [69]: 123.46, E, 0x00AABBCC   
  39. [Wed May 12 09:17:43 2010] [65]: 123.46, A, 0x00AABBCC   
  40. [Wed May 12 09:17:43 2010] [66]: 123.46, B, 0x00AABBCC   
  41. [Wed May 12 09:17:43 2010] [67]: 123.46, C, 0x00AABBCC   
  42. [Wed May 12 09:17:43 2010] [68]: 123.46, D, 0x00AABBCC   
  43. [Wed May 12 09:17:43 2010] [69]: 123.46, E, 0x00AABBCC   
fprintf版本比較麻煩一點,需要先建立一根檔案指標。
Code:
  1. //輸出到檔案   
  2. inline int TonyFPrintf(FILE* fp,                //檔案指標   
  3.                        bool bWithTimestamp,     //是否帶時間戳標誌   
  4.                       char* szFormat, ...)      //格式化字串   
  5. {   
  6.     if(!fp) return 0;   
  7.     if(!szFormat) return 0;   
  8.     char szBuf[TONY_LINE_MAX];   
  9.     int nLength=0;   
  10.     if(!bWithTimestamp)   
  11.     {   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  12.         TONY_FORMAT(nLength,szBuf,TONY_LINE_MAX,szFormat);   
  13.     }   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  14.     else  
  15.     {   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  16.         TONY_FORMAT_WITH_TIMESTAMP(nLength,szBuf,TONY_LINE_MAX,szFormat);   
  17.     }   //注意,由於內部是函式型巨集,if...else這個大括號必不可少   
  18.     return fprintf(fp,szBuf);   
  19. }   
  20. inline void TestTonyFPrintf(void)   
  21. {   
  22.     FILE* fp=null;   
  23.     int i=0;   
  24.     double dTest=123.456;   
  25.     unsigned int unTest=0xAABBCC;   
  26.     fp=fopen("test.txt","at");   
  27.     if(fp)   
  28.     {   
  29.         for(i='A';i<='E';i++)   
  30.         {   
  31.             TonyFPrintf(fp,0,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);   
  32.         }   
  33.         for(i='A';i<='E';i++)   
  34.         {   
  35.             TonyFPrintf(fp,1,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);   
  36.         }   
  37.         fclose(fp);   
  38.     }   
  39. }  
這個函式執行完後,螢幕上沒有,不過,磁碟上會出現一個檔案,叫做test.txt,裡面的內容和前面的一樣。
經過這些測試,我認為這次改版基本上成功了,使用這幾個變參處理巨集,我可以大幅度縮減很多變參函式的書寫長度,程式顯得很清爽,且功能比較完備。
我的計劃是,這些程式碼目前先自己用,等用個一年半載,穩定性差不多了,在《0bug-C/C++商用工程之道》的第二版中,我會應用到新的工程庫中去,供各位讀者使用哈。
上述程式碼在VS2008下測試通過,不過,我的理解是跨平臺的,由於全部是C的函式,處理的都是函式內部私有變數,因此,也是執行緒安全的。
大家看看,有問題再問哈。
肖舸

相關文章