C、C++格式化字元串
- 2019 年 10 月 3 日
- 筆記
引言
在C和C++開發中,我們經常會用到printf來進行字元串的格式化,例如printf("format string %d, %d", 1, 2);
,這樣的格式化只是用於列印調試資訊。printf函數實現的是接收可變參數,然後解析格式化的字元串,最後輸出到控制台。那麼問題來了,當我們需要實現一個函數,根據傳入的可變參數來生成格式化的字元串,應該怎麼辦呢?
你可以在這裡看到更好的排版
正文
可變參數
首先來一個可變參數使用示例,testVariadic
方法接收int行的可變參數,並以可變參數為-1表示結束。va_list用於遍歷可變參數,va_start
方法接收兩個參數,第一個為va_list
,第二個為可變參數前一個參數,下面的例子里該參數為a。
/** 下面是 <stdarg.h> 裡面重要的幾個宏定義如下: typedef char* va_list; void va_start ( va_list ap, prev_param ); // ANSI version type va_arg ( va_list ap, type ); void va_end ( va_list ap ); va_list 是一個字元指針,可以理解為指向當前參數的一個指針,取參必須通過這個指針進行。 <Step 1> 在調用參數表之前,定義一個 va_list 類型的變數,(假設va_list 類型變數被定義為ap); <Step 2> 然後應該對ap 進行初始化,讓它指向可變參數表裡面的第一個參數,這是通過 va_start 來實現的,第一個參數是 ap 本身,第二個參數是在變參表前面緊挨著的一個變數,即「...」之前的那個參數; <Step 3> 然後是獲取參數,調用va_arg,它的第一個參數是ap,第二個參數是要獲取的參數的指定類型,然後返回這個指定類型的值,並且把 ap 的位置指向變參表的下一個變數位置; <Step 4> 獲取所有的參數之後,我們有必要將這個 ap 指針關掉,以免發生危險,方法是調用 va_end,他是輸入的參數 ap 置為 NULL,應該養成獲取完參數表之後關閉指針的習慣。說白了,就是讓我們的程式具有健壯性。通常va_start和va_end是成對出現。 */ //-1表示可變參數結束 void receiveVariadic(int a, ...) { va_list list; va_start(list, a); int arg = a; while (arg != -1) { arg = va_arg(list, int); printf("%d ", arg); } printf("n"); va_end(list); } //test void testVari() { printf("------%s------n", __FUNCTION__); //-1表示可變參數結束 receiveVariadic(1, 2, 3, 4, 5, 6, -1); }
運行結果
------testVari------ 2 3 4 5 6 -1
格式化字元串
好了,我們已經介紹了怎樣實現一個接收可變參數的C函數,接下來介紹根據接收的可變參數來格式化字元串。這裡介紹兩種方式,第一種是利用宏定義,第二種通過函數的方式來實現。
通過宏定義的方式
en…讓咱們先來看看第一個版本的宏,這個宏定義對於不熟悉宏的人來說可能看著有點費勁,不過不要怕,稍後會做解釋,程式碼如下:
#define myFormatStringByMacro_WithoutReturn(format, ...) do { int size = snprintf(NULL, 0, format, ##__VA_ARGS__); size++; char *buf = (char *)malloc(size); snprintf(buf, size, format, ##__VA_ARGS__); printf("%s", buf); free(buf); } while(0)
宏基礎知識
首先需要介紹宏用到的知識:, 這個
的作用是可換行定義宏,畢竟如果一行很長的宏可讀性很差,使用方式在換行時加上
即可。第二個是介紹
(format, ...)
,這裡的...
是預定義的宏,用於接收可變參數,就像是printf
函數一樣。接著介紹##__VA_ARGS__
,同樣的__VA_ARGS__
也是預定義的宏,表示接收到的...
傳入的可變參數。##
的作用是用來處理未傳入可變參數的情況,當沒有傳入可變參數的時候,編譯器或通過優化將snprintf(NULL, 0, format, ##__VA_ARGS__);
優化為snprintf(NULL, 0, format);
。你可以理解為沒有可變參數時,##
前的逗號,
與__VA_ARGS__
都被「幹掉了」。
你一定會覺得困惑,為什麼要寫do-while
語句呢?這是為了宏的健壯性,如果使用宏的人像下面這樣使用的話,就會出問題
#define testMarco(a, b) int _a = a + 1; int _b = b + 1; printf("n%d", _a + _b); void test() { if (1 > 0) testMarco(1, 2); }
上面的程式碼連編譯都不會通過, 會報錯如下:
如果手動展開這個宏的話,會變成這個樣子,問題就顯而易見了。但是如果if
語句加上了{}
的話,就不會有問題,可以看出規範寫法是多麼的重要?(皮一下很開心)。
void test() { if (1 > 0) int _a = 1 + 1; int _b = 2 + 1; printf("n%d", _a + _b);; }
加上do-while
以後就不一樣,加上do-while後的程式碼如下:
#define testMarco(a, b) do { int _a = a + 1; int _b = b + 1; printf("n%d", _a + _b); } while(0) void test() { if (1 > 0) testMarco(1, 2); }
預處理之後程式碼如下:
//展開後的程式碼 void test() { if (1 > 0) do { int _a = 1 + 1; int _b = 2 + 1; printf("n%d", _a + _b); } while(0); }
好了,宏的基礎知識就介紹這麼多了,接下來進入正題。
程式碼解析
為了方便閱讀,原諒我在這裡再貼一遍宏定義的程式碼:
#define myFormatStringByMacro_WithoutReturn(format, ...) do { int size = snprintf(NULL, 0, format, ##__VA_ARGS__); size++; char *buf = (char *)malloc(size); snprintf(buf, size, format, ##__VA_ARGS__); printf("%s", buf); free(buf); } while(0)
首先,介紹一下snprintf()
函數,此函數的定義如下:
/** @param __str 接收格式化結果的指針 @param __size 接收的size @param __format 格式化的字元串 @param ... 可變參數 @return 返回格式化後實際上寫入的大小a,a <= __size */ int snprintf(char * __restrict __str, size_t __size, const char * __restrict __format, ...) __printflike(3, 4);
為了方便理解,使用方式是這個樣子的:
void testSnprintf() { printf("------%s------n", __FUNCTION__); char des[50]; int size = snprintf(des, 50, "less length %d", 50); printf("size:%dn", size); }
運行結果:
------testSnprintf------ size:14
snprintf
函數還有一個用法是__str
和__size
分別傳入NULL和0,返回值會是格式化字元串的實際長度,可以通過這個方式來獲取正確的格式化size,從而避免malloc多餘的空間,造成空間浪費。同時返回的size是不包含結束符