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 }
View Code

參看該資料:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM