1. 程式人生 > >c可變引數詳解

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列印。
}