目录
1.可变参数原理
1.1 函数参数入栈原理
1.2 可变参数如何实现?
1.2.1 可变参数实现原理
1.2.2 固定参数有什么用?
1.2.3 va_start,va_arg,va_end如何使用?
2.printf函数实现原理
2.1 printf函数流程
2.2 printf函数格式解析原理
2.2.1 printf函数原型
2.2.2 printf格式解析
3.实现一个简易版printf函数
1.可变参数原理
1.1 函数参数入栈原理
图 1-1 函数参数入栈原理
函数参数采用从右至左的方式入栈,最右的参数从栈顶入栈,以此类推,每个参数都会存储在栈中,形成一个函数参数列表。
可变参数能够实现的根本原因是程序知道如何解析存储在栈中的函数参数列表。
1.2 可变参数如何实现?
1.2.1 可变参数实现原理
可变参数函数原型由固定参数和可变参数组成,如:
int func(fmt,...);
- fmt:固定参数。
- ...:可变参数。
前面我们已经知道函数参数是如何存储在栈中,要实现可变参数,我们需要从栈中还原出可变参数的每一个参数,如何才能从栈中还原出可变参数呢?
这个我们需要用到系统提供的方法:va_start,va_arg,va_end。
typedef char * va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( (void)(ap = (va_list)&v + _INTSIZEOF(v)) )
#define va_arg(ap,t) (*(t *)(ap = ap + _INTSIZEOF(t), ap - _INTSIZEOF(t)))
#define va_end(ap) ( (void)(ap = (va_list)0))
- va_list:va_list实际为char指针,用来记录参数的地址。
- va_start:用于初始化一个va_list类型的变量。它接受两个参数,第一个参数是要初始化的va_list变量,第二个参数是变长参数函数中的最后一个固定参数(实际指向的是...前面的固定参数)。va_start会根据最后一个固定参数的地址来确定可变参数列表的起始位置。
- va_arg:用于访问变长参数列表中的每个参数。它接受两个参数,第一个参数是要访问的va_list变量,第二个参数是要访问的参数的类型,va_arg会返回当前参数的值。
- va_end:将va_list设置成NULL,防止va_list变成野指针。
宏#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))解析?
_INTSIZEOF(n)作用为计算传入参数n在内存中占用的字节数,以4字节对齐。 因为是以4字节对齐,不足4字节的部分依然占用4字节。
举个栗子:
char类型,_INTSIZEOF(char) = ((sizeof(char) + sizeof(int) - 1) & ~(sizeof(int) - 1)) = (1+4 -1) & ~(4 -1) = 0x00000004 & 0xfffffff3= 4;
short类型,_INTSIZEOF(short) = ((sizeof(short) + sizeof(int) - 1) & ~(sizeof(int) - 1)) = (2+4 -1) & ~(4 -1) = 0x00000005 & 0xfffffff3= 4;
int类型,_INTSIZEOF(int) = ((sizeof(int) + sizeof(int) - 1) & ~(sizeof(int) - 1)) = (4+4 -1) & ~(4 -1) = 0x00000007 & 0xfffffff3= 4;
long long int类型,_INTSIZEOF(long long int) = ((sizeof(long long int) + sizeof(int) - 1) & ~(sizeof(int) - 1)) = (8+4 -1) & ~(4 -1) = 0x0000000b & 0xfffffff3= 8;
1.2.2 固定参数有什么用?
固定参数主要有一下两个作用:
(1)定位可变参数的地址。
固定参数是一个实际的参数,我们很容易知道固定参数的地址,因为固定参数和可变参数地址上是连续的,知道固定参数地址后,我们能够通过固定参数地址获取到每个可变参数地址。
(2)定义可变参数遍历规则。
固定参数提供一些关键信息用于遍历可变参数,通常我们用的比较多的有以下情况:
- 情况1:固定参数是一个整型数,该整型数数值代表可变参数个数,我们知道可变参数个数后,就能遍历整个可变参数表,如果不知道可变参数个数,我们无法知道可变参数截止点。
- 情况2:固定参数是一个字符串,我们根据字符串的规定去解析可变参数,printf函数使用的是这种情况。这种情况,固定参数更多的像是正则表达式,我们解析该正则表达式,就能知道可变参数表的实际样貌。
1.2.3 va_start,va_arg,va_end如何使用?
图 1-2 vva_start,va_arg,va_end如何使用
a.使用va_list定义一个指针变量ap。
b.调用va_start(ap, 固定参数1),将va_list指针变量指向固定参数1后的第一个地址。
c.每次调用va_arg(ap, 参数类型),va_arg会将va_list指针变量指向下一个可变参数地址,同时返回当前可变参数的值。
d.调用va_end,将va_list指针变量清零,防止va_list指针变量变成野指针。
2.printf函数实现原理
2.1 printf函数流程
图 2-1 printf函数流程
printf函数称为格式化IO函数,printf函数和普通输出函数(如:fputs,puts等函数)的区别在于多了格式化的操作,格式化就是可变参数解析。
printf函数需要实现两个功能:
- 根据固定参数format完成可变参数解析。
- 解析生成的字符串通过系统调用写入内核并输出到屏幕。
2.2 printf函数格式解析原理
2.2.1 printf函数原型
#include <stdio.h>
int printf(const char *format, ...);
函数参数:
format:固定参数,格式化字符串,用于解析可变参数。
...:可变参数。
返回值:
成功:返回一个整型值,表示成功输出的字符数。
失败:返回一个负数。
2.2.2 printf格式解析
printf函数格式由一个个基本格式“%[标志][宽度][.精度][长度]类型”组成。
[]为可选项。
其他项为必选项。
表 2-1 printf格式
表 2-2 标志表
表 2-3 宽度表
表 2-4 精度表
表 2-5 长度表
表 2-6 类型表
printf通常还需用到个特殊的字符表(转义字符表),如下表:
表 2-7 转义字符表
3.实现一个简易版printf函数
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int my_printf(const char *format, ...) {
va_list ap;
#define BUF_SIZE (1024)
#define STR_SIZE (64)
char buf[BUF_SIZE] = {0};
char c;
char *pos = buf;
va_start(ap, format);
while((c = *format)) {
if (c == '%') {
format++;
c = *format;
switch(c) {
case 'd':
{
int value = va_arg(ap, int);
char str[STR_SIZE] = {0};
sprintf(str, "%d", value);
memcpy(pos, str, strlen(str));
pos += strlen(str);
break;
}
case 'u':
{
unsigned int value = va_arg(ap, unsigned int);
char str[STR_SIZE] = {0};
sprintf(str, "%u", value);
memcpy(pos, str, strlen(str));
pos += strlen(str);
break;
}
case 'o':
{
unsigned int value = va_arg(ap, unsigned int);
char str[STR_SIZE] = {0};
sprintf(str, "%o", value);
memcpy(pos, str, strlen(str));
pos += strlen(str);
break;
}
case 'x':
{
unsigned int value = va_arg(ap, unsigned int);
char str[STR_SIZE] = {0};
sprintf(str, "%x", value);
memcpy(pos, str, strlen(str));
pos += strlen(str);
break;
}
case 'c':
{
char value = va_arg(ap, int);
memcpy(pos, &value, 1);
pos++;
break;
}
break;
case 's':
{
char *value = va_arg(ap, char *);
memcpy(pos, value, strlen(value));
pos += strlen(value);
break;
}
case 'p':
{
long long *value = va_arg(ap, long long *);
char str[STR_SIZE] = {0};
sprintf(str, "%p", value);
memcpy(pos, str, strlen(str));
pos += strlen(str);
break;
}
case '%':
{
memcpy(pos, &c, 1);
pos++;
break;
}
default:
{
printf("format error");
return -1;
}
}
} else {
memcpy(pos, &c, 1);
pos++;
}
format++;
}
write(2, buf, strlen(buf));
return 0;
}
int main(int argc, char *argv[]) {
int num = 0;
my_printf("%d,%u,%o,%x,%c,%s,%p,%%\n", 1234, 1234, 1234, 1234, 'a', "myprintf", &num);
return 0;
}