自己用C语言实现printf

本文详细解析了C语言中可变参数函数的实现原理,以printf为例,探讨了va_start、va_arg和va_end宏的用途。通过示例代码展示了如何处理可变参数列表,包括字符、整数和字符串的打印。同时,文章还介绍了如何识别和处理不同格式的参数,如%d、%o、%x等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文参考:https://www.pianshen.com/article/35981882012/

前提的头文件:

//==================================================================================================
typedef char* va_list;
#define _INTSIZEOF1(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF1(v) )  
//ap指向fmt后面的地址
//#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_arg(ap,t)    (*(t *)( ap=ap + _INTSIZEOF1(t), ap- _INTSIZEOF1(t)))

#define va_end(ap)      ( ap = (va_list)0 )                                      //这里就不解释了,不难理解

unsigned char hex_tab[] = { '0','1','2','3','4','5','6','7',\
						 '8','9','a','b','c','d','e','f' };         
//==================================================================================================

一、可变参数的函数是怎么传参的?

我们定义:

int printf0(const char* fmt, ...)
{
	va_list ap;
	va_start(ap,fmt)
	my_vprintf(fmt, ap);
	va_end(ap);
	return 0;
}

其中比较难理解的就是

va_start(ap,fmt)
va_arg(ap,t)

这两个函数是什么意思,原文也解释的不太清楚

研究了一下,可变参数的函数可以看成是一串保持参数地址的字符串,占用一片连续的内存,printf可以看成是类似char**,如下图所示:
在这里插入图片描述

在这里插入图片描述

这个图很好地说明了问题,fmt的地址,参数a的地址,参数A的地址(图里的0x0098f914这列地址)是连续的,我们用ap指向了这些地址:

 ( ap = (va_list)&v + _INTSIZEOF1(v) ) 

也就是说ap的内容为0x0098f918,指向了第一个参数a。

假设传入一串参数:

printf0("test=%c,%c,%c\n\r", 'a', 'A','F');

那么fmt地址指向字符串

test=%c,%c,%c\n\r

ap指向字符串

‘a’

如果ap想指向下一个变量A,只需要ap++即可。

那么va_arg是什么意思呢?

#define va_arg(ap,t) (*(t *)( ap=ap + _INTSIZEOF1(t), ap- _INTSIZEOF1(t)))

起初我不理解#define的逗号是怎么用的上网找了不少,但是都是说逗号表达式,并没有详细的解析,那就只能自己把它猜测的拆开单步调试了;

程序里我自己定义了一个**va_arg111(&ap)*函数,就是为了可以还原va_arg的本来面目。由注释和前面的地址图知道ap是一个返回类型为char(va_list)类型的指针,里面保存的内容是printf函数各个变量的首地址,如果,那么va_arg的目的是可以让ap指针指向下一个变量的地址,并且返回当前变量的地址给需要用到的函数。
也就是这个意思:

int va_arg111(va_list * ap)
{

	*ap = *ap +4;
	//printf("ap+4 va_arg is %p\n", *ap);
	return (*(int*)(*ap - 4));
}

我在输出字符的时候做了成功的替换:

case 'c': outc(va_arg111(&ap)); break;

二、const char*是怎么打印的?

我们传入了fmt,fmt的地址为(0x00b07de8)只要++用putc输出即可,注意到这里putc是输入的ascii码对应的十进制值

static int outc(int c)
{
	
	putchar(c);                                        //这里的_out_putchar其实就是putchar,在.h中定义
	return 0;
}

三、遇到参数怎么办?
先识别是不是%,如果不是那么就用for循环照常打印fmt:

如果是,那么fmt++跳过%,用while识别%后的数字参数如%08d(注意这里还没有识别小数点),用switch来识别类型如%d%o。

如果遇到负数,那么将其传递至打印int的函数里,然后将其转换为字符串,在这里需要注意是倒着转换的,先让buf指向最末尾的位置,然后取余数(取低位)。


static int out_num(long n, int base, char lead, int maxwidth)
{
	unsigned long m = 0;
	char buf[256], * s = buf + sizeof(buf);    // sizeof算结束符'\0' ,strlen不算
	int count = 0, i = 0;				//注意这里s指向buf的末端,至于为什么继续往下看	


	*--s = '\0';                               //先--,在赋值结束符,因为sizeof算结束符在内的长度

	if (n < 0) {
		m = -n;                        //如果是输出的是负数就取反
	}
	else {
		m = n;
	}

	do {
		*--s = hex_tab[m % base];
		count++;
	} while ((m /= base) != 0);       //将要打印的数字从个位开始一位一位存储在数组buf中,如果上面不是指向buf末端,
	if (n < 0)
		*--s = '-';    //负数的话加负号

	return outs(s);
}

之后,利用va_arg函数再将ap先指向下一个变量再返回ap-4(当前变量的地址),根据类型分类判断打印出来;

for (; *fmt != '\0'; fmt++)
	{
		if (*fmt != '%') {             //顺序查找判断,遇到%就推出,否则继续循环输出
			outc(*fmt);
			continue;
		}

		fmt++;
		if (*fmt == '0') {                   //遇到‘0’说明前导码是0
			lead = '0';
			fmt++;
		}



		while (*fmt >= '0' && *fmt <= '9') {		 //紧接着的数字是长度,算出指定长度
			maxwidth *= 10;
			maxwidth += (*fmt - '0');
			fmt++;
		}
		
		switch (*fmt) {                                  //判断格式输出
		case 'd': out_num(va_arg(ap, int), 10, lead, maxwidth); break;
		case 'o': out_num(va_arg(ap, unsigned int), 8, lead, maxwidth); break;
		case 'u': out_num(va_arg(ap, unsigned int), 10, lead, maxwidth); break;
		case 'x': out_num(va_arg(ap, unsigned int), 16, lead, maxwidth); break;
		case 'c': outc(va_arg111(&ap)); break;

		//case 'c': outc(va_arg(ap, int)); break;
		case 's': outs(va_arg(ap, char*)); break;

		default:
			outc(*fmt);
			break;
		}
	}

