重写printf


前言

  在使用stm32时,有时候需要用到两个printf,但是通过重定向,只能把printf重定向到一个串口上。因此打算重写一个printf函数。

一、可变参函数

  要实现重写printf函数就首先需要学习可变参函数。可变参函数这个是怎么实现,首先我们就要先理解一点,参数是如何传递给函数的。众所周知,函数的数据是存放于栈中的,那么给一个函数传递传递参数的过程就是将函数的参数从右向左逐次压栈,例如:

  func(int i, char c, doube d)

  这个函数传递参数的过程就是将d,c,i逐次压到函数的栈中,由于栈是从高地址向低地址扩展的,所以d的地址最高,i的地址最低。

  理解了函数传递参数的过程,再来说一下va_list的原理,通常,可变参数的代码是这么写的:

void func(char *fmt, ...)
{
     va_list ap;

     va_start(ap, fmt);
     va_arg(ap, int);
     va_end(va);
}

  这里ap其实就是一个指针,指向了参数的地址。

  va_start()所做的就是让ap指向函数的最后一个确定的参数(声明程序中是fmt)的下一个参数的地址。

  va_arg()所做的就是根据ap指向的地址,和第二个参数所确定的类型,将这个参数的中的数据提取出来,作为返回值,同时让ap指向下一个参数。

  va_end()所做的就是让ap这个指针指向0。

// 使ap指向第一个可变参数的地址
#define  va_start(ap,v)     ( ap = (va_list)&v + _INTSIZEOF(v) )

// 使ap指向下一个可变参数,同时将目前ap所指向的参数提取出来并返回
#define  va_arg(ap,t)       ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

//  销毁ap
#define  va_end(ap)         ( ap = (va_list)0 )

参考地址:https://www.cnblogs.com/bwangel23/p/4700496.html

二、占位符和可变参共同实现printf

代码实现

/***********************************************
 * 文件名: myPrintf.c
 * 文件功能: 使用putchar函数模拟printf函数的功能
 * 编辑人: 王廷云
 * 编辑时间: 2017-10-14
 * 修改时间: 2018-1-12
************************************************/

#include <stdio.h>
#include <stdarg.h>

/* 功能函数声明 */
void myPrintf(char *s, ...);				// 需要实现的目标函数
void printNum(unsigned long num, int base); // 通用数字打印函数 
void printDeci(int dec);					// 打印十进制数
void printOct(unsigned oct);				// 打印八进制数
void printHex(unsigned hex);				// 打印十六进制数
void printAddr(unsigned long addr);			// 打印地址
void printStr(char *str);					// 打印字符串
void printFloat(double f);					// 打印浮点数

/*
 * 程序从主函数开始
 * 思路:
 * 	->实现功能类似于printf函数的myPrintf函数
 * 	->支持字符,字符串,十进制整数,八进制整数,
 * 	  十六进制整数,浮点数,地址的打印
*/
int main(int argc, char **argv)
{
	/* 用于测试的变量 */
    int a = 255;			// 整数
    float f = 10.000;				// 浮点数
    char c = 'z';					// 字符
    char *s = "this is a string";	// 字符串
    int *p = &a;					// 地址

	/* 这是printf函数的结果 */
    printf("printf   >>> 十进制:%d 八进制:%o 十六进制:%x\n", a, a, a);
	printf("printf   >>> 字符:'%c' 字符串:%s 浮点数:%f\n", c, s, f);
	printf("printf   >>> 地址:%p\n\n", p);

    /* 这是myPrintf函数的结果 */
    myPrintf("myPrintf >>> 十进制:%d 八进制:%o 十六进制:%x\n", a, a, a);
	myPrintf("myPrintf >>> 字符:'%c' 字符串:%s 浮点数:%f\n", c, s, f);
	myPrintf("myPrintf >>> 地址:%p\n", p);

    return 0;
}

