基于串口通信做my_printf时遇到的坑儿
首先,完成了串口向终端putty的打印函数ConsolePrint(),但该函数只能打印字符串,无法像stdio库中的printf函数一样打印整数和浮点数等。
因此,我先是使用了标准库stdio中的sprintf函数。该函数可以将所要打印的数字格式化成对应的字符串并存储到字符串数组中,如sprintf( str_buffer, "num : %.2f", 12.34)将字符串“num : ”及浮点数12.34转换成对应的字符串“num : 12.34”存储在str_buffer数组中,接下来就可以用串口打印函数ConsolePrint(str_buffer)将其打印到终端putty。具体的使用方法如下:
char str_buffer[ 32 ]; sprintf( str_buffer, "num : %d", 12.34 ); ConsolePrint( str_buffer );
但是这样使用很不方便,每次使用都要先声明一个数组,调sprintf进行格式化操作,最后再打印。那么能否将这些代码封装成一个函数呢?
答案是:很难!
首先我们需要抽象一个用户函数,使其能够像printf一样使用,既能够只打印字符串,也能够打印数字,而且可以一次打印多组数字。抽象得到的函数原型为:
void my_printf( const char *format, ... )
该函数是一个具有可变参数的函数,这样才能满足打印多组数字的要求。
我们想要在my_printf中调用sprintf函数,其函数原型为:
int _EXFUN(sprintf, (char *, const char *, ...)
这时就遇到了一个较为复杂的问题:一个具有可变参数的函数其子函数也具有可变参数。此时就需要在my_printf根据打印格式对可变参数进行解析(变参数的解析使用va_arg( ap, <type>,其中type是数据类型,必须根据打印格式知道其类型后才能解析出正确的数),然后再将解析后的数传给sprintf,在传的过程中还要看情况选择是否使用sprintf的变参数部分,整体来说非常麻烦。
--------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------
要解决这个问题可以使用vsprintf函数,其函数原型为
int _EXFUN(vsprintf, (char *, const char *, __VALIST)
vsprintf接受一个va_list类型的形参用于接受可变参数的栈指针,这样只要将my_printf中可变参数的栈指针传递给它即可,vsprintf内部有对可变参数进行解析的程序从而将数字按照指定格式转换成对应的字符串(即使调用my_Printf时没有使用可变参数,vsprintf也能够进行处理),这样my_printf函数只要将变参的栈指针传递给它进行处理即可,具体实现如下:
void my_printf( char const *format, ... ) { char buffer[ BUFFER_SIZE ]; va_list ap; va_start( ap, format ); //将ap初始化为变参数栈的栈顶指针 vsprintf( buffer, format , ap ); //将指针ap传递给vsprintf做格式化处理 va_end( ap ); Console_Print( buffer ); }
vsprintf函数的内部实现大致如下(并非标准库的实现):
int usr_vsprintf(char *dest, const char *fmt, va_list ap) { char c, sign, *cp, *dp = dest; int left_prec, right_prec, zero_fill, length, pad, pad_on_right; char buf[32]; long val; while ((c = *fmt++) != 0) { cp = buf; length = 0; if (c == '%') { c = *fmt++; left_prec = right_prec = pad_on_right = 0; if (c == '-') { c = *fmt++; pad_on_right++; } if (c == '0') { zero_fill = TRUE; c = *fmt++; } else { zero_fill = FALSE; } while (is_digit(c)) { left_prec = (left_prec * 10) + (c - '0'); c = *fmt++; } if (c == '.') { c = *fmt++; zero_fill++; while (is_digit(c)) { right_prec = (right_prec * 10) + (c - '0'); c = *fmt++; } } else { right_prec = left_prec; } sign = '\0'; /* handle type modifier */ if (c == 'l' || c == 'h') { c = *fmt++; } switch (c) { case 'd' : case 'u' : case 'x' : case 'X' : val = va_arg(ap, long); switch (c) { case 'd' : if (val < 0) { sign = '-'; val = -val; } /* fall through */ case 'u' : length = _cvt(val, buf, 10, "0123456789"); break; case 'x' : length = _cvt(val, buf, 16, "0123456789abcdef"); break; case 'X' : length = _cvt(val, buf, 16, "0123456789ABCDEF"); break; } break; case 's' : cp = va_arg(ap, char *); length = strlen(cp); break; case 'c' : c = (char)va_arg(ap, long); *dp++ = c; continue; case '%' : /* '%%' ==> output '%' */ *dp++ = c; break; default: *dp++ = '?'; } pad = left_prec - length; if (sign != '\0') { pad--; } if (zero_fill) { c = '0'; if (sign != '\0') { *dp++ = sign; sign = '\0'; } } else { c = ' '; } if (!pad_on_right) { while (pad-- > 0) { *dp++ = c; } } if (sign != '\0') { *dp++ = sign; } while (length-- > 0) { c = *cp++; if (c == '\n') { *dp++ = '\r'; } *dp++ = c; } if (pad_on_right) { while (pad-- > 0) { *dp++= ' '; } } } else { if (c == '\n') { *dp++= '\r'; } *dp++ = c; } } *dp = '\0'; return ((int)dp - (int)dest); }
据说,sprintf就是用vsprintf来实现的,其大致实现如下(非标准库代码):
int usr_sprintf(char *buf, char const *fmt, ...) { int ret; va_list ap; va_start(ap, fmt); ret = usr_vsprintf(buf, fmt, ap); va_end(ap); return ret; }
总结一下:
为什么不用sprintf而是vsprintf?引用https://blog.csdn.net/heybeaman/article/details/80495846#commentBox博主的话来说,就是因为 vsprintf() 比 sprintf() 更加接近底层(栈)。
为什么void my_printf( const char *format, ... )调用int _EXFUN(sprintf, (char *, const char *, ...)会难以处理?使用 sprintf() 只能原始的为它输入所有的参数而不能以传参的方式给它。
--------------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------
最后说说所谓的“坑儿”。my_printf函数定义在console_print.c文件中,在main函数调用,此时如果不在main中声明my_printf函数或者在consol_print.h中声明,而在main函数中include(声明如下),会发生变参数部分解析错误的情况,一般是把变参数解析为0。
extern void my_printf( const char *format, ... );