1. 程式人生 > >C、C++格式化字串

C、C++格式化字串

引言

在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語句加上了{}的話,就不會有問題,可以看出規範寫法是多麼的重要