C函式和巨集中的可變引數

2021-07-09 02:11:52 字數 4276 閱讀 3737

一:呼叫慣例

函式的呼叫方和被呼叫方對函式如何呼叫應該有統一的理解,否則函式就無法正確呼叫。比如foo(int n, int m),呼叫方如果認為壓棧順序是m,n,而foo認為壓棧順序是n, m,那麼這個函式就不會呼叫成功。

因此,函式的呼叫方和被呼叫方對於函式如何呼叫需要有個明確的約定,雙方都遵守同樣的約定,函式才能呼叫成功,這種約定稱為呼叫慣例,乙個呼叫慣例一般會規定如下幾個方面的內容:

1:函式引數的傳遞順序和方式

函式引數的傳遞有多種方式,最常見的是通過棧傳遞。函式的呼叫方將引數壓入棧中,函式自己在從棧中將引數取出。如果有多個引數,呼叫慣例要規定函式呼叫方引數壓棧的順序:從左至右,還是從右至左。有些呼叫慣例還允許使用暫存器傳遞引數,以提高效能。

2:棧的維護方式

在函式將引數壓棧之後,函式體會被呼叫,此後需要將被壓入棧中的引數全部彈出,使得棧在函式呼叫前後保持一致。這個彈出的工作可以由函式呼叫方完成,也可以由函式本身完成。

3:名字修飾策略

不同的呼叫慣例對函式名有不同的修飾策略

在c中,存在多個呼叫慣例,預設的呼叫慣例是cdecl,任何乙個沒有顯示執行呼叫慣例的函式預設都是cdecl慣例。另外,_cdecl是非標準關鍵字,在不同的編譯器中可以有不同的寫法,比如在gcc中,使用:__attribute__((cdecl))。cdecl的呼叫慣例:引數從右至左的順序入棧,引數出棧由呼叫方完成。

除了cdecl呼叫慣例之外,還存在許多別的呼叫慣例,比如stdcall,fastcall等,不再贅述。

二:函式中的可變引數

printf函式的原型如下:

int printf(const char *format, ...);
printf函式就是可變引數的典範。除了第乙個引數型別為const char *之外,可以追加任意數量,任意型別的引數。

可變引數的實現,得益於c語言預設的cdecl呼叫慣例,它從右向左進行引數的入棧,比如函式:int  sum(unsigned  num,  ...);num表示後面會傳遞num個整數,當呼叫sum時:int n = sum(3, 16, 38, 53);引數在棧上的布局如下圖:

函式內部,可以使用num得到數字3,而且其他引數在棧上的排列就是在num的高位址方向,從而可以通過num的位址計算出其他引數的位址,所以,sum函式的實現如下:

int sum(unsigned num, ...)

所以,cdecl呼叫慣例保證了引數的正確處理,但是在呼叫sum函式的時候,必須要知道有多少個不定引數,每個不定引數的型別是什麼。因此printf在format中指定了引數型別和引數個數。

可以使用stdarg.h中定義的巨集來訪問各個不定引數:

#include void va_start(va_list ap, last);

type va_arg(va_list ap, type);

void va_end(va_list ap);

void va_copy(va_list dest, va_list src);

首先需要定義va_list型別的變數:va_list  ap;  該變數會依次指向各個可變引數。     va_list實際上是乙個指標,指向各種不定引數,因型別不同,所以va_list以void *或char *為最佳選擇。

ap必須首先使用va_start初始化,ap只有經過va_start初始化之後,才可以被後續的va_arg和va_end使用。

va_start(ap, last);其中last是函式最後乙個具名引數,比如printf中的format。因va_start中會使用last的位址,因此該變數不能是暫存器變數,也不能是函式或者陣列型別。

經過va_start初始化之後,ap指向第乙個可變引數。

va_arg巨集返回當前可變引數的值,並使ap指向下乙個可變引數:type  va_arg(va_list ap,  type); type是ap指向的當前可變引數的型別。該巨集還會修改ap,使其指向下乙個可變引數。

如果後續沒有可變引數了,或者type與實際的可變引數型別不符,則va_arg會發生不可預知的錯誤。

同乙個函式中,每乙個va_start必須跟著乙個相應的va_end。經過va_end之後,ap將是未定義的。一般是將指標ap置null。

這些巨集可以如下實現:

#define va_list             char *

#define va_start(ap, arg) (ap = (va_list)(&arg) + sizeof(arg))

#define va_arg(ap, t) (*(t*)((ap += sizeof(t)) – sizeof(t)))

#define va_end(ap) (ap = (va_list)0)

va_copy將src複製到dest。行為上類似於,以同樣的步驟和引數,將之前對src的呼叫,施加於dest上。

va_start、va_arg、va_end和va_copy都是執行緒安全的。其中va_start、va_arg和va_end是c89中就定義的,在c99中,又新增了va_copy。

下面的例子,是實現乙個printf函式的簡易版:

#include #include void foo(char *fmt, ...)

va_end(ap);

}

三:巨集中的可變引數c99中,類似於函式,巨集也可以接受可變引數。比如下面的例子:

#define debug(format, ...) fprintf (stderr, format, __va_args__)
其中的」...」就是可變引數。在巨集的呼叫中,它會展開為最後乙個命名引數之後,』)』之前的所有token,包括逗號,並替換掉巨集體中的」__va_args__」。

比如下面的語句:

debug("the int is %d, string is %s\n", 3, "hello, world");
經過巨集替換之後,展開為下面的語句:

fprintf (stderr, "the int is %d, string is %s\n", 3, "hello, world");
在gcc中,除了支援上面的寫法之外,還支援使用更具描述性的名字表示可變引數,而不使用」__va_args__」。使用」argname...」的寫法表示可變引數,其中argname是引數名,可以任意取。比如上面的巨集定義,可以寫成下面的形式,效果是一樣的:

#define debug(format, arg...)  fprintf(stderr, format, arg)
但是,上面兩種寫法的debug巨集還有乙個共同的問題,就是必須提供乙個可變引數,否則無法匹配巨集,而且會出現語法錯誤,比如下面的語句:

debug("hehe\n");
展開後,就會擴充套件為下面的語句:

fprintf(stderr, "hehe\n", );
這顯然會報語法錯誤。

解決這個問題有兩種方法:

1:將所有引數都當成可變引數,將巨集定義成下面的形式:

#define debug(...)  fprintf (stderr, __va_args__)
或是:

#define debug(arg...)  fprintf(stderr, arghehe)
2:使用」##」。當將」##」置於逗號和可變引數之間時,它有特殊的意義。比如下面的寫法:

#define debug(format, ...) fprintf (stderr, format, ##__va_args__)
或是:

#define debug(format, arg...)  fprintf(stderr, format, ##arg)
這種情況下,當省略可變引數時,」##」會使得預編譯器刪除它前面的逗號。當確實提供了可變引數時,」##」不起作用,像正常的可變引數一樣進行替換。

推薦使用第二種方法。

參考:

《程式設計師的自我修養》

C函式和巨集中的可變引數

一 呼叫慣例 函式的呼叫方和被呼叫方對函式如何呼叫應該有統一的理解,否則函式就無法正確呼叫。比如foo int n,int m 呼叫方如果認為壓棧順序是m,n,而foo認為壓棧順序是n,m,那麼這個函式就不會呼叫成功。因此,函式的呼叫方和被呼叫方對於函式如何呼叫需要有個明確的約定,雙方都遵守同樣的約...

可變引數函式和巨集函式

格式化字串,可變引數函式需要借助於va list va start va arg va end巨集,可變引數巨集需要借助於 變參1 最後乙個可見引數型別是int int add int n,int main int add int n,該函式返回 n 個 int 數的和,n個int,跟在第乙個引數後...

《c和指標》 巨集可變引數

va list 是在c語言中解決變參問題的一組巨集,定義在標頭檔案下。va list的用法 1 首先在函式裡定義一具va list型的變數,這個變數是指向引數的指標 2 然後用va start巨集初始化變數剛定義的va list變數,這個巨集的第二個引數是第乙個可變引數的前乙個引數,是乙個固定的引數...