c可變引數詳解
前言
最近翻到今年前自己寫的hello word 的劣質程式碼。突然看見printf,這個可變引數的函式。而平時所編寫的都是固定引數。所以今天一步步德瞭解下可變引數函式的實現原理。
編寫一個自己寫的printf()函式。
需要了解的函式引數
1.可以通過…三個點表示可變引數
2.函式的引數是通過棧進行 。棧是由高到低來儲存,而且是從右往左讀入。函式傳遞過程就是壓棧的過程。如test(int a,int b,int c),先讀入c,再b再a。所以,如圖c的地址是最高的。
需要了解的位元組對齊
但發現了沒有,上面的地址,不管你是int還是char,地址都是4個位元組來排列。char不是隻佔一個位元組嗎。其實,在記憶體中,地址是不能隨便訪問的。比如在x86系統中,只能訪問4位元組倍數的地址
需要考慮的問題
函式傳入的可變引數是不固定的。那麼如何訪問這些引數呢??怎麼獲得引數的地址?
上面介紹了 引數傳遞是一個棧。棧遵守先進後出。那麼,傳入的引數1(上面的a)就是棧頂,而c就是棧底。只要知道剛開始傳入時候棧底的地址,最後的棧頂地址以及傳入引數型別,通過指標的移位可以獲得各個引數的地址。
可變引數函式程式碼
void dbg_print(const char* fmt, ...)
{
char dbg_st[1024];
va_list ap;
va_start(ap, fmt);//初始化ap,使ap指向第二個引數
vsprintf_s(dbg_st,fmt,ap);//使用引數列表ap,傳送格式化輸出到字串dbg_st。
va_end(ap);//釋放ap
printf(dbg_st);//將格式化好的字串dbg_st列印。
}
第三行 va_list
它就是一個char *的重新命名
typedef char* va_list
第四行 va_start()
va_start()用來初始化ap,先看看它的定義。(stdarg.h)
定義(詳解可以略過)
#define va_start __crt_va_start
沒啥用,還是定義,再看看 __crt_va_start定義
第一層定義
#define __crt_va_start(ap, x) ((void)(__vcrt_va_start_verify_argument_type<decltype(x)>(), __crt_va_start_a(ap, x)))
這個就挺有意思的了,又定義成了兩部分。
第一層定義的第一部分
__vcrt_va_start_verify_argument_type<decltype(x)>()函式實現如下,原來是關鍵字static_assert,用來靜態斷言,其語法很簡單:static_assert(常量表達式,提示字串)。如果第一個引數常量表達式的值為真(true或者非零值),那麼static_assert不做任何事情,就像它不存在一樣,否則會產生一條編譯錯誤,錯誤位置就是該static_assert語句所在行,錯誤提示就是第二個引數提示字串。
void __vcrt_va_start_verify_argument_type() throw()
{
static_assert(!__vcrt_va_list_is_reference<_Ty>::__the_value, "va_start argument must not have reference type and must not be parenthesized");
}
而常量表達式如下,是一些結構體模版。賦值語義就是false,取反就是true,為真就不做任何事。而如果是引用或者move語義,就會false,編譯器就會報錯。
template <typename _Ty>
struct __vcrt_va_list_is_reference
{
enum : bool { __the_value = false };
};
template <typename _Ty>
struct __vcrt_va_list_is_reference<_Ty&>
{
enum : bool { __the_value = true };
};
template <typename _Ty>
struct __vcrt_va_list_is_reference<_Ty&&>
{
enum : bool { __the_value = true };
};
我試一下,用引用傳入,果然如此
== 總結:原來,第一層定義的第一部分就是靜態斷言。用來編譯器報錯,沒啥用。==
第一層定義的第二部分
__crt_va_start_a(ap, x),又是兩部分,悲傷。
#define __crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
第一層定義的第二部分的第一部分
#define _ADDRESSOF(v) (&const_cast<char&>(reinterpret_cast<const volatile char&>(v)))
1–const_cast<type_id> (expression)
const_cast用來將const指標或者const引用修改為非const指標或者引用。如將expression強制轉換為type_id型別的非const引用。
2–reinterpret_cast (expression)
reinterpret_cast用來將一個指標轉換成一個整數或者指標等。如將expression指標或者 引用等轉換為type-id型別的(整數/指標)
第一層定義的第二部分的第一部分意思就是將v強制轉換為char型別的引用,再將這個char型別的引用去掉const,轉換為非const的char的引用。
總結:原來第一層定義的第二部分的第一部分就是將v強制型別轉換非const的char型別引用,也沒啥用。
第一層定義的第二部分的第二部分
((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))就是核心了,它是一個位演算法。作用:計算n的下一個引數的記憶體對齊地址。具體數學原理就不解釋了。如x86對齊是4位元組的倍數,n是傳入的第一個引數,如n是char =1, 1+3 & ~3-》 0100&1100 =4位元組。如果n是char * 指標為4位元組所以 n=4, 4+3& ~3-> 0111& 1100=4位元組。所以。這個演算法不管你第一個引數是什麼型別,都能獲得第二個引數的相對地址。
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
va_start(pt,fmt)函式總結
1 它會先靜態斷言,判斷是否有錯
2 它會強制轉換
3 它會計算第二個引數相對地址
4 將強制轉換的v+相對地址,賦值給pt。也就是第二個引數的地址。
第五行 vsprintf()函式
使用引數列表傳送格式化輸出到字串。
栗子
vsprintf()第一個引數是接收的陣列。第二個引數是格式化字串,第三個是待格式化的引數。如傳入2018,11,11引數,2018當做fmt去了,所以ap就是後面的第二個引數(11)開始的指標,會將11,11,傳入%d 中。
void printf(int fmt, ...)
{
CHAR dbg_st[1024];
va_list ap;
va_start(ap, fmt);
vsprintf_s(dbg_st, "光棍節快樂 2018年%d月%d日\n", ap);
va_end(ap);
printf(dbg_st);
}
printf(2018,11,11);
第五行 va_end()
va_end用來釋放ap
#define va_end __crt_va_end
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
over
所以,回過頭來看程式碼, 是不是很簡單。
dbg_print("hello %d",2018);
void dbg_print(const char* fmt, ...)
{
char dbg_st[1024];
va_list ap;
va_start(ap, fmt);//初始化ap,使ap指向第二個引數
vsprintf_s(dbg_st,fmt,ap);//使用引數列表ap,傳送格式化輸出到字串dbg_st。
va_end(ap);//釋放ap
printf(dbg_st);//將格式化好的字串dbg_st列印。
}