C、C++格式化字符串

引言

在C和C++開發中,咱們常常會用到printf來進行字符串的格式化,例如printf("format string %d, %d", 1, 2);,這樣的格式化只是用於打印調試信息。printf函數實現的是接收可變參數,而後解析格式化的字符串,最後輸出到控制檯。那麼問題來了,當咱們須要實現一個函數,根據傳入的可變參數來生成格式化的字符串,應該怎麼辦呢?git

你能夠在這裏看到更好的排版github

正文

可變參數

首先來一個可變參數使用示例,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);
}

運行結果app

------testVari------
2 3 4 5 6 -1

格式化字符串

好了,咱們已經介紹了怎樣實現一個接收可變參數的C函數,接下來介紹根據接收的可變參數來格式化字符串。這裏介紹兩種方式,第一種是利用宏定義,第二種經過函數的方式來實現。less

經過宏定義的方式

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語句呢?這是爲了宏的健壯性,若是使用宏的人像下面這樣使用的話,就會出問題ui

#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);
}

上面的代碼連編譯都不會經過, 會報錯以下:.net

若是手動展開這個宏的話,會變成這個樣子,問題就顯而易見了。可是若是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」宏的介紹和使用

相關文章
相關標籤/搜索