printf 宏 可变参数

1 编译器内置宏

先介绍几个编译器内置的宏定义,这些宏定义不仅可以帮助我们完成跨平台的源码编写,灵活使用也可以巧妙地帮我们输出非常有用的调试信息。

ANSI C标准中有几个标准预定义宏(也是常用的):

__LINE__:在源代码中插入当前源代码行号;

__FILE__:在源文件中插入当前源文件名;

__DATE__:在源文件中插入当前的编译日期

__TIME__:在源文件中插入当前编译时间;

__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;

例子如下:
#include <stdio.h>

#define DEBUG

#ifdef DEBUG
#define DEBUG(format,…) printf(“Date: “DATE”,File: “FILE”, Line: %05d: “format”\n”, LINE, ##VA_ARGS)
#else
#define DEBUG(format,…)
#endif

int main(int argc, char **argv) {
char str[]=“Hello World”;
DEBUG("%s",str);
return 0;
}
执行结果如下:
Date: Jan 2 2020,File: test7.c, Line: 00013: Hello World

  1. VA_ARGS … 宏和可变参数

sprintf 可变参数列表的实现

还是要先贴一下关键代码

static int sprintf(char * str, const char *fmt, …)
{
va_list args;
int i;

va_start(args, fmt);
i = vsprintf(str, fmt, args);
va_end(args);
return i;

}

上面这段代码相当简单,直觉告诉我们,这里面比较关键是这几个点:

va_list 是什么
va_start 是什么
vsprintf 实现了什么

我们来一一解答。

va_list 是什么?

typedef char *va_list;

1

从声明来看, va_list 就是一个字节指针,更确切地说它表示一个内存地址

va_start 是什么?

/* Amount of space required in an argument list for an arg of type TYPE.
TYPE may alternatively be an expression whose type is used. */

#define __va_rounded_size(TYPE)
(((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))

#ifndef sparc
#define va_start(AP, LASTARG)
(AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#else
#define va_start(AP, LASTARG)
(__builtin_saveregs (),
AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#endif

#define va_arg(AP, TYPE)
(AP += __va_rounded_size (TYPE),
*((TYPE *) (AP - __va_rounded_size (TYPE))))

va_start 是一个宏定义,套入 va_start(args, fmt) 后得到

args = (char *) &(fmt) + __va_rounded_size(fmt)

1

根据注释, __va_rounded_size(fmt) 是求得 fmt 所需要的内存空间,并且补齐到4字节边界。 综上所述, va_start 的作用就是调整 args 到 fmt 后面的内存地址处。至于为什么这么处理,我们还得看 vsprintf 的实现。

vsprintf 实现了什么?

vsprintf 的实现代码很长,大部分是处理格式化字符串的逻辑,我们只摘取其中涉及可变参数列表的部分。


switch (*fmt) {
case ‘c’:

*str++ = (unsigned char) va_arg(args, int);

break;
case ‘s’:
s = va_arg(args, char *);

break;
case ‘o’:
str = number(str, va_arg(args, unsigned long), 8,
field_width, precision, flags);
break;
case ‘p’:

str = number(str,
(unsigned long) va_arg(args, void *), 16,
field_width, precision, flags);
break;

}

这段逻辑是该函数的重点,简单的说,在扫描 fmt 的过程中,如果遇到特殊字符,就从参数中获取对应类型的值。如何获取?可以看出,在每个分支中都会有 va_arg 的调用,就是通过这个宏取得对应类型的值。我们找一个例子来说明一下。

把 va_arg(args, unsigned long) 通过宏转换之后,得到如下语句:

(args += __va_rounded_size(unsigned long), *((unsigned long *)(args - __va_rounded_size(unsigned long))));

1
2

乍一看是个挺复杂的语句, 其实它做了两件事,一是从 args 当前所表示的内存处取出对应类型的值,二就是让 args 指向所取值后面的内存地址处。

弄清楚了几个关键点之后,我们再回过头来看看。sprintf 的典型调用如下所示:

sprintf(buf, “This is only a test %d, %s”, 1, “hello”);

1

结合 C 函数调用的机制,所有的参数都会在函数调用前按照一定的顺序被压入栈中, 只要通过 va_start 设置好参数的内存起始位置,就可以通过 va_arg 取得所有对应类型的参数,而具体要取得什么类型的参数,则是由格式字符串中的特殊字符决定的。

#include<stdio.h>
#include<stdarg.h>
int Average(int n, …)
{
va_list arg;
int sum = 0;
int i = 0;
va_start(arg, n);
for (i = 0; i < n; i++)
{
sum += va_arg(arg, int);
}
return sum / n;
va_end(arg);
}

int Max(int n, …)
{
va_list arg;
va_start(arg, n);
int i = 0;
int max = va_arg(arg, int);
for (i = 0; i < n - 1;i++)
{
int ret = va_arg(arg, int);
if (ret > max)
{
max = ret;
}
}
return max;
va_end(arg);
}
int main()
{
int num = 0;
num = Average(7, 1, 2, 3, 4, 5, 6, 7);
printf("%d\n", num);
num = Max(6, 1, 2, 7, 5, 6, 8);
printf("%d\n", num);
return 0;
}

发布了28 篇原创文章 · 获赞 6 · 访问量 1513

猜你喜欢

转载自blog.csdn.net/lx123010/article/details/103808431
今日推荐