對C的printf函式的可變長引數實現的分析

2021-06-20 11:14:21 字數 4434 閱讀 6372

內容摘要:一直以來都覺得printf似乎是c語言庫中功能最強大的函式之一,不僅因為它能格式化輸出,更在於它的引數個數沒有限制,要幾個就給幾個,來者不拒。printf這種對引數個數和引數型別的強大適應性,讓人產生了對它進行探索的濃厚興趣。 

【1. 使用情形 】

[cpp]view plain

copy

inta =10;  

double

b = 20.0;  

char

*str = 

"hello world"

;  printf("begin print  

");  

printf("a=%d, b=%.3f, str=%s"

, a, b, str);  

...  

從printf的使用情況來看,我們不難發現乙個規律,就是無論其可變的引數有多少個,printf的第乙個引數總是乙個字串。而正是這第乙個引數,使得它可以確認後面還有有多少個引數尾隨。而尾隨的每個引數占用的棧空間大小又是通過第乙個格式字串確定的。然而printf到底是怎樣取第乙個引數後面的引數值的呢,請看如下代。

【2. printf 函式的實現】

[cpp]view plain

copy

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))))

[cpp]view plain

copy

static

char

sprint_buf[1024];  

intprintf(

char

*fmt, ...)    

[cpp]view plain

copy

static

inline

long

write(

intfd, 

const

char

*buf, off_t count)    

【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)))) 的含**釋一下如下:

[cpp]view plain

copy

va_start(ap, a)    

在printf的va_start(args, fmt)中,fmt的型別為char *, 因此對於乙個32為系統 sizeof(char *) = 4, 如果int大小也是32,則va_start(args, fmt);相當於 char *args = (char *)(&fmt) + 4; 此時args的值正好為fmt後第乙個引數的位址。對於如下的可變引數函式

[cpp]view plain

copy

void

fun(

double

d,...)    

則 va_start(args, d);相當於char *args = (char *)&d + sizeof(double);此時args正好指向d後面的第乙個引數。可變引數函式的實現與函式呼叫的棧結構有關,正常情況下c/c++的函式引數入棧規則為__stdcall, 它是從右到左的,即函式中的最右邊的引數最先入棧。對於函式

[cpp]view plain

copy

void

fun(

inta, 

intb, 

intc)    

其棧結構為0x1ffc-->d  0x2000-->a  0x2004-->b  0x2008-->c

對於任何編譯器,每個棧單元的大小都是sizeof(int), 而函式的每個引數都至少要佔乙個棧單元大小,如函式 void fun1(char a, int b, double c, short d) 對乙個32的系統其棧的結構就是0x1ffc-->a (4位元組) 0x2000-->b (4位元組) 0x2004-->c (8位元組) 0x200c-->d (4位元組)對於函式void fun1(char a, int b, double c, short d)如果知道了引數a的位址,則要取後續引數的值則可以通過a的位址計算a後面引數的位址,然後取對應的值,而後面引數的個數可以直接由變數a指定,當然也可以像printf一樣根據第乙個引數中的%模式個數來決定後續引數的個數和型別。如果引數的個數由第乙個引數a直接決定,則後續引數的型別如果沒有變化並且是已知的,則我們可以這樣來取後續引數, 假定後續引數的型別都是double; 

[cpp]view plain

copy

void

fun1(

intnum, ...)    

如果後續引數的型別是變化而且是未知的,則必須通過乙個引數中設定模式來匹配後續引數的個數和型別,就像printf一樣,當然我們可以定義自己的模式,如可以用i表示int引數,d表示double引數,為了簡單,我們用乙個字元表示乙個引數,並由該字元的名稱決定引數的型別而字元的出現的順序也表示後續引數的順序。 我們可以這樣定義字元和引數型別的對映表,i---int   s---signed short    l---long   c---char "ild"模式用於表示後續有三個引數,按順序分別為int, long, double型別的三個引數那麼這樣我們可以定義自己版本的printf如下

[cpp]view plain

copy

void

printf(

char

*fmt, ...)  

break

;  case

'l':  

ltoa((*(long

*)pi),s,10);  

strcat(line, s);  

pi++;  

break

;  default

:  break

;  }  

}  }  

也可以這樣定義我們的max函式,它返回多個輸入整型引數的最大值

[cpp]view plain

copy

intmax(

intn, ...)  

return

ret;  

}  

可以這樣呼叫, 後續引數的個數由第乙個引數指定

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);

【4. 結論】

對於可變引數函式的呼叫有一點需要注意,實際的可變引數的個數必須比前面模式指定的個數要多,或者不小於,也即後續引數多一點不要緊,但不能少,如果少了則會訪問到函式引數以外的堆疊區域,這可能會把程式搞崩掉。前面模式的型別和後面實際引數的型別不匹配也有可能造成把程式搞崩潰,只要模式指定的資料長度大於後續引數長度,則這種情況就會發生。如:

printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);

引數1,2,3,4的預設型別為整型,而模式指定的需要為double型,其資料長度比int大,這種情況就有可能訪問函式引數堆疊以外的區域,從而造成危險。但是printf("%d, %d, %d", 1.0, 20., 3.0);這種情況雖然結果可能不正確,但是確不會造成災難性後果。因為實際指定的引數長度比要求的引數長度長,堆疊不會越界。

C 系列 函式可變長引數

一 基礎部分 1.1 什麼是可變長引數 可變長引數 顧名思義,就是函式的引數長度 數量 是可變的。比如 c 語言的 printf 系列的 格式化輸入輸出等 函式,都是引數可變的。下面是 printf 函式的宣告 int printf const char format,可變引數函式宣告方式都是類似的...

c語言中可變引數的原理 printf 函式

函式原型 int printf const char format argument 返 回 值 成功則返回實際輸出的字元數,失敗返回 1.函式說明 在printf 函式中,format後面的引數個數不確定,且型別也不確定,這些引數都存放在棧內.呼叫printf 函式時,根據format裡的格式 d...

c語言可變引數原理以及printf函式的自實現

可變引數 使用場景 不確定傳入引數的個數,例如printf,scanf,原理 由於函式引數是存放在棧中的,而且是從左到右依次入棧 引數的位址從左到右依次增大 從右到左依次初始化,所以,函式的引數位置是確定的,一旦我們知道了某乙個引數的位址我們就可以獲得所有引數的位址。知道了原理就可以自己來實現乙個可...