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是不包含結束符的,所以真正寫入要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("%sn", 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("%sn", 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("%sn", 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」宏的介紹和使用