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語句加上了{}的話,就不會有問題,可以看出規范寫法是多么的重要🐶(皮一下很開心)。

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:%d\n", size);
}

運行結果:

------testSnprintf------
size:14

snprintf函數還有一個用法是__str__size分別傳入NULL和0,返回值會是格式化字符串的實際長度,可以通過這個方式來獲取正確的格式化size,從而避免malloc多余的空間,造成空間浪費。同時返回的size是不包含結束符\0的,所以真正寫入要buffer時,需要對size + 1。

相信通過我的解釋,你一定能看懂上面這段代碼了吧。哦,對了malloc的代碼一定要記得free(敲重點)。

到了這里,如果細心思考的同學一定會問?這個宏根本沒有實際用途好不好,我要的是能夠把格式化的字符串作為返回值返回的,僅僅打印直接用printf不就好了。其實,這樣的宏還是有作用的,比如說當你要記錄日志時,你可以像這樣使用:

#define Log_Debug(format, ...) \
do { \
int size = snprintf(NULL, 0, format, ##__VA_ARGS__);\
size++; \
char *buf = (char *)malloc(size); \
snprintf(buf, size, format, ##__VA_ARGS__); \
doLog(buf); \
free(buf); \
} while(0)

要將結果字符串返回的話,需要用到GNU C的賦值擴展,使用方式如下:

int a = ({
        int b = 2;
        int c = 4;
        b + c;
    });

這段代碼變量a最終值會是6。利用gnu這個擴展,將之前的宏改造一下就能實現我們的需求,改造完成后是這個樣子的:

#define myFormatStringByMacro_ReturnFormatString(format, ...) \
({ \
    int size = snprintf(NULL, 0, format, ##__VA_ARGS__);\
    size++; \
    char *buf = (char *)malloc(size); \
    snprintf(buf, size, format, ##__VA_ARGS__); \
    buf; \
});

調用宏的代碼:

void testByMacro1()
{
    printf("------%s------\n", __FUNCTION__);
    char *a = myFormatStringByMacro_ReturnFormatString("format by macro, %d %s", 123, "well done");
    printf("%s\n", a);
    free(a);
}

原諒我的啰嗦,malloc開辟的空間一定要記得free。運行結果:

------testByMacro1------
format by macro, 123 well done

至此利用宏的方式就介紹完了。

通過函數的方式

老規矩先上代碼

char *myFormatStringByFun(char *format, ...)
{
    va_list list;
    //1. 先獲取格式化后字符串的長度
    va_start(list, format);
    int size = vsnprintf(NULL, 0, format, list);
    va_end(list);
    if(size <= 0) {
        return NULL;
    }
    size++;
    
    //2. 復位va_list,將格式化字符串寫入到buf
    va_start(list, format);
    char *buf = (char *)malloc(size);
    vsnprintf(buf, size, format, list);
    va_end(list);
    return buf;
}

這里利用的是vsnprintf函數,此函數的定義在stdio.h中的定義如下:

/**

 @param __str 目標字符串
 @param __size 要賦值的大小
 @param __format 格式化字符串
 @param va_list 可變參數列表
 @return 返回格式化后實際上寫入的大小a,a <= __size
 */
int     vsnprintf(char * __restrict __str, size_t __size, const char * __restrict __format, va_list) __printflike(3, 0);

vsnprintf的具體使用方式和之前介紹的snprintf是差不多的,這里就不再詳細介紹了,不大明白的同學可以看看上面的介紹。哦,對了,這兩個函數都是定義在stdio.h這個頭文件下的

接下來就是試一下我們封裝的函數了

void testByFun()
{
    printf("------%s------\n", __FUNCTION__);
    char *b = myFormatStringByFun("format by fun %d %s", 321, "nice");
    printf("%s\n", b);
}

運行結果:

------testByFun------
format by fun 321 nice

格式化字符串的方法差不多介紹完了,不知道善於思考的你有沒想到直接用宏定義來調用我們封裝的函數呢?我就在這直接給出宏定義和使用方式了

#define myFormatStringByFunQuick(format, ...) myFormatStringByFun(format, ##__VA_ARGS__);
void testMyFormatStringByFunQuick() {
    printf("------%s------\n", __FUNCTION__);
    char *formatString = myFormatStringByFunQuick("amazing happen, %s", "cool");
    printf("%s\n", formatString);
}

運行結果:

------testMyFormatStringByFunQuick------
amazing happen

C++版本

對了,最初實現是用的C++版本,這里使用的是泛型,代碼是這個樣子的:

template< typename... Args >
std::string string_sprintf( const char* format, Args... args ) {
    int length = std::snprintf( nullptr, 0, format, args... );
    assert( length >= 0 );
    
    char* buf = new char[length + 1];
    std::snprintf( buf, length + 1, format, args... );
    
    std::string str( buf );
    delete[] buf;
    return str;
}

其實和C語言版本的沒什么差別,只是多了泛型的東西而已,相信聰明的你一定能看懂,看不懂的話,就去看看C++的泛型知識吧,哈哈哈。

結語

終於介紹完了,你可以在這里下載代碼。寫博客是真的有點累人,不過對於最近被面試打擊的我來說,寫博客能夠讓我對知識理解的更加透徹,畢竟要自己認真思考后才能夠寫的明白(至少我覺得講明白了,哈哈哈)。如果有什么說的不對的地方,還請指出,感謝你的閱讀,thks。

參考資料

std::string formatting like sprintf

宏定義的黑魔法 - 宏菜鳥起飛手冊

整理:C/C++可變參數,“## VA_ARGS”宏的介紹和使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM