1. 使用情形 c++
1.
int a =10;
2.
double b = 20.0;
3.
char *str = "Hello world";
4.
printf("begin print\n");
5.
printf("a=%d, b=%.3f, str=%s\n", a, b, str);
6.
...
從printf的使用狀況來看,咱們不難發現一個規律,就是不管其可變的參數有多少個,printf的第一個參數老是一個字符串。而正是這第一個參數,使得它能夠確認後面還有有多少個參數尾隨。而尾隨的每一個參數佔用的棧空間大小又是經過第一個格式字符串肯定的。然而printf究竟是怎樣取第一個參數後面的參數值的呢,請看以下代碼 函數 2. printf 函數的實現 spa
01.
//acenv.h
02.
typedef char *va_list;
03.
04.
#define _AUPBND (sizeof (acpi_native_int) - 1)
05.
#define _ADNBND (sizeof (acpi_native_int) - 1)
06.
07.
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
08.
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
09.
#define va_end(ap) (void) 0
10.
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
11.
12.
//start.c
13.
static char sprint_buf[1024];
14.
int printf(char *fmt, ...)
15.
{
16.
va_list args;
17.
int n;
18.
va_start(args, fmt);
19.
n = vsprintf(sprint_buf, fmt, args);
20.
va_end(args);
21.
write(stdout, sprint_buf, n);
22.
return n;
23.
}
24.
25.
//unistd.h
26.
static inline long write(int fd, const char *buf, off_t count)
27.
{
28.
return sys_write(fd, buf, count);
29.
}
3. 分析 內存 從上面的代碼來看,printf彷佛並不複雜,它經過一個宏va_start把全部的可變參數放到了由args指向的一塊內存中,而後再調用vsprintf. 真正的參數個數以及格式的肯定是在vsprintf搞定的了。因爲vsprintf的代碼比較複雜,也不是咱們這裏要討論的重點,因此下面就再也不列出了。咱們這裏要討論的重點是va_start(ap, A)宏的實現,它對定位從參數A後面的參數有重大的制導意義。如今把 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 的含義解釋一下以下: ci
1.
va_start(ap, A)
2.
{
3.
char *ap = ((char *)(&A)) + sizeof(A)並int類型大小地址對齊
4.
}
在printf的va_start(args, fmt)中,fmt的類型爲char *, 所以對於一個32爲系統 sizeof(char *) = 4, 若是int大小也是32,則va_start(args, fmt);至關於 char *args = (char *)(&fmt) + 4; 此時args的值正好爲fmt後第一個參數的地址。對於以下的可變參數函數 字符串
1.
void fun(double d,...)
2.
{
3.
va_list args;
4.
int n;
5.
va_start(args, d);
6.
}
則 va_start(args, d);至關於 編譯器
1.
char *args = (char *)&d + sizeof(double);
此時args正好指向d後面的第一個參數。 it 可變參數函數的實現與函數調用的棧結構有關,正常狀況下c/c++的函數參數入棧規則爲__stdcall, 它是從右到左的,即函數中的最右邊的參數最早入棧。對於函數 編譯
1.
void fun(int a, int b, int c)
2.
{
3.
int d;
4.
...
5.
}
其棧結構爲 table
1.
0x1ffc-->d
2.
0x2000-->a
3.
0x2004-->b
4.
0x2008-->c
對於任何編譯器,每一個棧單元的大小都是sizeof(int), 而函數的每一個參數都至少要佔一個棧單元大小,如函數 void fun1(char a, int b, double c, short d) 對一個32的系統其棧的結構就是
1.
0x1ffc-->a (4字節)
2.
0x2000-->b (4字節)
3.
0x2004-->c (8字節)
4.
0x200c-->d (4字節)
對於函數void fun1(char a, int b, double c, short d) 若是知道了參數a的地址,則要取後續參數的值則能夠經過a的地址計算a後面參數的地址,而後取對應的值,然後面參數的個數能夠直接由變量a指定,固然也能夠像printf同樣根據第一個參數中的%模式個數來決定後續參數的個數和類型。若是參數的個數由第一個參數a直接決定,則後續參數的類型若是沒有變化而且是已知的,則咱們能夠這樣來取後續參數, 假定後續參數的類型都是double;
1.
void fun1(int num, ...)
2.
{
3.
double *p = (double *)((&num)+1);
4.
double Param1 = *p;
5.
double Param2 = *(p+1);
6.
...
7.
double Paramn *(p+num);
8.
}
若是後續參數的類型是變化並且是未知的,則必須經過一個參數中設定模式來匹配後續參數的個數和類型,就像printf同樣,固然咱們能夠定義本身的模式,如能夠用i表示int參數,d表示double參數,爲了簡單,咱們用一個字符表示一個參數,並由該字符的名稱決定參數的類型而字符的出現的順序也表示後續參數的順序。 咱們能夠這樣定義字符和參數類型的映射表,
1.
i---int
2.
s---signed short
3.
l---long
4.
c---char
"ild"模式用於表示後續有三個參數,按順序分別爲int, long, double類型的三個參數那麼這樣咱們能夠定義本身版本的printf 以下
01.
void printf(char *fmt, ...)
02.
{
03.
char s[80] = "";
04.
int paramCount = strlen(fmt);
05.
write(stdout, "paramCount = " , strlen(paramCount = ));
06.
itoa(paramCount,s,10);
07.
write(stdout, s, strlen(s));
08.
char *p = (char *)(&fmt) + sizeof(char *);
09.
int *pi = (int *)p;
10.
for (int i=0; i<paramCount; i++)
11.
{
12.
char line[80] = "";
13.
strcpy(line, "param");
14.
itoa(i+1, s, 10);
15.
strcat(line, s);
16.
strcat(line, "=");
17.
switch(fmt[i])
18.
{
19.
case 'i':
20.
case 's':
21.
itoa((*pi),s,10);
22.
strcat(line, s);
23.
pi++;
24.
break;
25.
case 'c':
26.
{
27.
int len = strlen(line);
28.
line[len] = (char)(*pi);
29.
line[len+1] = '\0';
30.
}
31.
break;
32.
case 'l':
33.
ltoa((*(long *)pi),s,10);
34.
strcat(line, s);
35.
pi++;
36.
break;
37.
default:
38.
break;
39.
}
40.
}
41.
}
也能夠這樣定義咱們的Max函數,它返回多個輸入整型參數的最大值
01.
int Max(int n, ...)
02.
{
03.
int *p = &n + 1;
04.
int ret = *p;
05.
for (int i=0; i<n; i++)
06.
{
07.
if (ret < *(p + i))
08.
ret = *(p + i);
09.
}
10.
return ret;
11.
}
能夠這樣調用, 後續參數的個數由第一個參數指定
1.
int m = Max(3, 45, 12, 56);
2.
int m = Max(1, 3);
3.
int m = Max(2, 23, 45);
4.
5.
int first = 34, second = 45, third=5;
6.
int m = Max(5, first, second, third, 100, 4);
結論 對於可變參數函數的調用有一點須要注意,實際的可變參數的個數必須比前面模式指定的個數要多,或者不小於, 也即後續參數多一點沒關係,但不能少, 若是少了則會訪問到函數參數之外的堆棧區域,這可能會把程序搞崩掉。前面模式的類型和後面實際參數的類型不匹配也有可能形成把程序搞崩潰,只要模式指定的數據長度大於後續參數長度,則這種狀況就會發生。如:
1.
printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);
參數1,2,3,4的默認類型爲整型,而模式指定的須要爲double型,其數據長度比int大,這種狀況就有可能訪問函數參數堆棧之外的區域,從而形成危險。可是printf("%d, %d, %d", 1.0, 20., 3.0);這種狀況雖然結果可能不正確,可是確不會形成災難性後果。由於實際指定的參數長度比要求的參數長度長,堆棧不會越界。 |