C/C++雜談-printf的可變參數機制
文章目錄
- C/C++雜談-printf的可變參數機制
- printf的使用
- printf的源碼
- 源碼剖析
- 多參數實現機制原理
C++11引入了可變參數模板機制,對模板參數進行了高度泛化,但是對于可變參數其實C語言學習中早已遇到過,那就是printf可以進行多參數的輸出,這是怎么實現的呢?
printf的使用
我們對于printf的用法無非兩種
const char *str = "hello , world\n";printf(str);//直接傳入字符串地址int year = 2023;printf("%d%s", year, "原神啟動");//傳入格式控制字符串地址和參數
我們printf的參數是先是一個字符串,后面才是我們的輸出變量,可以嗅出printf對于多參數的控制應該和傳入的第一個字符串有關,那么究竟是如何實現的呢?
printf的源碼
//acenv.h
typedef char *va_list;
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
//start.c
static char sprint_buf[1024];
int printf(char *fmt, ...)//格式控制字符串和C的函數多參數
{va_list args;//va_list就是char * 的typedefint n;va_start(args, fmt);n = vsprintf(sprint_buf, fmt, args);va_end(args);write(stdout, sprint_buf, n);return n;
}
//unistd.h
static inline long write(int fd, const char *buf, off_t count)
{return sys_write(fd, buf, count);
}
源碼剖析
映入眼簾的就是一串宏定義
看我們printf的函數參數部分,char *fmt就是我們的格式控制字符串,后面的…是C的函數多參數,即后面的參數數目不定
va_list就是char * 的typedef,也就是定義了名為args的char指針
va_start(args, fmt);就是把args指向fmt后面的第一個參數的地址
這里對va_start進行解釋
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
可見ap指向的是A地址往后偏移_bnd字節,而 _bnd 傳參了 _ADNBND,_ADNBND = (sizeof (acpi_native_int) - 1)
typedef s32 acpi_native_int 若采用這種宏定義,表明int 類型是用32位表示,也表示當前內存是4字節對齊
acpi_native_int這個參數是平臺相關的
所以sizeof (acpi_native_int)是當前平臺的int大小,我們假設是4字節,那么_ADNBND就是3
#define _bnd(char,3) ==> (1+3)&(~3) ==> 4#define _bnd(int,3) ==> (4+3)&(~3) ==> 4#define _bnd(double,3) ==> (4+3)&(~3) ==> 8
我們通過上述樣例可以明白**_bnd就是獲取類型A的內存對齊大小**,假如32位平臺那么就是4的倍數
所以va_start(args, fmt) 就是把fmt偏移char*內存對齊大小個字節然后賦值給args,這樣args指向的就是格式字符串后面的參數
n = vsprintf(sprint_buf, fmt, args);這里的n則是我們實際控制輸出的字符數,我們printf實際就是一個輸出字符的函數,n也是我們的返回值
而后面的 write(stdout, sprint_buf, n);無非就是把緩沖區里的n個字符輸出到stdout輸出流,這就不是我們討論的重點了
多參數實現機制原理
通過上面的剖析,我們發現printf由格式控制字符串得到下一個參數的起始地址,而下一個起始地址是fmt地址偏移內存對齊大小個字節
這是為什么呢?
這跟函數的壓棧順序有關。我們C/C++默認__cdel的從右至左將參數壓棧,而我們棧是向下增長的,所以先入棧的地址高,后入棧的地址低,所以格式字符串的地址最低,往上偏移自然能得到其他參數的地址
void func(int a, int b, int c)
{printf("a = %d located [%x]\n", a, &a);printf("b = %d located [%x]\n", b, &b);printf("c = %d located [%x]\n", c, &c);
}
signed main()
{func(1, 2, 3);return 0;
}
//輸出
a = 1 located [b3bff960]
b = 2 located [b3bff968]
c = 3 located [b3bff970]
得到地址后,由于我們規定格式控制字符串中%的數量即為輸出參數數量,然后就能拿到所有參數放到緩沖區,再輸出到標準輸出流
如果我們想要實現多參數機制(需要了解<stdarg.h>),自然也要通過我們的參數設定模式,類似格式控制中百分號的數量來確定參數的數目,而名稱出現的順序對應參數的順序。
可見C語言的多參數機制是很繁瑣的,而我們C++11引入可變參數模板也正是為了追求更好的參數泛化。