實現簡單的printf函式
阿新 • • 發佈:2019-02-14
首先,要介紹一下printf實現的原理
printf函式原型如下:
返回值是int,返回輸出的字元個數。
例如:
測試結果:
hello world,100
返回值:16
引數format是一個字元指標,指向printf裡的第一個字串。
引數...是不定引數。這是printf能夠實現的核心。
接下來介紹一下不定引數是如何實現的。
int printf(const char* format,...);
比如我們printf實際輸入的引數有4個,printf(char* format,arg1,arg2,arg3,arg4);
這些引數在記憶體中從低地址到高地址依次為format,arg1,arg2,arg3,arg4。
因為format是指標,所以所佔的位元組大小為一個int的大小。
所以如果我們找到format的儲存地址,從format首地址開始,加上一個int的大小,此時地址剛好就是引數arg1的首地址,然後再加上sizeof(arg1),此時地址又剛好是arg2的首地址,這樣我們就能依次找出引數所在地址。
具體實現時,我們只需要定義一個指標變數ap指向arg1引數的起始地址,同時分析format引數所指的字串,從字串第一個字元開始檢查,如果遇到%則通過分析%後面的字元就能判斷出變數的型別,此時輸出ap地址上所指向的變數的值,同時ap指標向右移動該變數型別大小位元組個單位,使ap指向下一個引數的儲存地址,然後再次分析字串,直到分析到字串結尾結束。
通過上面的引數入棧方式我們可以得到如下結論:
如果想將棧中的引數讀出來,我們只需要知道,棧頂元素的地址即第一個引數的地址即可。通過前面變參函式的分析,通過變參函式第一個引數可以知道傳遞的引數個數。
當然,每個引數都有自己的型別,還有的就是位元組對齊了。在讀取引數的時候,這些問題都必須考慮到。
實際上處理變參時,已經有封裝好的巨集處理這些所有問題
這些巨集在不同的作業系統,有不同的實現,想使用的話,只需要包含標頭檔案stdarg.h就可以了。
(1)va_start巨集的作用 :
printf(const char* format,arg1,agr2,....)
實現ap指向第一個實際引數arg1的地址,實際引數指第一個引數format後的第一個引數arg1。即va_start(ap,format)。
(2)va_arg巨集作用:
t指的是分析出來的實際引數的變數型別,首先ap向後移動sizeof(t)個單位,指向下一個實際引數的地址,同時返回(ap-sizeof(t))的地址,返回的地址跟剛開始時地址一樣。實際上就是為了ap移動到下一個引數的地址,為了下一次輸出。
(3)va_end巨集的作用
將ap指標賦值為NULL,即0
看一下實現程式碼:
執行結果:
ch = A,str = hello world,dec = 1234,flt = 1234.4568
實際上,實現時可以用一個更簡單的函式,vprintf函式。
int vprintf(char *format, va_list param);
printf的功能就是用它來實現的,所不同的是,它用一個引數取代了變長引數表,且此引數是通過呼叫va_start巨集進行初始化。其實vprintf也是經過封裝的一個函式。
這樣就省了我們呼叫巨集對變參函式進行處理,只要開始呼叫一次va_start巨集進行一次初始化即可。
程式碼如下:
執行結果:
hello world,10
printf函式原型如下:
int printf(const char* format,...);
返回值是int,返回輸出的字元個數。
例如:
int main()
{
int n;
n=printf("hello world,%d\n",100);
printf("返回值:%d\n",n);
return 0;
}
測試結果:
hello world,100
返回值:16
測試結果是16,是因為100雖然是整型數,但是輸出時計算返回值它是3個字元。
引數format是一個字元指標,指向printf裡的第一個字串。
引數...是不定引數。這是printf能夠實現的核心。
接下來介紹一下不定引數是如何實現的。
int printf(const char* format,...);
函式的引數由右向左依次入棧,如下圖:
比如我們printf實際輸入的引數有4個,printf(char* format,arg1,arg2,arg3,arg4);
這些引數在記憶體中從低地址到高地址依次為format,arg1,arg2,arg3,arg4。
因為format是指標,所以所佔的位元組大小為一個int的大小。
所以如果我們找到format的儲存地址,從format首地址開始,加上一個int的大小,此時地址剛好就是引數arg1的首地址,然後再加上sizeof(arg1),此時地址又剛好是arg2的首地址,這樣我們就能依次找出引數所在地址。
具體實現時,我們只需要定義一個指標變數ap指向arg1引數的起始地址,同時分析format引數所指的字串,從字串第一個字元開始檢查,如果遇到%則通過分析%後面的字元就能判斷出變數的型別,此時輸出ap地址上所指向的變數的值,同時ap指標向右移動該變數型別大小位元組個單位,使ap指向下一個引數的儲存地址,然後再次分析字串,直到分析到字串結尾結束。
通過上面的引數入棧方式我們可以得到如下結論:
如果想將棧中的引數讀出來,我們只需要知道,棧頂元素的地址即第一個引數的地址即可。通過前面變參函式的分析,通過變參函式第一個引數可以知道傳遞的引數個數。
當然,每個引數都有自己的型別,還有的就是位元組對齊了。在讀取引數的時候,這些問題都必須考慮到。
實際上處理變參時,已經有封裝好的巨集處理這些所有問題
typedef char * va_list; //將char*別名為va_list; #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v)) #define va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) #define va_end(ap) (ap = (va_list)0)
這些巨集在不同的作業系統,有不同的實現,想使用的話,只需要包含標頭檔案stdarg.h就可以了。
(1)va_start巨集的作用 :
printf(const char* format,arg1,agr2,....)
實現ap指向第一個實際引數arg1的地址,實際引數指第一個引數format後的第一個引數arg1。即va_start(ap,format)。
(2)va_arg巨集作用:
t指的是分析出來的實際引數的變數型別,首先ap向後移動sizeof(t)個單位,指向下一個實際引數的地址,同時返回(ap-sizeof(t))的地址,返回的地址跟剛開始時地址一樣。實際上就是為了ap移動到下一個引數的地址,為了下一次輸出。
(3)va_end巨集的作用
將ap指標賦值為NULL,即0
看一下實現程式碼:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<stdarg.h> void printch(const char ch) //輸出字元 { putchar(ch); } void printint(const int dec) //輸出整型數 { if(dec == 0) { return; } printint(dec / 10); putchar((char)(dec % 10 + '0')); } void printstr(const char *ptr) //輸出字串 { while(*ptr) { putchar(*ptr); ptr++; } } void printfloat(const float flt) //輸出浮點數,小數點第5位四捨五入 { int tmpint = (int)flt; int tmpflt = (int)(100000 * (flt - tmpint)); if(tmpflt % 10 >= 5) { tmpflt = tmpflt / 10 + 1; } else { tmpflt = tmpflt / 10; } printint(tmpint); putchar('.'); printint(tmpflt); } void my_printf(const char *format,...) { va_list ap; va_start(ap,format); //將ap指向第一個實際引數的地址 while(*format) { if(*format != '%') { putchar(*format); format++; } else { format++; switch(*format) { case 'c': { char valch = va_arg(ap,int); //記錄當前實踐引數所在地址 printch(valch); format++; break; } case 'd': { int valint = va_arg(ap,int); printint(valint); format++; break; } case 's': { char *valstr = va_arg(ap,char *); printstr(valstr); format++; break; } case 'f': { float valflt = va_arg(ap,double); printfloat(valflt); format++; break; } default: { printch(*format); format++; } } } } va_end(ap); } int main() { char ch = 'A'; char *str = "hello world"; int dec = 1234; float flt = 1234.45678; my_printf("ch = %c,str = %s,dec = %d,flt = %f\n",ch,str,dec,flt); return 0; }
執行結果:
ch = A,str = hello world,dec = 1234,flt = 1234.4568
實際上,實現時可以用一個更簡單的函式,vprintf函式。
int vprintf(char *format, va_list param);
printf的功能就是用它來實現的,所不同的是,它用一個引數取代了變長引數表,且此引數是通過呼叫va_start巨集進行初始化。其實vprintf也是經過封裝的一個函式。
這樣就省了我們呼叫巨集對變參函式進行處理,只要開始呼叫一次va_start巨集進行一次初始化即可。
程式碼如下:
#include<stdio.h>
#include<stdarg.h>
int my_printf(char *str,...)
{
int n; //記錄返回值
va_list list;
va_start(list,str);
n=vprintf(str,list);
va_end(list);
return n;
}
int main()
{
my_printf("%s,%d","hello world",10);
}
執行結果:
hello world,10