实现简单的printf函数

本文深入解析printf函数的工作原理,包括其参数处理机制、不定参数的实现方式及内存布局。详细介绍了如何通过va_list、va_start、va_arg和va_end宏来处理变长参数,并提供代码示例。

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

首先,要介绍一下printf实现的原理


printf函数原型如下:

int printf(const char* format,…);


返回值是int,返回输出的字符个数。


例如:
  1. int main()
  2. {
  3. int n;
  4. n=printf(“hello world,%d\n”,100);
  5. printf(“返回值:%d\n”,n);
  6. return 0;
  7. }



测试结果:


hello world,100


返回值:16

测试结果是16,是因为100虽然是整型数,但是输出时计算返回值它是3个字符。



参数format是一个字符指针,指向printf里的第一个字符串。

参数...是不定参数。这是printf能够实现的核心。

接下来介绍一下不定参数是如何实现的。

int printf(const char* format,...);

函数的参数由右向左依次入栈,如下图:



比如我们printf实际输入的参数有4个,printf(char* format,arg1,arg2,arg3,arg4);

这些参数在内存中从低地址到高地址依次为format,arg1,arg2,arg3,arg4。

因为format是指针,所以所占的字节大小为一个int的大小。

所以如果我们找到format的储存地址,从format首地址开始,加上一个int的大小,此时地址刚好就是参数arg1的首地址,然后再加上sizeof(arg1),此时地址又刚好是arg2的首地址,这样我们就能依次找出参数所在地址。

具体实现时,我们只需要定义一个指针变量ap指向arg1参数的起始地址,同时分析format参数所指的字符串,从字符串第一个字符开始检查,如果遇到%则通过分析%后面的字符就能判断出变量的类型,此时输出ap地址上所指向的变量的值,同时ap指针向右移动该变量类型大小字节个单位,使ap指向下一个参数的储存地址,然后再次分析字符串,直到分析到字符串结尾结束。

通过上面的参数入栈方式我们可以得到如下结论:


如果想将栈中的参数读出来,我们只需要知道,栈顶元素的地址即第一个参数的地址即可。通过前面变参函数的分析,通过变参函数第一个参数可以知道传递的参数个数。

当然,每个参数都有自己的类型,还有的就是字节对齐了。在读取参数的时候,这些问题都必须考虑到。

实际上处理变参时,已经有封装好的宏处理这些所有问题

  1. typedef char * va_list; //将char*别名为va_list;
  2. #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
  3. #define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v))
  4. #define va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
  5. #define va_end(ap) (ap = (va_list)0)




这些宏在不同的操作系统,有不同的实现,想使用的话,只需要包含头文件stdarg.h就可以了。

(1)va_start宏的作用 :

printf(const char* format,arg1,agr2,....)

实现ap指向第一个实际参数arg1的地址,实际参数指第一个参数format后的第一个参数arg1。即va_start(ap,format)。

(2)va_arg宏作用:

t指的是分析出来的实际参数的变量类型,首先ap向后移动sizeof(t)个单位,指向下一个实际参数的地址,同时返回(ap-sizeof(t))的地址,返回的地址跟刚开始时地址一样。实际上就是为了ap移动到下一个参数的地址,为了下一次输出。

(3)va_end宏的作用

将ap指针赋值为NULL,即0

看一下实现代码:

  1. #include<stdio.h>
  2. #include<stdlib.h>
  3. #include<string.h>
  4. #include<stdarg.h>
  5. void printch(const char ch) //输出字符
  6. {
  7. putchar(ch);
  8. }
  9. void printint(const int dec) //输出整型数
  10. {
  11. if(dec == 0)
  12. {
  13. return;
  14. }
  15. printint(dec / 10);
  16. putchar((char)(dec % 10 + '0'));
  17. }
  18. void printstr(const char *ptr) //输出字符串
  19. {
  20. while(*ptr)
  21. {
  22. putchar(*ptr);
  23. ptr++;
  24. }
  25. }
  26. void printfloat(const float flt) //输出浮点数,小数点第5位四舍五入
  27. {
  28. int tmpint = (int)flt;
  29. int tmpflt = (int)(100000 * (flt - tmpint));
  30. if(tmpflt % 10 >= 5)
  31. {
  32. tmpflt = tmpflt / 10 + 1;
  33. }
  34. else
  35. {
  36. tmpflt = tmpflt / 10;
  37. }
  38. printint(tmpint);
  39. putchar('.');
  40. printint(tmpflt);
  41. }
  42. void my_printf(const char *format,...)
  43. {
  44. va_list ap;
  45. va_start(ap,format); //将ap指向第一个实际参数的地址
  46. while(*format)
  47. {
  48. if(*format != '%')
  49. {
  50. putchar(*format);
  51. format++;
  52. }
  53. else
  54. {
  55. format++;
  56. switch(*format)
  57. {
  58. case 'c':
  59. {
  60. char valch = va_arg(ap,int); //记录当前实践参数所在地址
  61. printch(valch);
  62. format++;
  63. break;
  64. }
  65. case 'd':
  66. {
  67. int valint = va_arg(ap,int);
  68. printint(valint);
  69. format++;
  70. break;
  71. }
  72. case 's':
  73. {
  74. char *valstr = va_arg(ap,char *);
  75. printstr(valstr);
  76. format++;
  77. break;
  78. }
  79. case 'f':
  80. {
  81. float valflt = va_arg(ap,double);
  82. printfloat(valflt);
  83. format++;
  84. break;
  85. }
  86. default:
  87. {
  88. printch(*format);
  89. format++;
  90. }
  91. }
  92. }
  93. }
  94. va_end(ap);
  95. }
  96. int main()
  97. {
  98. char ch = 'A';
  99. char *str = "hello world";
  100. int dec = 1234;
  101. float flt = 1234.45678;
  102. my_printf("ch = %c,str = %s,dec = %d,flt = %f\n",ch,str,dec,flt);
  103. return 0;
  104. }

 运行结果:

ch = A,str = hello world,dec = 1234,flt = 1234.4568





实际上,实现时可以用一个更简单的函数,vprintf函数。

int vprintf(char *format, va_list param);

printf的功能就是用它来实现的,所不同的是,它用一个参数取代了变长参数表,且此参数是通过调用va_start宏进行初始化。其实vprintf也是经过封装的一个函数。
这样就省了我们调用宏对变参函数进行处理,只要开始调用一次va_start宏进行一次初始化即可。


代码如下:

  1. #include<stdio.h>
  2. #include<stdarg.h>
  3. int my_printf(char *str,...)
  4. {
  5. int n; //记录返回值
  6. va_list list;
  7. va_start(list,str);
  8. n=vprintf(str,list);
  9. va_end(list);
  10. return n;
  11. }
  12. int main()
  13. {
  14. my_printf("%s,%d","hello world",10);
  15. }

运行结果:

hello world,10




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值