注意 ! 文章中會出現大量可怕的 printf,需要搭配 Google 或 man page 閱讀 XD
C 語言沒有實作所謂的「字串」,取而代之的是「字元陣列」
也就是說 char * str = "Hello World !"; 這樣的東西,只不過是一段連續的記憶體
這使得 C 語言如果要初始化一個「格式化」字串有些麻煩
我就曾經設想過,C 語言有沒有辦法寫成像 Python 這樣高階 ?
s = 'It %s %d seconds .' % ('cost', 10) print(s)
|
如果去研究一下 printf 家族的東西,其實看來是有機會的
一般來說 printf 是寫入到標準輸出,你可能還知道 fprintf 是寫入到檔案輸出
而有一個 sprintf 是寫入到 C 字串用的
這裡有一個小問題 : sprintf 是危險的,可能會 Buffer overflow,那 snprintf 呢 ?
snprintf 安全多了,就是有限制長度版本的 sprintf
可是還是得預先知道字串長度,才有辦法設定 Buffer 大小
這就是 C 語言搞格式化字串麻煩的地方
原生的 C 語言既沒有「字串」的概念,還得自行管理記憶體
記憶體給太多是浪費,給太少會 Buffer overflow
究竟有沒有什麼辦法可以先算好格式化字串的大小 ?
查資料的時候又發現了 asprintf,這東西又是 sprintf 加強版
函數原型 : int asprintf(char **strp, const char *fmt, ...);
asprintf 完全是我們要找的東西,它可以拿來初始化格式化字串
而且乾脆連字串長度都幫你算好了,再幫你 malloc 跟寫入
使用完後要記得 free 就好
可惜 asprintf 是 GNU 的擴展,實際上根本不屬於 C 語言或者 POSIX 的標準
所以乾脆自己寫一個 asprintf,可以參考以下的 code
// asprintf.h
#ifndef __AS_H__ #define __AS_H__
#include <stdarg.h>
int vasprintf(char ** buffer, const char * format, va_list ap); int asprintf(char ** buffer, const char * format, ...);
#endif
|
// asprintf.c
#include <stdio.h> #include <stdlib.h> #include <stdarg.h>
int vasprintf(char ** buffer, const char * format, va_list ap) { int len = vsnprintf(NULL, 0, format, ap); char * str = malloc(sizeof(char) * (len + 1)); vsnprintf(str, len + 1, format, ap); *buffer = str; return len; }
int asprintf(char ** buffer, const char * format, ...) { va_list ap; va_start(ap, format); int ret = vasprintf(buffer, format, ap); va_end(ap); return ret; }
|
// main.c
#include <stdio.h> #include <stdlib.h>
#include "asprintf.h"
int main(int argc, char const * argv[]) { char * buffer; asprintf(&buffer, "It %s %d seconds .", "cost", 10); printf("%s", buffer); free(buffer); return 0; }
|
一共會有三支檔案,包含編譯到執行的畫面都如圖所示
如果只看 main.c 會發現,asprintf 真的是一個超高階的寫法
然後我再刻這東西的時候有參考一篇很棒的文章
Using asprintf() on windows
首先解釋一下 asprintf 的函式原型 : int asprintf(char **strp, const char *fmt, ...);
回傳值的 int 是告訴你字串長度,printf 家族都一樣
strp 是一個 pointer to pointer 的技巧,達到間接改寫 strp 的內容
fmt 就是格式化字串本身,就像平常用 printf 那樣
... 是不定參數,讓你傳入 %d 或 %s 要接的東西,也跟 printf 一樣
成功傳入一個 Buffer 跟格式化字串後,裡面用到一些 va_ 開頭的函式
這些東西是定義在 stdarg.h 裡面的,用來處理不定參數值
這時候又會 call 到 vasprintf,把原本的 ... 改成 va_list 的方式傳入
vasprintf 裡面又 call 了兩次 vsnprintf
vsnprintf 是 C 語言的標準,它的功能是把 va_list 的內容寫到 C 字串,並且限制長度
第一次 call 我們帶入 NULL 跟 0,因為並不是要真的寫入,而是讓它幫我們算字串長度
知道字串長度就解決我們最初的問題了,終於知道如何 malloc 剛好的大小
第二次 call 就真的帶入 Buffer、長度、格式化內容,並且真的寫入
接著回傳長度,asprintf 再把 va_list 釋放掉,再傳回長度
我們就成功初始化一個格式化字串了
這邊幫大家整理一下重點 :
1. printf 家族的成員雖然很多,但只是各種輸出到各種地方而已
不用感到太害怕,也不要死記
2. printf 家族的成員,其中帶有 n 的都是限制長度版本 ( 安全版本 )
就像 strcpy 還有 strncpy 版本一樣
3. printf 家族的成員,回傳值都是 int,每一個都會幫你算字串長度
4. asprintf 是一個很棒的函式,讓你用極度高階的方式初始化格式化字串
但是依然脫離不了記憶體管理,使用完記得要 free 還回去