printf函數:
棧是從內存的高地址向低地址生長的,函數參數壓棧順序是從右到左,printf的第一個參數就是那個字符指針即為被雙引號括起來的那一部分,函數通過判斷字符串里控制參數的個數(%5.4lf等等)來判斷參數個數及數據類型。例如printf("%d,%d",a,b);匯編代碼為:
.section .data string out = "%d,%d" push b push a push $out call printf
參數是最后的先壓入棧中,最先的后壓入棧中,參數控制的那個字符串常量是最后被壓入的
幾個宏:
C中變長實參頭文件stdarg.h提供了一個數據類型va_list和三個宏(va_start、va_arg和va_end),用它們在被調用函數不知道參數個數和類型時對可變參數表進行測試,從而為訪問可變參數提供了方便且有效的方法。va_list是一個char類型的指針,當被調用函數使用一個可變參數時,它聲明一個類型為va_list的變量,該變量用來指向va_arg和va_end所需信息的位置。下面給出va_list在C中的源碼:
typedef char * va_list;
void va_start(va-list ap,lastfix)是一個宏,它使va_list類型變量ap指向被傳遞給函數的可變參數表中的第一個參數,在第一次調用va_arg和va_end之前,必須首先調用該宏。va-start的第二個參數lastfix是傳遞給被調用函數的最后一個固定參數的標識符。
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //得到可變參數中第一個參數的首地址
type va_arg(va_list ap,type)也是一個宏,其有雙重目的,第一個是返回ap所指對象的值,第二個是修改參數指針ap使其增加以指向表中下一個參數。va_arg的第二個參數提供了修改參數指針所必需的信息。在第一次使用va_arg時,它返回可變參數表中的第一個參數,后續的調用都返回表中的下一個參數,下面給出va_arg在C中的源碼:
#define va_arg(ap,type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) //將參數轉換成需要的類型,並使ap指向下一個參數
注意:type如果依次為依次為char型、char * 型、int型和float型時,在va-arg中它們的類型則應分別為int、char *、int和double.(對齊原則)
void va-end(va-list ap)也是一個宏,該宏用於被調用函數完成正常返回,功能就是把指針ap賦值為0,使它不指向內存的變量。下面給出va_end在C中的源碼:
#define va_end(ap) ( ap = (va_list)0 )
va-end必須在va-arg讀完所有參數后再調用,否則會產生意想不到的后果。
例子:
#include<iostream> using namespace std; #include<stdarg.h> int sum(int n,...) { int i , sum = 0; va_list vap; va_start(vap , n); //指向可變參數表中的第一個參數 for(i = 0 ; i < n ; ++i) sum += va_arg(vap , int); //取出可變參數表中的參數,並修改參數指針vap使其增加以指向表中下一個參數 va_end(vap); //把指針vap賦值為0 return sum; } int main(void) { int m = sum(3 , 45 , 89 , 72); cout<<m<<endl; return 0; }
注意:
int f(...) { ...... ...... ...... }
不能這樣定義,因為這是ANSI C 所要求的,至少得定義一個固定參數。這個參數將被傳遞給va_start(),然后用va_arg()和va_end()來確定所有實際調用時可變長參數的類型和值
例子:
#include<stdio.h> #include<stdarg.h> void myitoa(int n, char str[], int radix) //將整數表達成字符形態 { int i , j , remain; char tmp; i = 0; do //例如n = 25,radix = 10(表達成10進制), { remain = n % radix; if(remain > 9) str[i] = remain - 10 + 'A'; //為了十六進制,10將表示成A else str[i] = remain + '0'; //將整數+'0' = 整數對應的ASCII碼 i++; }while(n /= radix); str[i] = '\0'; for(i-- , j = 0 ; j <= i ; j++ , i--) //25%10 = 5,25/10 = 2,2%10 = 2,2/10 = 0,所以str中結果是倒置的,翻轉一下 { tmp = str[j]; str[j] = str[i]; str[i] = tmp; } } void myprintf(const char *format, ...) { char c, ch, str[30]; va_list ap; va_start(ap, format); while((c = *format)) { switch(c) { case '%': ch = *++format; switch(ch) { case 'd': //%d { int n = va_arg(ap, int); //表明可變參數中有整數,輸出 myitoa(n, str, 10); fputs(str, stdout); break; } case 'x': //%x十六進制輸出整數 { int n = va_arg(ap, int); myitoa(n, str, 16); fputs(str, stdout); break; } case 'f': { double f = va_arg(ap, double); //%f,輸出浮點數 int n; n = f; myitoa(n, str, 10); //把浮點數拆分成整數和小數部分,然后小數部分乘上1e6成整數再輸出 fputs(str, stdout); putchar('.'); n = (f - n) * 1000000; myitoa(n, str, 10); fputs(str, stdout); break; } case 'c': { putchar(va_arg(ap, int)); break; } case 's': { char *p = va_arg(ap, char *); //輸出字符串 fputs(p, stdout); break; } case '%': //%% 輸出% { putchar('%'); break; } default: { fputs("format invalid!", stdout); break; } } break; default: putchar(c); break; } format++; } va_end(ap); } int main(void) { myprintf("%d, %x, %f, %c, %s, %%,%a\n", 10, 15, 3.14, 'B', "hello"); return 0; }
超級大栗子:
//沒有輸出%lf和%f格式 //\r和\n在console_putc_color中定義 #include "console.h" #include "string.h" #include "vargs.h" #include "debug.h" static int vsprintf(char *buff, const char *format, va_list args); void printk(const char *format, ...) { // 避免頻繁創建臨時變量,內核的棧很寶貴 static char buff[1024]; va_list args; int i; va_start(args, format); i = vsprintf(buff, format, args); va_end(args); buff[i] = '\0'; console_write(buff); } void printk_color(real_color_t back, real_color_t fore, const char *format, ...) { // 避免頻繁創建臨時變量,內核的棧很寶貴 static char buff[1024]; va_list args; int i; va_start(args, format); i = vsprintf(buff, format, args); va_end(args); buff[i] = '\0'; console_write_color(buff, back, fore); } #define is_digit(c) ((c) >= '0' && (c) <= '9') static int skip_atoi(const char **s) //把字符表述的整型數字轉化成真正的整型 { int i = 0; while (is_digit(**s)) { i = i * 10 + *((*s)++) - '0'; } return i; } #define ZEROPAD 1 // pad with zero用0填補 ‘%04d’ 4位寬度,不夠前面補零 #define SIGN 2 // unsigned/signed long ‘%d和%i , %u(有符號)’ #define PLUS 4 // show plus ‘+’ #define SPACE 8 // space if plus ‘ ’ #define LEFT 16 // left justified ‘-’,左對齊即數據先輸出,不夠寬度用指定符號補齊 #define SPECIAL 32 // 0x ‘#’ #define SMALL 64 // use 'abcdef' instead of 'ABCDEF' //注意此處數字設計,1 = 0000001,2 = 0000010, 4 = 0000100 ,8 = 0001000... //6位中每一位對應一種格式,有則為1 #define do_div(n,base) ({ \ int __res; \ __asm__("divl %4":"=a" (n),"=d" (__res):"0" (n),"1" (0),"r" (base)); \ //(ax)/ (%4) = n/base __res; }) //返回商_res,divl長字4字節,剛好符合下面的int //32位相除,商在EAX,余數在EDX,#define a ({1;2;3;})奇怪的表達調用a ,即為3,同理調用do_div,即為_res值 static char *number(char *str, int num, int base, int size/*field_width*/, int precision, int type/*flag*/) { char c, sign, tmp[36]; const char *digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; int i; if (type & SMALL) { //用小寫 digits ="0123456789abcdefghijklmnopqrstuvwxyz"; } if (type & LEFT) { //如果flag既有左對齊,又有用0來補,此處指明數位不夠,則用左對齊規則(用空格補),不用0補 type &= ~ZEROPAD; } if (base < 2 || base > 36) { return 0; }//base = 2, 10, 8,16 c = (type & ZEROPAD) ? '0' : ' ' ;//如果flag既有左對齊,又有用0來補,上面吧ZEROPAD刪去了,所以用空格 if (type & SIGN && num < 0) {// + 輸出符號(正號或負號) sign = '-'; // 空格 輸出值為正時冠以空格,為負時冠以負號 num = -num; // } else { sign = (type&PLUS) ? '+' : ((type&SPACE) ? ' ' : 0);//num可能為0和正數,有PLUS那么輸出帶+,沒有PLUS有空格輸出帶空格,沒有空格那就是為0 } if (sign) { //有正負號,占去一位寬度 size--; } if (type & SPECIAL) { //有SPECIAL if (base == 16) { size -= 2; //輸出前綴0x占去兩位 } else if (base == 8) {//輸出前綴o size--; } } i = 0; if (num == 0) { tmp[i++] = '0'; } else { while (num != 0) { tmp[i++] = digits[do_div(num,base)];//假如num為25,tmp從0開始存着‘5’,‘2’ } } if (i > precision) {//精度限制,主要用於小數點后幾位(雖然沒有) precision = i; } size -= precision;//如果表達的是帶小數的整數,size剩下為整數部分位數 if (!(type&(ZEROPAD+LEFT))) {//ZEROPAD和LEFT都沒有 while (size-- > 0) { *str++ = ' '; } } if (sign) { //帶符號的附上正負號 *str++ = sign; } if (type & SPECIAL) { if (base == 8) {//八進制帶上0 *str++ = '0'; } else if (base == 16) {//十六進制帶上0x *str++ = '0'; *str++ = digits[33]; } } if (!(type&LEFT)) { //沒有LEFT while (size-- > 0) { *str++ = c; } } while (i < precision--) {//用0補齊到指定寬度 *str++ = '0'; } while (i-- > 0) { //反着到給str,num25,10進制,tmp中存‘5’‘2’,現在str中變成25 *str++ = tmp[i]; } while (size-- > 0) {//用空格補齊到指定寬度 *str++ = ' '; } return str; } static int vsprintf(char *buff, const char *format, va_list args) { int len; int i; char *str; char *s; int *ip; int flags; // flags to number() int field_width; // width of output field 輸出結果寬度 int precision; // min. # of digits for integers; max number of chars for from string //輸出精度,確定小數點后多少位(雖然沒有)和字符串長度 for (str = buff ; *format ; ++format) { if (*format != '%') { *str++ = *format; continue; } flags = 0; //*format = '%' repeat: ++format; // this also skips first '%' ++format跳過'%' switch (*format) { case '-': flags |= LEFT; //輸出寬度為4,數據為16 ,輸出結果為“16 ”(補上倆空格) goto repeat; case '+': flags |= PLUS; goo repeat; case ' ': flags |= SPACE; goto repeat; case '#': flags |= SPECIAL; goto repeat; case '0': flags |= ZEROPAD; goto repeat; } // - 結果左對齊,右邊填空格 // + 輸出符號(正號或負號) // 空格 輸出值為正時冠以空格,為負時冠以負號 // # 對c、s、d、u類無影響; // 對o類,在輸出時加前綴o; // 對x類,在輸出時加前綴0x; // 對e、g、f 類當結果有小數時才給出小數點。 // 0 printf("%04d", 16);輸出數據0016,寬度為4 // get field width field_width = -1; // if (is_digit(*format)) { //例如%15d,指定輸出寬度為15,用空格來補 field_width = skip_atoi(&format); //例如%010,skip_atoi返回10,定義輸出數據寬度 } else if (*format == '*') { //例如printf("%*d", 4, 16); 指定輸出寬度為4,不夠用空格補 // it's the next argument field_width = va_arg(args, int); if (field_width < 0) { //如果printf("%*d", -7, 16);那么那個負號就相當於指定左對齊'-',然后7表示輸出寬度為7,用空格補 field_width = -field_width; flags |= LEFT; } } // get the precision precision = -1; //此處的precision主要是用於字符串,不用於小數點后幾位,因為沒有 if (*format == '.') { //%5.4lf指定輸出寬度為5,精度為4,如果數據實際長度超過5(123.1234567) ++format; //故應該按實際位數輸出,小數位數超過4位部分被截去“123.1234” if (is_digit(*format)) { precision = skip_atoi(&format); } else if (*format == '*') { //根據傳入實參指定精度 // it's the next argument precision = va_arg(args, int); } if (precision < 0) { precision = 0; } } // get the conversion qualifier //int qualifier = -1; // 'h', 'l', or 'L' for integer fields if (*format == 'h' || *format == 'l' || *format == 'L') { // %ld 表示輸出long整數 //qualifier = *format; // %lf 表示輸出double浮點數 ++format; } switch (*format) { case 'c': //字符 if (!(flags & LEFT)) { //沒有LEFT,最后輸出數據 while (--field_width > 0) { *str++ = ' '; } } *str++ = (unsigned char) va_arg(args, int); while (--field_width > 0) { //有LEFT,無需else,因為如果有LEFT,上面已將field_width減成0 *str++ = ' '; } break; case 's': //字符串 s = va_arg(args, char *); len = strlen(s); //根據精度來確定輸出字符串長度 if (precision < 0) { precision = len; } else if (len > precision) { len = precision; } if (!(flags & LEFT)) { //沒有LEFT,最后輸出數據 while (len < field_width--) { *str++ = ' '; } } for (i = 0; i < len; ++i) { *str++ = *s++; } while (len < field_width--) {//補齊到寬度 *str++ = ' '; } break; case 'o': //八進制整數 str = number(str, va_arg(args, unsigned long), 8, field_width, precision, flags); break; case 'p': //%p輸出指針的值 if (field_width == -1) { field_width = 8; flags |= ZEROPAD; } str = number(str, (unsigned long) va_arg(args, void *), 16, field_width, precision, flags); break; case 'x': //%x, %X無符號以十六進制表示的整數,%x:16進制中為:abcdef,%x:ABCDEF flags |= SMALL; //沒有break呦!!! case 'X': str = number(str, va_arg(args, unsigned long), 16, field_width, precision, flags); break; case 'd': case 'i': flags |= SIGN; //%d,%i加上十進制符號整數 case 'u': // %u十進制無符號整數 str = number(str, va_arg(args, unsigned long), 10, field_width, precision, flags); break; case 'b': //實際上printf不提供輸出二進制 str = number(str, va_arg(args, unsigned long), 2, field_width, precision, flags); break; case 'n': ip = va_arg(args, int *); *ip = (str - buff); //記錄輸出的數據長度??? break; default: if (*format != '%') *str++ = '%'; if (*format) { *str++ = *format; //屁精屁精的,比如像%%,第一個if不進,進第二個加入%,其他的%w,w != %,str加入%,w進第二個if,str加入w } else { //沒想到特殊情況=_= --format; } break; } } *str = '\0'; return (str - buff); //輸出結果長度 }
注釋已經加上,慢慢品味內核中的printk函數 (T 。T)!!!
Done!!!
引用: