一、 在談論這問題前我們先要了解什麼是可變參數:
int printf( const char* format, ...);
printf知道為什麼叫這個名字么?原來f就是format。所以命名也是很講究的。
然後Win下有有好幾種不同的調用約定,比如__stdcall,__pascal,__cdecl。
在Windows下__stdcall,__pascal是一樣的,一般用於固定參數的函式,
不過效率不高,所以除非一定要用,一般都採用上一種。
這是使用過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平台下的宏定義:
VC2005在這些東西的定義前有這個東西“#elif defined(_M_IX86) //。。。”
覺得M代表Machine,IX86大概是Intel X86的意思吧。
然後這些東西被放到了vadefs.h中,名字前都加了個_crt_,比如va_strat就變成了_crt_va_start
最後在stdarg.h中加入了下面的東西來滿足標準
#include<vadefs.h>
#define va_start_crt_va_start
//......
typedef char *va_list;
#define _INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))//簡單的位操作,應該可以理解
/*_INTSIZEOF(n)宏是為了考慮那些記憶體地址需要對齊的系統(轉載者註:X86MS就是這樣的系統),從宏的名字來應該是跟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編譯器中,默認情況下,參數進棧的順序是由右向左的(不同的編譯器的實現可能將參數從右向左壓棧,也可能從左向右壓棧,這個順序我們是不能加以利用的,應該考慮到代碼的移植性。所以應該用標準化的方式,在第二個例子中會給出。儘管如此,了解下VC的模式對於我們的理解也是有好處的),因此,參數進棧以後的記憶體模型如下圖所示:最後一個固定參數的地址位於第一個可變參數之下,並且是連續存儲的。
|——————————————————————————|
|最後一個可變參數 | ->高記憶體地址處
|——————————————————————————|
...................
|——————————————————————————|
|第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
//其實這裡是不必要這么寫的,反正調用好後都會失效,但是作為一個好習慣應該保留
}
int main(int argc, char* argv[]){
int i = 1234, j = 5678;
myprintf("the first test:i=%d\n",i,j);
myprintf("the secend test:i=%d; %x;j=%d;\n",i,0xABCD,j);
myprintf("OK\n");
system("pause");
return 0;
}
在intel+win2k+vc6的機器執行結果如下:
the first test:i=1234
the secend test:i=1234; 0xabcd; j=5678;
OK
VC2005也能得到同樣結果。
四.套用
求最大值:
//這個程式原作者掛得很慘,所以自己重新寫了一個
#include <stdarg.h>
#include <stdio.h>
int mymax(int n,...){
int m=0,i,y;
va_list x; //說明變數x 要儘量這么寫,儘量用宏名
va_start(x,n); //x被初始化為指向n後的第一個可變參數
for(i=1;i<=n;++i){
//將變數x所指向的int類型的值賦給y,同時使x指向下一個參數
y=va_arg(x,int);
if(y>m) m=y;
}
va_end(x); //清除變數x
return m;
}
int main(){
printf("max1=%d,max2=%d\n",mymax(3,5,56,50),mymax(6,0,4,32,45,12,500));
/*
*注意調用的時候千萬要搞清參數的個數!
*原作者犯了個很搞笑的錯誤
*前面明明寫了3,結果後面只跟了兩個參數
*就像mymax(3,5,56)
*最後,gcc自帶一個max函式,所以換個名字叫mymax
*/
return 0;
}
VC2005,DevCpp編譯通過,運行結果:
max1=56,max2=500