最近翻到今年前自己写的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打印。
}
它就是一个char *的重命名
typedef char* va_list
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(), __crt_va_start_a(ap, x)))
这个就挺有意思的了,又定义成了两部分。
__vcrt_va_start_verify_argument_type
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(reinterpret_cast(v)))
1–const_cast
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))
1 它会先静态断言,判断是否有错
2 它会强制转换
3 它会计算第二个参数相对地址
4 将强制转换的v+相对地址,赋值给pt。也就是第二个参数的地址。
使用参数列表发送格式化输出到字符串。
栗子
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用来释放ap
#define va_end __crt_va_end
#define __crt_va_end(ap) ((void)(ap = (va_list)0))
所以,回过头来看代码, 是不是很简单。
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打印。
}