printf 函数的实现原理
1 /* 2 * ===================================================================================== 3 * 4 * Filename: printf.c 5 * 6 * Description: printf 函数的实现 7 * 8 * Version: 1.0 9 * Created: 2010年12月12日 14时48分18秒 10 * Revision: none 11 * Compiler: gcc 12 * 13 * Author: Yang Shao Kun (), cdutyangshaokun@163.com 14 * Company: College of Information Engineering of CDUT 15 * 16 * ===================================================================================== 17 */ 18 要了解变参函数的实现,首先我们的弄清楚几个问题: 19 1:该函数有几个参数。 20 2:该函数增样去访问这些参数。 21 3:在访问完成后,如何从堆栈中释放这些参数。 22 对于c语言,它的调用规则遵循_cdedl调用规则。 23 在_cdedl规则中:1.参数从右到左依次入栈 24 2.调用者负责清理堆栈 25 3.参数的数量类型不会导致编译阶段的错误 26 要弄清楚变参函数的原理,我们需要解决上述的3个问题,其中的第三个问题,根据调 27 用原则,那我们现在可以不管。 28 要处理变参函数,需要用到 va_list 类型,和 va_start,va_end,va_arg 宏定义。我 29 看网上的许多资料说这些参数都是定义在stdarg.h这个头文件中,但是在我的linux机 30 器上,我的版本是fedorea 14,用vim访问的时候,确是在 acenv.h这个头文件中,估 31 计是内核的版本不一样的原因吧!!! 32 上面的这几个宏和其中的类型,在内核中是这样来实现的: 33 #ifndef _VALIST 34 #define _VALIST 35 typedef char *va_list; 36 #endif /* _VALIST */ 37 /* 38 * Storage alignment properties 39 */ 40 #define _AUPBND (sizeof (acpi_native_int) - 1) 41 #define _ADNBND (sizeof (acpi_native_int) - 1) 42 /* 43 * Variable argument list macro definitions 44 */ 45 #define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd))) 46 #define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) 47 #define va_end(ap) (void) 0 48 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 49 #endif /* va_arg */ 50 首先来看 va_list 类型,其实这是一个字符指针。 51 va_start,是使ap指针指向变参函数中的下一个参数。 52 我们现在来看_bnd 宏的实现: 53 首先: 54 typedef s32 acpi_native_int; 55 typedef int s32; 56 看出来,acpi_native_int 其实就是 int 类型,那么, 57 #define _AUPBND (sizeof (acpi_native_int) - 1) 58 #define _ADNBND (sizeof (acpi_native_int) - 1) 59 这两个值就应该是相等的,都-等于:3==0x00000003,按位取反后的结果就是:0xfffff 60 ffc,因此, 61 _bnd(x,bnd)宏在32位机下就是 62 (((sizeof (X)) + (3)) & (0xfffffffc)),那么作用就很明显是取4的整数,就相当与 63 整数除法后取ceiling--向上取整。 64 回过头来看 va_start(ap,A),初始化参数指针ap,将函数参数A右边右边第一个参数地 65 址赋值给ap,A必须是一个参数的指针,所以,此种类型函数至少要有一个普通的参数 66 ,从而提供给va_start ,这样va_start才能找到可变参数在栈上的位置。 67 va_arg(ap,T),获得ap指向参数的值,同时使ap指向下一个参数,T用来指名当前参数类 68 型。 69 va_end 在有些简单的实现中不起任何作用,在有些实现中可能会把ap改成无效值,这 70 里,是把ap指针指向了 NULL。 71 c标准要求在同一个函数中va_start 和va_end 要配对的出现。 72 那么到现在,处理多参数函数的步骤就是 73 1:首先是要保证该函数至少有一个参数,同时用...参数申明函数是变参函数。 74 2:在函数内部以va_start(ap,A)宏初始化参数指针。 75 3:用va_arg(ap,T)从左到右逐个取参数值。 76 printf()格式转换的一般形式如下: 77 %[flags][width][.prec][type] 78 prec有一下几种情况: 79 正整数的最小位数 80 在浮点数中表示的小数位数 81 %g格式表示有效为的最大值 82 %s格式表示字符串的最大长度 83 若为*符号表示下个参数值为最大长度 84 width:为输出的最小长度,如果这个输出参数并非数值,而是*符号,则表示以下一个参数当做输出长度。 85 现在来看看我们的printf函数的实现,在内核中printf函数被封装成下面的代码: 86 static char sprint_buf[1024]; 87 int printf(const char *fmt, ...) 88 { 89 va_list args; 90 int n; 91 va_start(args, fmt);//初始化参数指针 92 n = vsprintf(sprint_buf, fmt, args);/*函数放回已经处理的字符串长度*/ 93 va_end(args);//与va_start 配对出现,处理ap指针 94 if (console_ops.write) 95 console_ops.write(sprint_buf, n);/*调用控制台的结构中的write函数,将sprintf_buf中的内容输出n个字节到设备*/ 96 return n; 97 } 98 vs_printf函数的实现代码是: 99 int vsprintf(char *buf, const char *fmt, va_list args) 100 { 101 int len; 102 unsigned long long num; 103 int i, base; 104 char * str; 105 const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/ 106 int flags; /* flags to number() */ 107 int field_width; /* width of output field */ 108 int precision; /* min. # of digits for integers; max 109 number of chars for from string */ 110 int qualifier; /* 'h', 'l', or 'L' for integer fields */ 111 /* 'z' support added 23/7/1999 S.H. */ 112 /* 'z' changed to 'Z' --davidm 1/25/99 */ 113 for (str=buf ; *fmt ; ++fmt) 114 { 115 if (*fmt != '%') /*使指针指向格式控制符'%,以方便以后处理flags'*/ 116 { 117 *str++ = *fmt; 118 continue; 119 } 120 /* process flags */ 121 flags = 0; 122 repeat: 123 ++fmt; /* this also skips first '%'--跳过格式控制符'%' */ 124 switch (*fmt) 125 { 126 case '-': flags |= LEFT; goto repeat;/*左对齐-left justify*/ 127 case '+': flags |= PLUS; goto repeat;/*p plus with ’+‘*/ 128 case ' ': flags |= SPACE; goto repeat;/*p with space*/ 129 case '#': flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/ 130 case '0': flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/ 131 } 132 //#define ZEROPAD 1 /* pad with zero */ 133 //#define SIGN 2 /* unsigned/signed long */ 134 //#define PLUS 4 /* show plus */ 135 //#define SPACE 8 /* space if plus */ 136 //#define LEFT 16 /* left justified */ 137 //#define SPECIAL 32 /* 0x */ 138 //#define LARGE 64 /* use 'ABCDEF' instead of 'abcdef' */ 139 /* get field width ----deal 域宽 取当前参数字段宽度域值,放入field_width 变量中。如果宽度域中是数值则直接取其为宽度值。 如果宽度域中是字符'*',表示下一个参数指定宽度。因此调用va_arg 取宽度值。若此时宽度值小于0,则该负数表示其带有标志域'-'标志(左靠齐),因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值。 */ 140 field_width = -1; 141 if ('0' <= *fmt && *fmt <= '9') 142 field_width = skip_atoi(&fmt); 143 else if (*fmt == '*') 144 { 145 ++fmt;/*skip '*' */ 146 /* it's the next argument */ 147 field_width = va_arg(args, int); 148 if (field_width < 0) { 149 field_width = -field_width; 150 flags |= LEFT; 151 } 152 } 153 /* get the precision-----即是处理.pre 有效位 */ 154 precision = -1; 155 if (*fmt == '.') 156 { 157 ++fmt; 158 if ('0' <= *fmt && *fmt <= '9') 159 precision = skip_atoi(&fmt); 160 else if (*fmt == '*') /*如果精度域中是字符'*',表示下一个参数指定精度。因此调用va_arg 取精度值。若此时宽度值小于0,则将字段精度值取为0。*/ 161 { 162 ++fmt; 163 /* it's the next argument */ 164 precision = va_arg(args, int); 165 } 166 if (precision < 0) 167 precision = 0; 168 } 169 /* get the conversion qualifier 分析长度修饰符,并将其存入qualifer 变量*/ 170 qualifier = -1; 171 if (*fmt == 'l' && *(fmt + 1) == 'l') 172 { 173 qualifier = 'q'; 174 fmt += 2; 175 } 176 else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'|| *fmt == 'Z') 177 { 178 qualifier = *fmt; 179 ++fmt; 180 } 181 /* default base */ 182 base = 10; 183 /*处理type部分*/ 184 switch (*fmt) 185 { 186 case 'c': 187 if (!(flags & LEFT))/*没有左对齐标志,那么填充field_width-1个空格*/ 188 while (--field_width > 0) 189 *str++ = ' '; 190 *str++ = (unsigned char) va_arg(args, int); 191 while (--field_width > 0)/*不是左对齐*/ 192 *str++ = ' ';/*在参数后输出field_width-1个空格*/ 193 continue; 194 /*如果转换参数是s,则,表示对应的参数是字符串,首先取参数字符串的长度,如果超过了精度域值,则取精度域值为最大长度*/ 195 case 's': 196 s = va_arg(args, char *); 197 if (!s) 198 s = ""; 199 len = strnlen(s, precision);/*字符串的长度,最大为precision*/ 200 if (!(flags & LEFT)) 201 while (len < field_width--)/*如果不是左对齐,则左侧补空格=field_width-len个空格*/ 202 *str++ = ' '; 203 for (i = 0; i < len; ++i) 204 *str++ = *s++; 205 while (len < field_width--)/*如果是左对齐,则右侧补空格数=field_width-len*/ 206 *str++ = ' '; 207 continue; 208 /*如果格式转换符是'p',表示对应参数的一个指针类型。此时若该参数没有设置宽度域,则默认宽度为8,并且需要添零。然后调用number()*/ 209 case 'p': 210 if (field_width == -1) 211 { 212 field_width = 2*sizeof(void *); 213 flags |= ZEROPAD; 214 } 215 str = number(str,(unsigned long) va_arg(args, void *), 16, 216 field_width, precision, flags); 217 continue; 218 // 若格式转换指示符是'n',则表示要把到目前为止转换输出的字符数保存到对应参数指针指定的位置中。 219 // 首先利用va_arg()得该参数指针,然后将已经转换好的字符数存入该指针所指的位置 220 case 'n': 221 if (qualifier == 'l') 222 { 223 long * ip = va_arg(args, long *); 224 *ip = (str - buf); 225 } 226 else if (qualifier == 'Z') 227 { 228 size_t * ip = va_arg(args, size_t *); 229 *ip = (str - buf); 230 } 231 else 232 { 233 int * ip = va_arg(args, int *); 234 *ip = (str - buf); 235 } 236 continue; 237 //若格式转换符不是'%',则表示格式字符串有错,直接将一个'%'写入输出串中。 238 // 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中,并返回到继续处理 239 //格式字符串。 240 case '%': 241 *str++ = '%'; 242 continue; 243 /* integer number formats - set up the flags and "break" */ 244 case 'o': 245 base = 8; 246 break; 247 case 'X': 248 flags |= LARGE; 249 case 'x': 250 base = 16; 251 break; 252 // 如果格式转换字符是'd','i'或'u',则表示对应参数是整数,'d', 'i'代表符号整数,因此需要加上 253 // 带符号标志。'u'代表无符号整数 254 case 'd': 255 case 'i': 256 flags |= SIGN; 257 case 'u': 258 break; 259 default: 260 *str++ = '%'; 261 if (*fmt) 262 *str++ = *fmt; 263 else 264 --fmt; 265 continue; 266 } 267 /*处理字符的修饰符,同时如果flags有符号位的话,将参数转变成有符号的数*/ 268 if (qualifier == 'l') 269 { 270 num = va_arg(args, unsigned long); 271 if (flags & SIGN) 272 num = (signed long) num; 273 } 274 else if (qualifier == 'q') 275 { 276 num = va_arg(args, unsigned long long); 277 if (flags & SIGN) 278 num = (signed long long) num; 279 } 280 else if (qualifier == 'Z') 281 { 282 num = va_arg(args, size_t); 283 } 284 else if (qualifier == 'h') 285 { 286 num = (unsigned short) va_arg(args, int); 287 if (flags & SIGN) 288 num = (signed short) num; 289 } 290 else 291 { 292 num = va_arg(args, unsigned int); 293 if (flags & SIGN) 294 num = (signed int) num; 295 } 296 str = number(str, num, base, field_width, precision, flags); 297 } 298 *str = '/0';/*最后在转换好的字符串上加上NULL*/ 299 return str-buf;/*返回转换好的字符串的长度值*/ 300 }
参看该资料:
C中的可变参数研究
一. 何谓可变参数
int printf( const char* format, ...);
这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示). 而我们又可以用各种方式来调用printf,如:
printf("%d",value);
printf("%s",str);
printf("the number is %d ,string is:%s", value, str);
二.实现原理
C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:
typedef char *va_list;
/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
/*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/
#define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v) )
/*va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址*/
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
/*这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/
#define va_end(ap) ( ap = (va_list)0 )
/*x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. */
以下再用图来表示:
在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|——————————————————————————|
|最后一个可变参数 | ->高内存地址处
|——————————————————————————|
...................
|——————————————————————————|
|第N个可变参数 | ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
| | 即第N个可变参数的地址。
|——————————————— |
………………………….
|——————————————————————————|
|第一个可变参数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一个可变参数的地址
|——————————————— |
|———————————————————————— ——|
| |
|最后一个固定参数 | -> start的起始地址
|—————————————— —| .................
|—————————————————————————— |
| |
|——————————————— |-> 低内存地址处
三.printf研究
下面是一个简单的printf函数的实现,参考了中的156页的例子,读者可以结合书上的代码与本文参照。
#include "stdio.h"
#include "stdlib.h"
void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,//参数必须都是int 类型
{
char* pArg=NULL; //等价于原来的va_list
char c;
pArg = (char*) &fmt; //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
pArg += sizeof(fmt); //等价于原来的va_start
do
{
c =*fmt;
if (c != '%')
{
putchar(c); //照原样输出字符
}
else
{
//按格式字符输出数据
switch(*++fmt)
{
case 'd':
printf("%d",*((int*)pArg));
break;
case 'x':
printf("%#x",*((int*)pArg));
break;
default:
break;
}
pArg += sizeof(int); //等价于原来的va_arg
}
++fmt;
}while (*fmt != '\0');
pArg = NULL; //等价于va_end
return;
}
int main(int argc, char* argv[])
{
int i = 1234;
int j = 5678;
myprintf("the first test:i=%d",i,j);
myprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j);
system("pause");
return 0;
}
在intel+win2k+vc6的机器执行结果如下:
the first test:i=1234
the secend test:i=1234; 0xabcd;j=5678;
四.应用
求最大值:
#include //不定数目参数需要的宏
int max(int n,int num,...)
{
va_list x;//说明变量x
va_start(x,num);//x被初始化为指向num后的第一个参数
int m=num;
for(int i=1;i {
//将变量x所指向的int类型的值赋给y,同时使x指向下一个参数
int y=va_arg(x,int);
if(y>m)m=y;
}
va_end(x);//清除变量x
return m;
}
int main()
{
printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533));
return 0;
}