在C語言中,我們都知道給函數傳參,有傳址調用和傳值調用的差別。但是,很少有書籍、文章專門論述到,C語言的函數傳參,還有另外一大類應用,就是變參處理。舉個例子,我們最常用的printf函數,就是典型的變參函數,它的參數不固定,可以使用格式化字符控制輸出格式。這個大家可能都很熟悉。
變參函數用途很多,其通過設計,對外提供變參接口,允許上層業務層自由地通過格式化字符串來實現對自己輸出行為的控制,這在很多debug和syslog日志輸出場合很有用,我的書《0bug-C/C++商用工程之道》裡面,第五章開篇就在講這個設計方法。這也是幾乎所有C底層庫進行格式化輸出的最基本手段。
關於如何使用C語言變參函數,實現有效的字符串格式化處理,我想大家可能很早就學會了,但是,近期幾個朋友問我問題,我才發現,很多人還是不了解如何設計變參函數。正好,近期我優化我的工程庫,特別重新設計的變參函數的處理方法。我這裡就share一下,供大家參考。
還是那句話哈,一家之言,歡迎拍磚。
由於前期我很多博文,我的書《0bug-C/C++商用工程之道》,都大量講過變參處理辦法,我這裡就不細講了,大家有興趣,可以看看我的SafePrintf這個函數,這在過去很多博文中都出現過了,呵呵,算是“代碼明星”了。
Code:
- int SafePrintf(char* szBuf,int nMaxLength,char *szFormat, ...)
- {
- int nListCount=0;
- va_list pArgList;
-
- if (!szBuf) goto SafePrintf_END_PROCESS;
- va_start (pArgList,szFormat);
- nListCount+=Linux_Win_vsnprintf(szBuf+nListCount,
- nMaxLength-nListCount,szFormat,pArgList);
- va_end(pArgList);
- if(nListCount>(nMaxLength-1)) nListCount=nMaxLength-1;
- *(szBuf+nListCount)='\0';
-
- SafePrintf_END_PROCESS:
- return nListCount;
- }
不過,這裡面有個潛在的問題,我一直沒有解決好,就是說,雖然我提供了一個SafePrintf函數來處理變參,但如果另外一個函數,也提供變參界面,這時候,很不好把自己的變參參數傳遞給SafePrintf來處理。如下例:
Code:
- void Func(char* szFormat,...)
- {
- char szBuf[256];
- SafePrintf(szBuf,256,...); //???
- }
這樣直接傳遞...是肯定錯誤的,根據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:
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdarg.h>
- #include <time.h>
- #include <fcntl.h>
- #include <signal.h>
-
- #ifdef WIN32
- #include <conio.h>
- #include <windows.h>
- #include <process.h>
- #include <winsock.h>
- #else // not WIN32
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <netinet/in.h>
- #include <string.h>
- #include <sys/time.h>
- #include <arpa/inet.h>
- #include <errno.h>
- #include <termios.h>
- #include <netdb.h>
- #include <getopt.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #endif
- ////////////////////////////////////////////////////////////////////
- #ifdef WIN32
- #pragma warning (disable : 4800)
- #pragma warning (disable : 4996)
- #pragma warning (disable : 4200)
- #pragma warning (disable : 4244)
- #pragma warning (disable : 4010)
- #define Linux_Win_vsnprintf _vsnprintf
- #else // not WIN32
- #define Linux_Win_vsnprintf vsnprintf
- #endif
- #ifndef null
- #define null 0
- #endif
開始做事,我首先作了如下函數型宏代碼:
Code:
- #define TONY_FORMAT(nPrintLength,szBuf,nBufferSize,szFormat) \
- { \
- va_list pArgList; \
- va_start (pArgList,szFormat); \
- nPrintLength+=Linux_Win_vsnprintf(szBuf+nPrintLength, \
- nBufferSize-nPrintLength,szFormat,pArgList); \
- va_end(pArgList); \
- if(nPrintLength>(nBufferSize-1)) nPrintLength=nBufferSize-1; \
- *(szBuf+nPrintLength)='\0'; \
- }
這個宏有4個參數,我解釋一下:
nPrintLength:這個很重要,C的規約,處理變參的函數一般要返回一個int,表示變參展開後,真實的字節數,注意,這裡沒有包括字符串這個'\0'的位寬,即僅僅是strlen的長度。很多時候,C語言程序員習慣於要采納這個值參與後續計算,嗯,我們後面就有這個例子,所以,外部傳進來一個變量nPrintLength,就是求這個值。
這也看出來,函數型宏,全部相當於傳址調用,可以直接修改外部的變量的值的。
szBuf,nBufferSize:就是希望把變參展開,填充到的緩沖區和緩沖區長度,我強調0bug編程,很多時候,外部傳入一個緩沖區要求函數填充的時候,都必須給一個邊界,避免內存寫出界導致崩潰,這個nBufferSize就是干這個的,內部的設計會保證不超過這個邊界。
szFormat:精華了哈,前面說那麼麻煩的va_list傳遞變參模式,在此簡化為直接把szFormat傳進來就好了。我認為這是這個設計最漂亮的一點,大大簡化了調用者的程序行為,再也不麻煩了,呵呵。
ok,有了這個宏,我們來改寫一下前面經典的SafePrintf看看:
Code:
- //安全的變參打印函數
- inline int SafePrintf(char* szBuf,int nBufSize,char* szFormat, ...)
- {
- if(!szBuf) return 0;
- if(!nBufSize) return 0;
- if(!szFormat) return 0;
- int nRet=0;
- TONY_FORMAT(nRet,szBuf,nBufSize,szFormat);
- return nRet;
- }
大家注意到什麼沒有?SafePrintf裡面復雜的邏輯不見了,全部被整合成為TONY_FORMAT這個函數宏的調用。
嗯,考慮到很多時候,我們做Debug或者日志輸出,需要打印的時候自動加上一個時間戳,因此,我又做了變參處理宏的時間戳版本:
Code:
- #define TONY_FORMAT_WITH_TIMESTAMP(nPrintLength,szBuf,nBufferSize,szFormat) \
- { \
- time_t t; \
- struct tm *pTM=NULL; \
- time(&t); \
- pTM = localtime(&t); \
- nPrintLength+=SafePrintf(szBuf,nBufferSize,"[%s",asctime(pTM)); \
- szBuf[nPrintLength-1]='\0'; \
- nPrintLength--; \
- nPrintLength+=SafePrintf(szBuf+nPrintLength,nBufferSize-nPrintLength,"] "); \
- TONY_FORMAT(nPrintLength,szBuf,nBufferSize,szFormat); \
- }
大家注意沒,這裡面,TONY_FORMAT_WITH_TIMESTAMP馬上就在調用前面的SafePrintf,以及TONY_FORMAT。這是我做程序的習慣,每個模塊寫出來就是要給人用的,自己往往就是第一個用戶,函數接口,api設計得好不好,自己一用就知道,不好用就調整,調整到自己爽為止。
把自己站在用戶的立場上,把程序調整到自己用起來都“爽”,你的程序就能獲得用戶的好評。
我一直說,“
程序員的用戶,不僅僅是終端用戶,還包括和你自己一樣的,甚至就是你自己,程序員。”就是這個意思,大家能理解嗎?
這裡面有個細節大家注意一下,asctime這個系統函數很討厭,它格式化的字符串,最後自動帶著一個回車,這會打亂我的輸出順序,所以我用了 szBuf[nPrintLength-1]='\0'; 這句話來回退,消滅這個多余的回車。
當然,有了這個時間戳宏,我們也可以很輕松寫出SafePrintf的時間戳版本:
Code:
- inline int SafePrintfWithTimestamp(char* szBuf,int nBufSize,char* szFormat, ...)
- {
- if(!szBuf) return 0;
- if(!nBufSize) return 0;
- if(!szFormat) return 0;
- int nRet=0;
- TONY_FORMAT_WITH_TIMESTAMP(nRet,szBuf,nBufSize,szFormat);
- return nRet;
- }
還是要給個測試嘛:
Code:
- inline void Test_TONY_FORMAT(void)
- {
- char szBuf[256];
- int nLength=0;
- nLength=SafePrintf(szBuf,256,"Test: %d",100);
- printf("[%d] %s\n",nLength,szBuf);
- nLength=SafePrintfWithTimestamp(szBuf,256,"Test: %d",100);
- printf("[%d] %s\n",nLength,szBuf);
- }
- 結果:
- [9] Test: 100
- [36] [Wed May 12 10:10:32 2010] Test: 100
不過,為了仔細甄別,我還是單獨寫了兩個變參處理函數來驗證這個變參傳遞情況,第一個模擬printf,第二個模擬fprintf,大家可以看看代碼。
這是printf版本:
Code:
- #define TONY_LINE_MAX 1024 //最大一行輸出的字符數
- //輸出到控制台
- inline int TonyPrintf(bool bWithTimestamp, //是否帶時間戳標志
- char* szFormat, ...) //格式化字符串
- {
- if(!szFormat) return 0;
- char szBuf[TONY_LINE_MAX];
- int nLength=0;
- if(!bWithTimestamp)
- { //注意,由於內部是函數型宏,if...else這個大括號必不可少
- TONY_FORMAT(nLength,szBuf,TONY_LINE_MAX,szFormat);
- } //注意,由於內部是函數型宏,if...else這個大括號必不可少
- else
- { //注意,由於內部是函數型宏,if...else這個大括號必不可少
- TONY_FORMAT_WITH_TIMESTAMP(nLength,szBuf,TONY_LINE_MAX,szFormat);
- } //注意,由於內部是函數型宏,if...else這個大括號必不可少
- return printf(szBuf);
- }
- inline void TestTonyPrintf(void)
- {
- int i=0;
- double dTest=123.456;
- unsigned int unTest=0xAABBCC;
- for(i='A';i<='E';i++)
- {
- TonyPrintf(0,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);
- }
- for(i='A';i<='E';i++)
- {
- TonyPrintf(1,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);
- }
- }
- 運行結果:
- [65]: 123.46, A, 0x00AABBCC
- [66]: 123.46, B, 0x00AABBCC
- [67]: 123.46, C, 0x00AABBCC
- [68]: 123.46, D, 0x00AABBCC
- [69]: 123.46, E, 0x00AABBCC
- [Wed May 12 09:17:43 2010] [65]: 123.46, A, 0x00AABBCC
- [Wed May 12 09:17:43 2010] [66]: 123.46, B, 0x00AABBCC
- [Wed May 12 09:17:43 2010] [67]: 123.46, C, 0x00AABBCC
- [Wed May 12 09:17:43 2010] [68]: 123.46, D, 0x00AABBCC
- [Wed May 12 09:17:43 2010] [69]: 123.46, E, 0x00AABBCC
fprintf版本比較麻煩一點,需要先創建一根文件指針。
Code:
- //輸出到文件
- inline int TonyFPrintf(FILE* fp, //文件指針
- bool bWithTimestamp, //是否帶時間戳標志
- char* szFormat, ...) //格式化字符串
- {
- if(!fp) return 0;
- if(!szFormat) return 0;
- char szBuf[TONY_LINE_MAX];
- int nLength=0;
- if(!bWithTimestamp)
- { //注意,由於內部是函數型宏,if...else這個大括號必不可少
- TONY_FORMAT(nLength,szBuf,TONY_LINE_MAX,szFormat);
- } //注意,由於內部是函數型宏,if...else這個大括號必不可少
- else
- { //注意,由於內部是函數型宏,if...else這個大括號必不可少
- TONY_FORMAT_WITH_TIMESTAMP(nLength,szBuf,TONY_LINE_MAX,szFormat);
- } //注意,由於內部是函數型宏,if...else這個大括號必不可少
- return fprintf(fp,szBuf);
- }
- inline void TestTonyFPrintf(void)
- {
- FILE* fp=null;
- int i=0;
- double dTest=123.456;
- unsigned int unTest=0xAABBCC;
- fp=fopen("test.txt","at");
- if(fp)
- {
- for(i='A';i<='E';i++)
- {
- TonyFPrintf(fp,0,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);
- }
- for(i='A';i<='E';i++)
- {
- TonyFPrintf(fp,1,"[%d]: %0.2f, %c, 0x%08X\n",i,dTest,i,unTest);
- }
- fclose(fp);
- }
- }
這個函數運行完後,屏幕上沒有,不過,磁盤上會出現一個文件,叫做test.txt,裡面的內容和前面的一樣。
經過這些測試,我認為這次改版基本上成功了,使用這幾個變參處理宏,我可以大幅度縮減很多變參函數的書寫長度,程序顯得很清爽,且功能比較完備。
我的計劃是,這些代碼目前先自己用,等用個一年半載,穩定性差不多了,在《0bug-C/C++商用工程之道》的第二版中,我會應用到新的工程庫中去,供各位讀者使用哈。
上述代碼在VS2008下測試通過,不過,我的理解是跨平台的,由於全部是C的函數,處理的都是函數內部私有變量,因此,也是線程安全的。
大家看看,有問題再問哈。
=======================================================
在線底價購買《0bug-C/C++商用工程之道》
直接點擊下面鏈接或拷貝到浏覽器地址欄)
http://s.click.taobao.com/t_3?&p=mm_13866629_0_0&n=23&l=http%3A%2F%2Fsearch8.taobao.com%2Fbrowse%2F0%2Fn-g%2Corvv64tborsvwmjvgawdkmbqgboq---g%2Cgaqge5lhebbs6qzlfmqmttgtyo42jm6m22xllqa-------------1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20---40--coefp-0-all-0.htm%3Fpid%3Dmm_13866629_0_0
肖舸
本文出自 “肖舸的blog” 博客,請務必保留此出處http://tonyxiaohome.blog.51cto.com/925273/314371