
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;
}