c可变参数详解

前言

最近翻到今年前自己写的hello word 的劣质代码。突然看见printf,这个可变参数的函数。而平时所编写的都是固定参数。所以今天一步步德了解下可变参数函数的实现原理。
编写一个自己写的printf()函数。

需要了解的函数参数

1.可以通过…三个点表示可变参数
2.函数的参数是通过栈进行 。栈是由高到低来存储,而且是从右往左读入。函数传递过程就是压栈的过程。如test(int a,int b,int c),先读入c,再b再a。所以,如图c的地址是最高的。
c可变参数详解_第1张图片

需要了解的字节对齐

但发现了没有,上面的地址,不管你是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(), __crt_va_start_a(ap, x)))

这个就挺有意思的了,又定义成了两部分。

第一层定义的第一部分

__vcrt_va_start_verify_argument_type()函数实现如下,原来是关键字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 };
        };

我试一下,用引用传入,果然如此
c可变参数详解_第2张图片
== 总结:原来,第一层定义的第一部分就是静态断言。用来编译器报错,没啥用。==

第一层定义的第二部分

__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 (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);

c可变参数详解_第3张图片

第五行 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打印。
}

你可能感兴趣的:(c/c++)