/*
 * 函数名: myPrintf
 * 函数功能: 打印格式字符串
 * 参数: 1. 包含格式符的字符串地址 2.可变参
 * 返回值: 无
*/
void myPrintf(char *s, ...)
{
    int i = 0;

	/* 可变参第一步 */
    va_list va_ptr;

	/* 可变参第二部 */
    va_start(va_ptr, s);

	/* 循环打印所有格式字符串 */
    while (s[i] != '\0')
    {
		/* 普通字符正常打印 */
		if (s[i] != '%')
		{
    	    putchar(s[i++]);
			continue;
		}
		
		/* 格式字符特殊处理 */
		switch (s[++i])   // i先++是为了取'%'后面的格式字符
		{
		    /* 根据格式字符的不同来调用不同的函数 */
			case 'd': printDeci(va_arg(va_ptr,int));           
			  		  break; 
		    case 'o': printOct(va_arg(va_ptr,unsigned int));  
			  		  break;
		    case 'x': printHex(va_arg(va_ptr,unsigned int));  
			  		  break;
		    case 'c': putchar(va_arg(va_ptr,int));            
			  		  break;
		    case 'p': printAddr(va_arg(va_ptr,unsigned long));
			  		  break;
		    case 'f': printFloat(va_arg(va_ptr,double));      
			  		  break;
		    case 's': printStr(va_arg(va_ptr,char *));
					  break;
			default : break;
		}

		i++; // 下一个字符
    }

	/* 可变参最后一步 */
    va_end(va_ptr);
}

/*
 * 函数名: printNum()
 * 函数功能: 通用数字打印函数可以把整型值打印成
 *           10进制数,8进制数,2进制数,16进制数
 * 参数: 1.需要打印的整数,无符号长整型是为了兼容
 *         地址格式打印; 2.打印的进制
 *  返回值: 无
*/
void printNum(unsigned long num, int base)
{
    /* 递归结束条件 */
	if (num == 0)
    {
        return;
    }
    
    /* 继续递归 */
	printNum(num/base, base);

	/* 逆序打印结果 */
    putchar("0123456789abcdef"[num%base]);
}


/*
 * 函数名: printDeci
 * 函数功能: 打印十进制数
 * 参数: 十进制整数
 * 返回值: 无
*/
void printDeci(int dec)
{
    int num;

    /* 处理有符号整数为负数时的情况 */
	if (dec < 0)
    {
        putchar('-');
		dec = -dec;  	   // 该操作存在溢出风险:最小的负数没有对应的正数
    }

    /* 处理整数为时0的情况 */
    if (dec == 0)
    {
        putchar('0');
		return;
    }
    else
    {
        printNum(dec, 10); // 打印十进制数
    }
}

/*
 * 函数名: printOct
 * 函数功能: 打印八进制整数
 * 参数: 无符号整数
 * 返回值: 无
*/
void printOct(unsigned oct)
{
    if (oct == 0)			// 处理整数为0的情况
    {
		putchar('0');
		return;
    }
    else
    {
        printNum(oct,8);	// 打印8进制数
    }
}

/*
 * 函数名: printHex
 * 函数功能: 打印十六进制整数
 * 参数: 无符号整数
 * 返回值: 无
*/
void printHex(unsigned hex)
{
    if (hex == 0)			// 处理整数为0的情况
    {
        putchar('0');
		return;
    }
    else
    {
        printNum(hex,16);	// 打印十六进制数
    }
}

/*
 * 函数名: printAddr
 * 函数功能: 打印地址
 * 参数: 待打印的地址
 * 返回值: 无
*/
void printAddr(unsigned long addr)
{
    /* 打印前导"0x" */
	putchar('0');
    putchar('x');

	/* 打印地址:格式和十六进制一样 */
    printNum(addr, 16);
}

/*
 * 函数名: printStr
 * 函数功能: 打印字符串
 * 参数: 字符串地址
 * 返回值: 无
*/
void printStr(char *str)
{
    int i = 0;

    while (str[i] != '\0')
    {
        putchar(str[i++]);
    }
}

