首先列举几个宏 __VA_ARGS__ , va_start , va_arg , va_end
__VA_ARGS__ c语言高级宏,描述可变参数
va_list 用来保存宏va_start、va_arg和va_end所需信息的一种类型。为了访问变长参数列表中的参数,必须声明 va_list类型的一个对象
定义: typedef char * va_list;
va_start 访问变长参数列表中的参数之前使用的宏,它初始化用va_list声明的对象,初始化结果供宏va_arg和va_end使用;实质上取得可变参数首地址
va_arg 展开成一个表达式的宏,该表达式具有变长参数列表中下一个参数的值和类型。每次调用va_arg都会修改
用va_list声明的对象,从而使该对象指向参数列表中的下一个参数;即获得可变参数
va_end 该宏使程序能够从变长参数列表用宏va_start引用的函数中正常返回。表示获取参数结束
问题提出
昨天我们在讨论使用printf重定向到串口的时候,只需要重定义fputc函数即可,并勾选使用microlib 库,但是如果想要自己实现呢,该如何去做
在写代码的时候,印象中c语言的参数是固定的,没有重载,参数不可变
但是在使用printf的时候却可以输入几乎任意个参数,是怎么做到的
其中printf函数原型 int printf( const char* format, ...);
在自己实现printf之前首先介绍一下c语言的可变参数。
可变参数
c 语言参数传递
c语言中,参数传递通过栈来实现,正常情况下的入栈规则为
__stdcall ,即参数从右到左,右边的参数先入栈,左边的参数后入栈,结果就是右边的参数在栈底,左边的参数在栈顶。
对于固定参数而言,应该是这样
void func(Int a,int b,int c)
{
...
}
参数排列,(假设向上增长)
a
b
c
对于一般的32位系统而言,栈字节大小应该是4字节
所以这里也就会牵扯到后面的一个4字节对其的问题
这里我们明确知道有几个参数,那么也就知道放在什么地方的是哪个参数,如果是可变参数呢,自然就没有那么顺利了。
传统c语言中允许不带任何固定参数的可变参数函数,真是xxxxx,估计找到参数在个地址都得费劲。还好现在的c要求至少有一个固定参数
,这至少告诉我们参数起始地址。
但编译器怎么知道你到底传入了几个参数?答案是不知道,因此这也给程序的健壮性带来了隐患,可能一不小心就改变其他的内容。
像printf这样,他是根据前面的固定参数来得知有几个参数,经常我们使用的标准输出格式 %c d f g...................
如果你参数个数不匹配,就容易出问题
可变参数的栈结构应该如图所示
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
展开上面一开始提到的宏
vs平台下
#define _ADDRESSOF(v) ( &(v) )
#define _INTSIZEOF(n) ( (sizeof(&n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (char *)( &v ) + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)( (ap += _INTSIZEOF(t) ) - _INTSIZEOF(t) ) )
#define va_end(ap) ( ap = (char *)0 )
解释一下 几个宏的意思
_INTSIZEOF 这个就是为了4字节对齐, 如果n是1,2,3个字节,
_INTSIZEOF (n)=4;如果是5,6,7则结果为8
现在来小试一下可变参数
- <blockquote>void func(int a, ...)
现在回到正题
如何实现printf
可以这样设想,按照要求的格式打印到一个字符数组里面,然后使用串口打印出整个字符串不就可以了吗?
这用这些宏,我用不同的方法来实现
1、借助于sprintf
- #define myprint(...) sprintf(buf,__VA_ARGS__) ;printf("%s",buf);
这样就可以直接使用myprintf了,用法跟printf一样,(ps,这里我是在win 模拟,如果在板子上,printf改成串口发送的就可以了)
2、上面那个太简单了,复杂一点,借助
于 int vsprintf(char *string, char *format, va_list param);
va_list 可变参数地址
将参数全部打印好,然后通过串口发送
- int func1(const char *format, ...)
- {
3、如果想要自己一句话一句话来实现呢,当然可以,不过那些%XXXXX 格式很麻烦
我在百度上找了一个
地址在这
- #include <stdio.h>
- #include <stdarg.h>
- #include "print.h"
- int main(void)
- {
- print("print: %c\n", 'c');
- print("print %d\n", 1234567);
- print("print: %f\n", 1234567.1234567);
- print("print: %s\n", "string test");
- print("print: %b\n", 0x12345ff);
- print("print: %x\n", 0xabcdef);
- print("print: %%\n");
- return 0;
- }
- void print(char* fmt, ...)
- {
- double vargflt = 0;
- int vargint = 0;
- char* vargpch = NULL;
- char vargch = 0;
- char* pfmt = NULL;
- va_list vp;
- va_start(vp, fmt);
- pfmt = fmt;
- while(*pfmt)
- {
- if(*pfmt == '%')
- {
- switch(*(++pfmt))
- {
-
- case 'c':
- vargch = va_arg(vp, int);
- /* va_arg(ap, type), if type is narrow type (char, short, float) an error is given in strict ANSI
- mode, or a warning otherwise.In non-strict ANSI mode, 'type' is allowed to be any expression. */
- printch(vargch);
- break;
- case 'd':
- case 'i':
- vargint = va_arg(vp, int);
- printdec(vargint);
- break;
- case 'f':
- vargflt = va_arg(vp, double);
- /* va_arg(ap, type), if type is narrow type (char, short, float) an error is given in strict ANSI
- mode, or a warning otherwise.In non-strict ANSI mode, 'type' is allowed to be any expression. */
- printflt(vargflt);
- break;
- case 's':
- vargpch = va_arg(vp, char*);
- printstr(vargpch);
- break;
- case 'b':
- case 'B':
- vargint = va_arg(vp, int);
- printbin(vargint);
- break;
- case 'x':
- case 'X':
- vargint = va_arg(vp, int);
- printhex(vargint);
- break;
- case '%':
- printch('%');
- break;
- default:
- break;
- }
- pfmt++;
- }
- else
- {
- printch(*pfmt++);
- }
- }
- va_end(vp);
- }
- void printch(char ch)
- {
- console_print(ch);
- }
- void printdec(int dec)
- {
- if(dec==0)
- {
- return;
- }
- printdec(dec/10);
- printch( (char)(dec%10 + '0'));
- }
- void printflt(double flt)
- {
- int icnt = 0;
- int tmpint = 0;
-
- tmpint = (int)flt;
- printdec(tmpint);
- printch('.');
- flt = flt - tmpint;
- tmpint = (int)(flt * 1000000);
- printdec(tmpint);
- }
- void printstr(char* str)
- {
- while(*str)
- {
- printch(*str++);
- }
- }
- void printbin(int bin)
- {
- if(bin == 0)
- {
- printstr("0b");
- return;
- }
- printbin(bin/2);
- printch( (char)(bin%2 + '0'));
- }
- void printhex(int hex)
- {
- if(hex==0)
- {
- printstr("0x");
- return;
- }
- printhex(hex/16);
- if(hex < 10)
- {
- printch((char)(hex%16 + '0'));
- }
- else
- {
- printch((char)(hex%16 - 10 + 'a' ));
- }
- }
关于可变参数的一些推理证明
网上copy的
对于两个正整数 x, n 总存在整数 q, r 使得
x = nq + r, 其中 0<= r //最小非负剩余
q, r 是唯一确定的。q = [x/n], r = x - n[x/n]. 这个是带余除法的一个简单形式。在 c 语言中, q, r 容易计算出来: q = x/n, r = x % n.
所谓把 x 按 n 对齐指的是:若 r=0, 取 qn, 若 r>0, 取 (q+1)n. 这也相当于把 x 表示为:
x = nq + r', 其中 -n < r' <=0 //最大非正剩余
nq 是我们所求。关键是如何用 c 语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:
x+n = qn + (n+r'),其中 0 //最大正剩余
x+n-1 = qn + (n+r'-1), 其中 0<= n+r'-1 //最小非负剩余
所以 qn = [(x+n-1)/n]n. 用 c 语言计算就是:
((x+n-1)/n)*n
若 n 是 2 的方幂, 比如 2^m,则除为右移 m 位,乘为左移 m 位。所以把 x+n-1 的最低 m 个二进制位清 0就可以了。得到:
(x+n-1) & (~(n-1))
最后再加上一个关于调试用的技巧
特别是对于大型程序中,日志是非常重要的,不然问题找都找不到
那么可以利用这几个宏
__FILE__, __LINE__ ,__FUNCTION__
当前文件 当前行 当前函数
可以定义一个这样的宏
- #define myprintf(...) printf("[lch]:File:%s, Line:%d, Function:%s," \
- __VA_ARGS__, __FILE__, __LINE__ ,__FUNCTION__);
打印出出错位置,至于打印到哪里,那只是一个流向的问题,流向文件或终端