【C语言】printf参数使用说明
头文件
#include "stdarg.h"
//TI的BLE代码中包含下面这个头文件
//#include "printf-stdarg.h"
函数原型
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
典型的实现(ANSI C)
typedef char *va_list;
//va_start宏,获取可变参数列表的第一个参数的地址(list是类型为va_list的指针,param1是可变参数最左边的参数):
#define va_start(list,param1) ( list = (va_list)¶m1+ sizeof(param1) )
//va_arg宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数(mode参数描述了当前参数的类型):
#define va_arg(list,mode) ( (mode *) ( list += sizeof(mode) ) )[-1]
//va_end宏,清空va_list可变参数列表,使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码
#define va_end(list) ( list = (va_list)0 )
//注:以上sizeof()只是为了说明工作原理,实际实现中,增加的字节数需保证为为int的整数倍
//如:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统
C语言中函数参数的内存布局
1.函数参数是存储在栈中的,函数参数从右往左依次入栈。
2.栈由高地址往低地址生长
应用实例
printf实现
#include <stdarg.h>
int printf(char *format, ...)
{
va_list ap;
int n;
va_start(ap, format);
n = vprintf(format, ap);
va_end(ap);
return n;
}
```
error实现(错误打印函数)
```c
#include <stdio.h>
#include <stdarg.h>
void error(char *format, ...)
{
va_list ap;
va_start(ap, format);
fprintf(stderr, "Error: ");
vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
return;
}
```
## 不可错过的小惊喜
- va在这里是variable-argument(可变参数)的意思
- - va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同
- - 标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的
- - 由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型
- - 在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
- ⑴在固定参数中设标志-- printf函数就是用这个办法。
- ⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.
- 无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
- - 实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
- ①函数栈的生长方向
- ②参数的入栈顺序
- ③CPU的对齐方式
- ④内存地址的表达方式
- 结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。
- - 可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现.