从printf谈可变参数函数的实现
作者:戎亞新
摘要:一直以來都覺得printf似乎是c語言庫中功能最強(qiáng)大的函數(shù)之一,不僅因?yàn)樗芨袷交敵?#xff0c;更在于它的參數(shù)個(gè)數(shù)沒有限制,要幾個(gè)就給幾個(gè),來者不拒。printf這種對(duì)參數(shù)個(gè)數(shù)和參數(shù)類型的強(qiáng)大適應(yīng)性,讓人產(chǎn)生了對(duì)它進(jìn)行探索的濃厚興趣。
關(guān)鍵字:printf, 可變參數(shù)
1. 使用情形
int a =10; double b = 20.0; char *str = "Hello world"; printf("begin print\n"); printf("a=%d, b=%.3f, str=%s\n", a, b, str); ... 從printf的使用情況來看,我們不難發(fā)現(xiàn)一個(gè)規(guī)律,就是無論其可變的參數(shù)有多少個(gè),printf的第一個(gè)參數(shù)總是一個(gè)字符串。而正是這第一個(gè)參數(shù),使得它可以確認(rèn)后面還有有多少個(gè)參數(shù)尾隨。而尾隨的每個(gè)參數(shù)占用的棧空間大小又是通過第一個(gè)格式字符串確定的。然而printf到底是怎樣取第一個(gè)參數(shù)后面的參數(shù)值的呢,請(qǐng)看如下代碼2. printf 函數(shù)的實(shí)現(xiàn)
//acenv.h typedef char *va_list;#define _AUPBND (sizeof (acpi_native_int) - 1) #define _ADNBND (sizeof (acpi_native_int) - 1)#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd))) #define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) #define va_end(ap) (void) 0 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))//start.c static char sprint_buf[1024]; int printf(char *fmt, ...) {va_list args;int n;va_start(args, fmt);n = vsprintf(sprint_buf, fmt, args);va_end(args);write(stdout, sprint_buf, n);return n; }//unistd.h static inline long write(int fd, const char *buf, off_t count) {return sys_write(fd, buf, count); } 3. 分析從上面的代碼來看,printf似乎并不復(fù)雜,它通過一個(gè)宏va_start把所有的可變參數(shù)放到了由args指向的一塊內(nèi)存中,然后再調(diào)用vsprintf. 真正的參數(shù)個(gè)數(shù)以及格式的確定是在vsprintf搞定的了。由于vsprintf的代碼比較復(fù)雜,也不是我們這里要討論的重點(diǎn),所以下面就不再列出了。我們這里要討論的重點(diǎn)是va_start(ap, A)宏的實(shí)現(xiàn),它對(duì)定位從參數(shù)A后面的參數(shù)有重大的制導(dǎo)意義。現(xiàn)在把 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 的含義解釋一下如下:
va_start(ap, A){char *ap = ((char *)(&A)) + sizeof(A)并int類型大小地址對(duì)齊} 在printf的va_start(args, fmt)中,fmt的類型為char *, 因此對(duì)于一個(gè)32為系統(tǒng) sizeof(char *) = 4, 如果int大小也是32,則va_start(args, fmt);相當(dāng)于 char *args = (char *)(&fmt) + 4; 此時(shí)args的值正好為fmt后第一個(gè)參數(shù)的地址。對(duì)于如下的可變參數(shù)函數(shù) void fun(double d,...){va_list args;int n;va_start(args, d);} 則 va_start(args, d);相當(dāng)于 char *args = (char *)&d + sizeof(double); 此時(shí)args正好指向d后面的第一個(gè)參數(shù)。可變參數(shù)函數(shù)的實(shí)現(xiàn)與函數(shù)調(diào)用的棧結(jié)構(gòu)有關(guān),正常情況下c/c++的函數(shù)參數(shù)入棧規(guī)則為__stdcall, 它是從右到左的,即函數(shù)中的最右邊的參數(shù)最先入棧。對(duì)于函數(shù)
void fun(int a, int b, int c){int d;...} 其棧結(jié)構(gòu)為 0x1ffc-->d0x2000-->a0x2004-->b0x2008-->c 對(duì)于任何編譯器,每個(gè)棧單元的大小都是sizeof(int), 而函數(shù)的每個(gè)參數(shù)都至少要占一個(gè)棧單元大小,如函數(shù) void fun1(char a, int b, double c, short d) 對(duì)一個(gè)32的系統(tǒng)其棧的結(jié)構(gòu)就是 0x1ffc-->a (4字節(jié))0x2000-->b (4字節(jié))0x2004-->c (8字節(jié))0x200c-->d (4字節(jié)) 對(duì)于函數(shù)void fun1(char a, int b, double c, short d)如果知道了參數(shù)a的地址,則要取后續(xù)參數(shù)的值則可以通過a的地址計(jì)算a后面參數(shù)的地址,然后取對(duì)應(yīng)的值,而后面參數(shù)的個(gè)數(shù)可以直接由變量a指定,當(dāng)然也可以像printf一樣根據(jù)第一個(gè)參數(shù)中的%模式個(gè)數(shù)來決定后續(xù)參數(shù)的個(gè)數(shù)和類型。如果參數(shù)的個(gè)數(shù)由第一個(gè)參數(shù)a直接決定,則后續(xù)參數(shù)的類型如果沒有變化并且是已知的,則我們可以這樣來取后續(xù)參數(shù), 假定后續(xù)參數(shù)的類型都是double;
void fun1(int num, ...) {double *p = (double *)((&num)+1);double Param1 = *p;double Param2 = *(p+1);...double Paramn *(p+num); } 如果后續(xù)參數(shù)的類型是變化而且是未知的,則必須通過一個(gè)參數(shù)中設(shè)定模式來匹配后續(xù)參數(shù)的個(gè)數(shù)和類型,就像printf一樣,當(dāng)然我們可以定義自己的模式,如可以用i表示int參數(shù),d表示double參數(shù),為了簡(jiǎn)單,我們用一個(gè)字符表示一個(gè)參數(shù),并由該字符的名稱決定參數(shù)的類型而字符的出現(xiàn)的順序也表示后續(xù)參數(shù)的順序。 我們可以這樣定義字符和參數(shù)類型的映射表, i---int s---signed short l---long c---char "ild"模式用于表示后續(xù)有三個(gè)參數(shù),按順序分別為int, long, double類型的三個(gè)參數(shù)那么這樣我們可以定義自己版本的printf 如下 void printf(char *fmt, ...) {char s[80] = "";int paramCount = strlen(fmt);write(stdout, "paramCount = " , strlen(paramCount = ));itoa(paramCount,s,10);write(stdout, s, strlen(s));char *p = (char *)(&fmt) + sizeof(char *);int *pi = (int *)p;for (int i=0; i<paramCount; i++){char line[80] = "";strcpy(line, "param");itoa(i+1, s, 10);strcat(line, s);strcat(line, "=");switch(fmt[i]){case 'i':case 's':itoa((*pi),s,10);strcat(line, s);pi++;break;case 'c':{int len = strlen(line);line[len] = (char)(*pi);line[len+1] = '\0';}break;case 'l':ltoa((*(long *)pi),s,10);strcat(line, s);pi++;break;default:break;}} } 也可以這樣定義我們的Max函數(shù),它返回多個(gè)輸入整型參數(shù)的最大值 int Max(int n, ...) {int *p = &n + 1;int ret = *p;for (int i=0; i<n; i++){if (ret < *(p + i))ret = *(p + i);}return ret; } 可以這樣調(diào)用, 后續(xù)參數(shù)的個(gè)數(shù)由第一個(gè)參數(shù)指定 int m = Max(3, 45, 12, 56); int m = Max(1, 3); int m = Max(2, 23, 45);int first = 34, second = 45, third=5; int m = Max(5, first, second, third, 100, 4); 結(jié)論對(duì)于可變參數(shù)函數(shù)的調(diào)用有一點(diǎn)需要注意,實(shí)際的可變參數(shù)的個(gè)數(shù)必須比前面模式指定的個(gè)數(shù)要多,或者不小于, 也即后續(xù)參數(shù)多一點(diǎn)不要緊,但不能少, 如果少了則會(huì)訪問到函數(shù)參數(shù)以外的堆棧區(qū)域,這可能會(huì)把程序搞崩掉。前面模式的類型和后面實(shí)際參數(shù)的類型不匹配也有可能造成把程序搞崩潰,只要模式指定的數(shù)據(jù)長度大于后續(xù)參數(shù)長度,則這種情況就會(huì)發(fā)生。如:
printf("%.3f, %.3f, %.6e", 1, 2, 3, 4); 參數(shù)1,2,3,4的默認(rèn)類型為整型,而模式指定的需要為double型,其數(shù)據(jù)長度比int大,這種情況就有可能訪問函數(shù)參數(shù)堆棧以外的區(qū)域,從而造成危險(xiǎn)。但是printf("%d, %d, %d", 1.0, 20., 3.0);這種情況雖然結(jié)果可能不正確,但是確不會(huì)造成災(zāi)難性后果。因?yàn)閷?shí)際指定的參數(shù)長度比要求的參數(shù)長度長,堆棧不會(huì)越界。總結(jié)
以上是生活随笔為你收集整理的从printf谈可变参数函数的实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么做微信答题小程序
- 下一篇: php 中文获取拼音,php获取中文的拼