其余的略

以下是全部代码:

#include<stdio.h>



//==================================================================================================
typedef char* va_list;
#define _INTSIZEOF1(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF1(v) )   //ap指向fmt后面的地址
//#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_arg(ap,t)    ( *(t *)                           ( ap=ap + _INTSIZEOF1(t), ap- _INTSIZEOF1(t)     )             )

//#define va_arg(ap,t)    ( *(t *) ap )
#define va_end(ap)      ( ap = (va_list)0 )                                      //这里就不解释了,不难理解
//==================================================================================================
unsigned char hex_tab[] = { '0','1','2','3','4','5','6','7',\
						 '8','9','a','b','c','d','e','f' };               //输出各种进制下的字符

static int outc(int c)
{
	
	putchar(c);                                        //这里的_out_putchar其实就是putchar,在.h中定义
	return 0;
}

static int outs(const char* s)                                 //输出字符串
{
	while (*s != '\0')
		putchar(*s++);
	return 0;
}

static int out_num(long n, int base, char lead, int maxwidth)
{
	unsigned long m = 0;
	char buf[256], * s = buf + sizeof(buf);    // sizeof算结束符'\0' ,strlen不算
	int count = 0, i = 0;				//注意这里s指向buf的末端,至于为什么继续往下看	


	*--s = '\0';                               //先--,在赋值结束符,因为sizeof算结束符在内的长度

	if (n < 0) {
		m = -n;                        //如果是输出的是负数就取反
	}
	else {
		m = n;
	}

	do {
		*--s = hex_tab[m % base];
		count++;
	} while ((m /= base) != 0);       //将要打印的数字从个位开始一位一位存储在数组buf中,如果上面不是指向buf末端,
	if (n < 0)
		*--s = '-';    //负数的话加负号

	return outs(s);
}
//#define va_arg(ap,t)    ( *(t *) ( ap=ap + _INTSIZEOF1(t), ap- _INTSIZEOF1(t)     )  
int va_arg111(va_list * ap)
{

	*ap = *ap +4;
	//printf("ap+4 va_arg is %p\n", *ap);
	return (*(int*)(*ap - 4));
}

/*reference :   int vprintf(const char *format, va_list ap); */
static int my_vprintf(const char* fmt, va_list ap)
{
	char lead = ' ';
	int  maxwidth = 0;

	for (; *fmt != '\0'; fmt++)
	{
		if (*fmt != '%') {             //顺序查找判断,遇到%就推出,否则继续循环输出
			outc(*fmt);
			continue;
		}

		fmt++;
		if (*fmt == '0') {                   //遇到‘0’说明前导码是0
			lead = '0';
			fmt++;
		}



		while (*fmt >= '0' && *fmt <= '9') {		 //紧接着的数字是长度,算出指定长度
			maxwidth *= 10;
			maxwidth += (*fmt - '0');
			fmt++;
		}
		
		switch (*fmt) {                                  //判断格式输出
		case 'd': out_num(va_arg(ap, int), 10, lead, maxwidth); break;
		case 'o': out_num(va_arg(ap, unsigned int), 8, lead, maxwidth); break;
		case 'u': out_num(va_arg(ap, unsigned int), 10, lead, maxwidth); break;
		case 'x': out_num(va_arg(ap, unsigned int), 16, lead, maxwidth); break;
		case 'c': outc(va_arg111(&ap)); break;

		//case 'c': outc(va_arg(ap, int)); break;
		case 's': outs(va_arg(ap, char*)); break;

		default:
			outc(*fmt);
			break;
		}
	}
	return 0;
}


//reference :  int printf(const char *format, ...); 
int printf0(const char* fmt, ...)
{
	va_list ap;
		
	//va_start(ap, fmt);
	ap = (va_list)&fmt;
	//printf("ap add: %p\n", ap);
	//printf("fmt : %p\n", fmt);
	//printf("&fmt : %p\n", &fmt);
	ap = (va_list)&fmt + _INTSIZEOF1(fmt);
	//printf("*ap : %c\n", *ap);

	
	
	//printf("ap add: %p\n", &ap);
	//printf("ap : %c\n", ap);
	my_vprintf(fmt, ap);
	va_end(ap);
	return 0;
}


int my_printf_test(void)
{
	printf("%d\n", sizeof(char*));
	//printf0("My_printf test\n\r");
	printf0("test=%c,%c,%c\n\r", 'A', 'a','F');
	printf0("test decimal number =%d\n\r", 123456);
	printf0("test decimal number =%d\n\r", -123456);
	printf0("test hex     number =0x%x\n\r", 0x55aa55aa);
	printf0("test string         =%s\n\r", "yoyoyo");
	printf0("num=%08d\n\r", 12345);
	printf0("num=%8d\n\r", 12345);
	printf0("num=0x%08x\n\r", 0x12345);
	printf0("num=0x%8x\n\r", 0x12345);
	printf0("num=0x%02x\n\r", 0x1);
	printf0("num=0x%2x\n\r", 0x1);

	printf0("num=%05d\n\r", 0x1);
	printf0("num=%5d\n\r", 0x1);

	return 0;
}

int main()
{
	my_printf_test();
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值