天天看点

自己动手用C语言实现迷你printf函数可变参数函数简单解读(variadic function)printf函数实现原理简单解读

可变参数函数简单解读(variadic function)

平时写代码接触最多函数大概就是 printf 函数了,printf 函数就是一个可变参数函数,第2个参数就是可变参数,第一个参数是格式化字符串。格式化字符串中的转换修饰符决定了可变参数的数量和类型。

可变参数函数(variadic function):函数参数变量可变的函数。可变参函数包括至少一个强制参数(mandatory argument)+数量可变的可选参数(optional argument),强制性参数在前,可变参数用省略号(…)表示。

实现可变参数函数需要包含**<stdarg.h>**头文件,他提供需要用到的下列宏定义:

typedef char* va_list;
 void va_start(va_list argptr, lastparam);
 type va_arg(va_list argptr, type);
 void va_end(va_list argptr);
 void va_copy(va_list dest, va_list src);
           
  • typedef char va_list;*:简单理解就是指向就是存放可变参数列表的指针,访问这个列表可以得到每一个可变参数。
第一步:定义函数,并用这个类型定义一个变量:
int m_printf(const char *f_str,...)
{
	va_list p_list;
}
           
  • void va_start(va_list argptr, lastparam);:第一个参数为va_list类型的变量,第2个参数为可变参的数量。此函数会做可变参数链表的初始化等工作。
第2步:用va_start初始化 p_list变量。format为printf函数的第一个参数。
int m_printf(const char *f_str,...)
{
	va_list p_list;
	
	va_start(p_list,f_str);
}
           
  • type va_arg(va_list argptr, type);:第一个参数为va_list类型的变量,第2个参数为可变参数的类型。此函数会按照从左到右的顺序从可变参数列表里面pop出一项可变参数的值,然后p_list指针会指向下一个可变参数。
第3步:用va_arg获取可变参参数的值。根据参数类型,type需要设置为不同的类型。
int m_printf(const char *f_str,...)
{
	va_list p_list;
	int value;
	char* chs;
	
	va_start(p_list,f_str);
	... ...
	/* pop出一个int类型的值 */
	value = va_arg(p_args,int);
	/* pop出一个字符串 */
	chs = va_arg(p_args,char*);
}
           
  • void va_end(va_list argptr);:参数列表指针不需要被使用的时候需要调用此函数完成清理等工作。
最后一步调用va_end完成清理工作。
int m_printf(const char *f_str,...)
{
	va_list p_list;
	int value;
	char* chs;
	
	va_start(p_list,f_str);
	... ...
	/* pop出一个int类型的值 */
	value = va_arg(p_args,int);
	/* pop出一个字符串 */
	chs = va_arg(p_args,char*);
	... ...
	va_end(p_args);
}
           
  • 下面是一个简单的可变参数函数

    add()

    的实现代码:
/* num:表示可变参数的数量 */
int add(int num,...)
{
    int sum,i;
    va_list p_list;
    int value;
    sum = 0;

    va_start(p_list,num);

    for(i=0;i<num;i++)
    {
        value = va_arg(p_list,int);
        sum +=value;
    }

    va_end(p_list);

    return sum;
}

void add_t(void)
{
    printf("%d %d %d \n",add(2,3,4),add(3,4,5,1),add(4,2,2,1,1));
}
           

printf函数实现原理简单解读

printf函数的格式化输出是使用百分号

%

控制输出的格式。printf第一个参数是格式化字符串,所以需要解析这个字符串,然后根据不同的输出格式输出对应字符。最终调用

putc()

等类似函数输出一个个字符。

  • 目前本文实现了下面几种格式的输出。
控制符 说明
%d 十进制整型数据
%md m 为指定的输出字段的宽度。如果数据的位数小于 m,则左端补以空格,若大于 m,则按实际位数输出。
%0md m 为指定的输出字段的宽度。如果数据的位数小于 m,则左端补以0,若大于 m,则按实际位数输出。
%x 十六进制整形数据
%c 输出一个字符
%s 输出字符串

%d实现(整形数值转换为十进制字符串)

一个整形数值可以按照下面的代码转换为十进制字符串:

int value= 1234;
	char buff[32];
	char ch;
	buff[31] = 0;
	int index = 30;

	while(value!= 0)
	{
	    ch = value % 10 + '0';    /* 第1步:求余数得到个位并获得数字对应的字符 */
	    value = value / 10;       /* 第2步:丢弃个位 */
	    buff[index] = ch;        /* 保存到缓冲区 */
	    index--;
	}
           

上面代码的

ch = value % 10 + '0';

还可以简单的改为下面的代码:

static const char hex[] = "0123456789ABCDEF";
    ... ...
    ch = hex[value % 10];
           

%x实现(整形数值转换为16进制字符串)

  • 方法一:一个整形数值可以按照下面的代码转换为十六进制字符串:
int value= 1234;
	char buff[32];
	char ch;
	buff[31] = 0;
	int index = 30;

	while(value!= 0)
	{
	    ch = value % 16 + '0';    /* 第1步:求余数并获得数字对应的字符 */
	    value = value / 16;       /* 第2步:丢弃低4位 */
	    buff[index] = ch;        /* 保存到缓冲区 */
	    index--;
	}
           

上面代码的

ch = value % 10 + '0';

还可以简单的改为下面的代码:

static const char hex[] = "0123456789ABCDEF";
    ... ...
    ch = hex[value % 16];
           

转16进制字符串的代码实现和10进制类似,不同的只是求余和除法的10和16;

  • 方法二:数值转16进制字符串的代码等价于下面的代码:
int value= 1234;
	char buff[32];
	char ch;
	buff[31] = 0;
	int index = 30;
	static const char hex[] = "0123456789ABCDEF";

	while(value!= 0)
	{
	    ch = hex[value & 0xf];    	  /* 第1步:求余数并获得数字对应的字符 */
	    value = value >> 4;       	  /* 第2步:右移4位,丢弃低4位 */
	    buff[index] = ch;            /* 保存到缓冲区 */
	    index--;
	}
           

整形数值转不同进制字符串的规律

  • 假设一个

    int

    型的数值需要转换为不同进制类型的字符串,则可以按照下面的步骤:
#define    ARY    10    /* 不同数值表示不同的进制 */
	int value= 1234;
	char buff[32];
	char ch;
	buff[31] = 0;
	int index = 30;
	static const char hex[] = "0123456789ABCDEF";

	while(value!= 0)
	{
	    ch = hex[value % ARY];    	  /* 第1步:求余数并获得数字对应的字符 */
	    value = value / ARY;       	  /* 第2步:除以进制 */
	    buff[index] = ch;            /* 保存到缓冲区 */
	    index--;
	}
           
  • 因此一个数转换为2进制或者8进制的字符串只需要将上面代码的宏定义改为2,8就可以了。

十进制字符串转十进制数值

输出指定宽度的数值时需要将字符串转为10进制数值,

%md

其中的m,可以是字符串123或者10等,实现代码如下所示:

int width = 0;
	char fmt[] = "123";
	char *p = fmt;
	
    while((*p>= '1') && (*p<= '9'))
    {
        width = width * 10 + *p - '0';
        p++;
    } 
    printf("width=%d",width);
           

%c实现(输出一个字符)

输出一个字符的代码实现就很简单了,printf最终也是也是调用字符输出函数将字符串输出。

  • 在windows端可以调用

    putchar()

    函数输出一个字符,调用

    puts()

    输出一个字符串。因为

    puts()

    会默认输出一个换行符,所以可以自己简单实现一个输出字符串的函数。
  • 在MCU端则可以实现串口驱动,并实现输出一个字符及字符串的函数。

迷你 printf 函数整体实现代码

/* 实现迷你printf函数 */
void put_str(char* str)
{
    while(*str != 0)
    {
        putchar(*str);
        str ++;
    }
}

#define BUFF_SIZE    32
int m_printf(const char *fmt,...)
{
    int length = 0;             /* 返回值:打印的字符长度 */
    int index,width;            /* index:缓冲区索引,width:字符输出宽度 */
    int value,temp,stuff;       /* stuff:指定宽度输出0或者空格 */
    unsigned int value_x;       /* %x 输出值为无符号整形 */
    char* chs;
    char buff[BUFF_SIZE] = {0}; /* 缓冲区 */
    static const char hex[] = "0123456789ABCDEF";
    va_list p_args;

    va_start(p_args,fmt);

    while(*fmt != '\0')
    {
        if(*fmt == '%')    /* 格式化输出 */
        {
            fmt++;
            index = 30;
            width = 0;
            /* 指定宽度填充0或者空格并向右靠齐 */
            if(*fmt == '0')
            {
                stuff = '0';
                fmt++;
            }
            else
            {
                stuff = ' ';
            }
            while((*fmt >= '1') && (*fmt <= '9'))
            {
                width = width *10 + *fmt - '0';
                fmt++;
            }
            switch(*fmt)
            {
            case 'c':
                value = va_arg(p_args,int);
                putchar(value);
                fmt++;
                length ++;
                break;
            case 's':
                chs = va_arg(p_args,char*);
                put_str(chs);
                fmt++;
                length += strlen(chs);
                break;
            case 'd':
                value = va_arg(p_args,int);

                temp = value;
                /* 负数变正数 */
                if(value < 0)
                {
                    value = -value;
                }
                /* int 型数字转为十进制字符串 */
                while(value != 0)
                {
                    buff[index] = hex[value % 10];    /* 求余数得到个位并获得数字对应的字符 */
                    value = value / 10;        /* 丢弃个位 */
                    index--;
                    width --;
                }
                /* 负数添加负号 */
                if(temp < 0)
                {
                    buff[index] = '-';
                    index--;
                    width--;
                }

                fmt++;
                break;
            case 'x':
            case 'X':
                value_x = va_arg(p_args,unsigned int);
                /* 数值转16进制字符串 */
                while(value_x != 0)
                {
                    buff[index] = hex[value_x % 16];  /* 求余数得到个位数 */
                    value_x = value_x / 16;             /* 丢弃个位数 */
                    index --;
                    width--;
                }
                buff[index--] = 'x';                /* 添加 0x 前缀 */
                buff[index--] = '0';
                width -= 2;
                fmt++;
                break;
            default:

                break;
            }
            /* 按照指定宽度填充字符0或者空格,width可能为负数 */
            if(width > 0)
            {
                while(width != 0)
                {

                    buff[index] = stuff;
                    index --;
                    width --;
                }
            }

            if(index != 30)    /* 输出格式化字符串 */
            {
                put_str(&buff[++index]);
                length += BUFF_SIZE - 1 - index;
            }
        }
        else    /* 直接输出字符 */
        {
            putchar(*fmt);
            fmt++;
            length ++;
        }
    }

    va_end(p_args);

    return length;
}
/* 函数功能测试 */
void test_m(void)
{
    int r_value = 0;
    m_printf("hello print! %c %s %7d %07d %9x %09x\n",'a',"this",1234,-5678,0x1122,0x5566);

    r_value = m_printf("12345:%s %c %d %6d %x %6x\n","abcdef",'j',1234,567,0x1122,0x3344);
    printf("m r_value:%d\n",r_value);

    r_value = printf("12345:%s %c %d %6d %x %6x\n","abcdef",'j',1234,567,0x1122,0x3344);
    printf("p r_value:%d\n",r_value);
}
           

继续阅读