/*
 * 函数名: printFloat
 * 函数功能: 打印浮点数
 * 参数: 待打印浮点数
 * 返回值: 无
*/
void printFloat(double f)
{
    int temp;

	/* 先打印整数部分 */
    temp = (int)f;
    printNum(temp,10);
	
	/* 分隔点 */
    putchar('.');

	/* 打印小数部分 */
    f -= temp;
    if (f == 0)
    {
		/* 浮点型数据至少六位精度 */
		for (temp = 0; temp < 6; temp++)
		{
		    putchar('0');
		}
		return;
    }
    else
    {
        temp = (int)(f*1000000);
        printNum(temp,10);
    }
}

实现流程

  • 循环打印字符
  • 是否有占位符%
  • 判断占位符%后的格式%d %f %c
  • 读取变参转换成字符串

重要代码

case 'd': printDeci(va_arg(va_ptr,int)); 

调用va_arg(va_ptr,int)读取变参数值,再用printDeci()转为字符串打印

思考

case 'd': printDeci(va_arg(va_ptr,int)); 

  可以看到,%d都是用int类型去读取变参,但是char类型用%d打印,数据没问题。char类型占一个字节,int类型占4个字节,如果一个char的类型,用int去读取,会导致指针自增过多出错。但是,我们平时用的系统都是32位或者64位的,64位的有兼容32的汇编指令,32位操作系统操作一次,数据量都是32位的,因此无论是char还是int操作字节大小都是32位,参数传递过程中是需要进栈的,32位操作系统栈的最小单位就是4个字节,int的大小,因此打印char或者short类型都可以用%d打印,即使%d是按照int类型去读取。反而打印long long就需要用%lld。如果用%d打印就会出错,因为long long 站8字节,用int打印va_ptr指针自增1个地址,正确应该自增2个地址。
  32位存储char类型还是会合并存储到一个内存单元的,这是为了优化存储空间。

struct STUDENT
{
    char a;
    char b;
    int c;
}data;

  那么这三个成员是怎么对齐的?a 和 b 后面都是空 3 字节吗?不是!如果没有 b,那么 a 后面就空 3 字节,有了 b 则 b 就接着 a 后面填充。即:

在这里插入图片描述

  va_arg(va_ptr,char)这个代码是会崩溃的哦,地址无法增加1个字节

改进

数值转字符串可以用标准库实现


函数名                  作  用

itoa()           将整型值转换为字符串,可以转换成不同进制的字符串
itoa()           将长整型值转换为字符串
ultoa()         将无符号长整型值转换为字符串
ecvt()    将双精度浮点型值转换为字符串,转换结果中不包含十进制小数点
fcvt()    以指定位数为转换精度,余同ecvt()
gcvt()    将双精度浮点型值转换为字符串,转换结果中包含十进制小数点

putchar(“0123456789abcdef”[num%base]);

函数原型:int putchar(int char),参数是一个字符,"0123456789abcdef"是字符串的首地址,[num%base]是一个索引,合起来就是一个数组的索引,数组的名称也是首地址嘛。

a[-1]

a[-1]= 这个是有意义的, 而且有这样用的代码
比如我们都知道数组下表是从0开始的
那假如我们想从1开始怎么办
定义一个指针,指向a[-1]这个位置,
#include <stdio.h>
void main()
{
int a[] = {1,3,4};
int *p = &a[-1];
int i =0;
for( i = 1; i <4; i++)
{
printf("%d\n", p[i]);
}
}
1。 因为数组并不检查下表是否越界
2。 下表仅表示偏移, -1就表示第一个元素前面那个元素

参考地址:

https://blog.csdn.net/aiwangtingyun/article/details/79619265
http://c.biancheng.net/cpp/html/1573.html
http://c.biancheng.net/view/243.html

  • 4
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值