參數可變函數又稱VA函數,例如printf,scanf,exec。
1.舉例:
//fun:列印n後面參數的值
void fun(int n, ...);
int main()
{
int part1 = 128;
int part2 = 256;
int part3 = 512;
fun(part1, part2, part3);
return 0;
}
void fun(int part1, ...)
{
int *p = &part1; //擷取part1的地址
printf("%d\n", *++p);//列印part2的值
printf("%d\n", *++p);//列印part3的值
}
C預設的函數調用規範是_cdecl,即所有參數從右至左依次入棧,嚴格的fun聲明應該是:
void _cdecl fun(int n, ...);
在main中,調用fun函數前先將參數入棧,入棧的順序是:
push part3
push part2
push part1
然後調用fun,執行函數體代碼。
先壓棧的參數會放在高地址,因為棧是由高地址往低地址生長的,所以part1,part2,part3在記憶體中的順序將會是:
0xFE6C part3
0xFE70 part2
0xFE74 part1
這樣可以通過取得第一個參數的地址&part1和++操作分別訪問到後面的參數,但這必須是_cdecl函數調用規範,例如printf系列的庫函數(sprintf,fprintf)都是可以接受可變參數的函數。假設有下面一條語句:
printf("%d %d %d\n", m, n, k);
可以看出,我們可以通過第一個參數得到參數的個數(格式符的數目)和類型(例如%d),這就是為什麼printf("%d %d\n", m, n, k)可以成功執行,而printf("%d %d %d\n", m, n)會失敗的原因了。傳遞的參數如果多于格式符的個數可以忽略掉(在Linux下能正常執行),但是少於就會出現訪問越界(Linux下會警示告,但是仍然正常執行,越界的參數會是個隨機值)。(執行自己程式的時候記得加./,例如./a.out)
2.採用varargs宏來編寫支援可變參數列表的函數,在ASCI C標準裡,這些宏包含在stdarg.h。
例如以下代碼:
#include <stdio.h>
#include <stdarg.h>
void _cdecl fun(int n, ...);//可以不加_cdecl
int main(int argc, char *argv[])
{
int part1 = 128;
int part2 = 256;
int part3 = 512;
fun(part1, part2, part3);
return 0;
}
void fun(int n, ...)
{
va_list ap;
va_start(ap, n);
printf("%d\n", va_arg(ap, int));
printf("%d\n", va_arg(ap, int));
va_end(ap);
}
在Microsoft為VC提供的實現中,可以看到這樣的定義:
#define _ADDRESSOF(v) (&(v))
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
typedef char* va_list;
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )//ap指向v下一個參數的地址
#define va_arg(ap,t) (*(t*)( (ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //ap指向下一個參數的地址,但是返回的還 //是這個參數的值
#define va_end(ap) ( ap = (va_list)0 )
va_list 一個char型指標,每次單個位元組定址。
va_start 通過_INTSIZEOF計算參數類型大小,並讓ap獲得v後面參數對象的地址
va_arg ap指向參數列表中ap下一個參數對象,並返回ap之前指向的t型別參數對象
其中_INTSIZEOF(n)就是將n的長度化為int長度的整數倍。如果n是char類型,那麼sizeof(n)=1,化為int長度的整數倍就應該是4,假設sizeof(n)=4m+k(m>=0,k=0,1,2,3),sizeof(n)+4-1=4m+(k+3),~(sizeof(int)-1)=~(4-1)=~(00000011b)=11111100b,這樣任何數&~(sizeof(int)-1)後,後兩位肯定是0,那就是4的倍數了,(4m+(k+3))&~(sizeof(int)-1)可以保證能存放4m+k。
將這些宏還原重寫fun函數:
void fun(int part1, ...)
{
int part2, part3;
char *ap;//va_list ap;
ap = (char *)&part1+4;//va_start(ap, part1), ap指向part2
part2 = *(int *)(ap+=4 - 4);//返回part2,ap指向part3,part2=va_arg(ap,int)
part3 = *(int *)(ap+=4 - 4);//返回part2,ap指向part3,part3=va_arg(ap,int)
ap=(char *)0;//ap指向空,var_end(ap)
}
若part2為char,shor則會自動轉化成int型,float則會自動轉化成double型。
#include <stdio.h>
#include <stdarg.h>
void fun(int n, ...);
int main(int argc, char *argv[])
{
int part1 = 128;
float part2 = 256.0;//float
float part3 = 512.0;
fun(part1, part2, part3);
return 0;
}
void fun(int n, ...)
{
va_list ap;
va_start(ap, n);
printf("%f\n", va_arg(ap, double));//double正確,float錯誤,系統自動儲存為double型
printf("%f\n", va_arg(ap, double));
va_end(ap);
}
#include <stdio.h>
#include <stdarg.h>
void fun(int n, ...);
int main(int argc, char *argv[])
{
int part1 = 128;
char part2 = 'c';//char
short part3 = 8;//short
fun(part1, part2, part3);
return 0;
}
void fun(int n, ...)
{
va_list ap;
va_start(ap, n);
printf("%c\n", va_arg(ap, int));//char自動儲存為int,不能用char,即使這樣,仍然可用%c輸出字元
printf("%d\n", va_arg(ap, int));//short自動儲存為int,不能用short
va_end(ap);
}
以上這些問題都是記憶體對齊造成的。
3.vprintf,vfprintf,vsprintf,vsnprintf,vasprintf格式化輸出,有一個va_list參數
#include <stdio.h>
#include <stdarg.h>
int vprintf(const char *format, va_list ap);//格式化輸出到標準輸出,對應到printf
int vfprintf(FILE *stream, const char *format, va_list ap);//格式化輸出到檔案流,對應到fprintf
int vsprintf(char *s, const char *format, va_list ap);//格式化輸出到字串,對應到sprintf
int vsnprintf(char *s, size_t n, const char *format, va_list ap);//格式化輸出固定長度到字串,對應到snprintf
int vasprintf(char **ret, const char *format, va_list ap);//對應到asprintf
舉例,用vprintf實現error:
#include <stdio.h>
#include <stdarg.h>
void error(char *function_name, char *format, ...)
{
va_list ap;
va_start(ap, format);
/*print out name of function causing error*/
(void)fprintf(stderr, "ERR in %s:", function_name);
/*print out remainder of message*/
(void)vfprintf(stderr, format, ap);
va_end(ap);
(void)abort();
}