C/C++宏定義中可變參數 __VA_ARGS__ 的理解

用可變參數宏(variadic macros)傳遞可變參數表

你可能很熟悉在函數中使用可變參數表,如:

void printf(const char* format, ...);

一直以來,可變參數表還是隻能應用在真正的函數中,不能使用在宏中。

C99編譯器標準終於改變了這種局面,它允許你可以定義可變參數宏(variadic macros),這樣你就可以使用擁有可以變化的參數表的宏。可變參數宏就像下面這個樣子:

#define debug(...) printf(__VA_ARGS__)

缺省號代表一個可以變化的參數表。使用保留名 __VA_ARGS__把參數傳遞給宏。當宏的調用展開時,實際的參數就傳遞給 printf()了。例如:

Debug("Y = %d\n", y); //兩個參數

而處理器會把宏的調用替換成:

printf("Y = %d\n", y);

因爲debug()是一個可變參數宏,你能在每一次調用中傳遞不同數目的參數:

Debug("test"); //一個參數

可變參數宏不被ANSI/ISO C++ 所正式支持。因此,你應當檢查你的編譯器,看它是否支持這項技術。

用GCC和C99的可變參數宏, 更方便地打印調試信息

gcc的預處理提供的可變參數宏定義真是好用:

#ifdef DEBUG
#define dbgprint(format,args...)  fprintf(stderr, format, ##args)
#else
#define dbgprint(format,args...)
#endif

如此定義之後,代碼中就可以用dbgprint了,
例如dbgprint("aaa %s",  FILE);。感覺這個功能比較cool

  • 下面是C99的方法:

#define dgbmsg(fmt,...)  printf(fmt, __VA_ARGS__)

新的C99規範支持了可變參數的宏 __VA_ARGS__

具體使用如下:
以下內容爲程序代碼:

 #include <stdarg.h> 
 #include <stdio.h>
 #define LOGSTRINGS(fm, ...) printf(fm, __VA_ARGS__)

 int main() 
 {
 	LOGSTRINGS("hello, %d ", 10);      
 	return 0; 
 } 

但現在似乎只有gcc才支持(VC從VC2005開始支持)。

  • 可變參數的宏裏的‘##’操作說明

帶有可變參數的宏(Macros with a Variable Number of Arguments)
在1999年版本的ISO C 標準中,宏可以象函數一樣,定義時可以帶有可變參數。宏的語法和函數的語法類似。下面有個例子:

#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)

這裏,‘’指可變參數。這類宏在被調用時,它(這裏指‘’)被表示成零個或多個符號,包括裏面的逗號,一直到到右括弧結束爲止。當被調用時,在宏體(macro body)中,那些符號序列集合將代替裏面的__VA_ARGS__標識符。更多的信息可以參考CPP手冊。

GCC始終支持複雜的宏,它使用一種不同的語法從而可以使你可以給可變參數一個名字,如同其它參數一樣。例如下面的例子:

#define debug(format, args...) fprintf (stderr, format, args)

這和上面舉的那個ISO C定義的宏例子是完全一樣的,但是這麼寫可讀性更強並且更容易進行描述。

GNU CPP還有兩種更復雜的宏擴展,支持上面兩種格式的定義格式。

在標準C裏,你不能省略可變參數,但是你卻可以給它傳遞一個空的參數。例如,下面的宏調用在ISO C裏是非法的,因爲字符串後面沒有逗號:

debug ("A message")

GNU CPP在這種情況下可以讓你完全的忽略可變參數。在上面的例子中,編譯器仍然會有問題(complain),因爲宏展開後,裏面的字符串後面會有個多餘的逗號。

爲了解決這個問題,CPP使用一個特殊的 ## 操作。書寫格式爲:

#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)

這裏,如果可變參數被忽略或爲空,‘##’操作將使預處理器(preprocessor)去除掉它前面的那個逗號。如果你在宏調用時,確實提供了一些可變參數,GNU CPP也會工作正常,它會把這些可變參數放到逗號的後面。象其它的pasted macro參數一樣,這些參數不是宏的擴展。

  • 怎樣寫參數個數可變的宏

一種流行的技巧是用一個單獨的用括弧括起來的的 “參數” 定義和調用宏, 參數在 宏擴展的時候成爲類似 printf() 那樣的函數的整個參數列表。

#define DEBUG(args) (printf("DEBUG: "), printf args)

if(n != 0) DEBUG(("n is %d\n", n));

明顯的缺陷是調用者必須記住使用一對額外的括弧。

gcc 有一個擴展可以讓函數式的宏接受可變個數的參數。 但這不是標準。另一種 可能的解決方案是根據參數個數使用多個宏 (DEBUG1, DEBUG2, 等等), 或者用 逗號玩個這樣的花招:

#define DEBUG(args) (printf("DEBUG: "), printf(args))
#define _ ,    DEBUG("i = %d" _ i);

C99 引入了對參數個數可變的函數式宏的正式支持。在宏 "原型" 的末尾加上符號 (就像在參數可變的函數定義中), 宏定義中的僞宏 __VA_ARGS__就會在調用時 替換成可變參數。

最後, 你總是可以使用真實的函數, 接受明確定義的可變參數

如果你需要替換宏, 使用一個 函數和一個非函數式宏, 如 #define printf myprintf。

  • 自定義調試信息的輸出

調試信息的輸出方法有很多種, 例如直接用printf, 或者出錯時使用perror, fprintf等將信息直接打印到終端上, 在Qt上面一般使用qDebug,而守護進程則一般是使用syslog將調試信息輸出到日誌文件中等等…
  使用標準的方法打印調試信息有時候不是很方便, 例如Qt編程, 在調試已有的代碼時, 我想在打印調試信息的地方, 把代碼位置也打印出來以方便定位錯誤, 或者需要在調試信息前面加一個前輟, 好方便在調試信息太多的時候可以用grep過濾一下, 僅顯示本模塊的調試信息, 這時就需要一個一個地修改已有的qDebug, 使其成爲以下形式:

qDebug( "[模塊名稱] 調試信息  File:%s, Line:%d", __FILE__, __LINE__ );

這樣的修改比較煩人, 而且一不小心會遺漏某個沒改的…
  爲了能方便地管理調試信息的輸出,一個比較簡單的方法就是自已定義一個打印調試信息的宏, 然後替換原來的,廢話就不多說了,直接給出一個現成的,下面是一個例子, 我用WiFi表示當前代碼的模塊名稱,我要求在模塊中的所有調試信息前面均帶有[WiFi]前輟,這樣我就能方便地只需使用命令行

| grep "WiFi"

來過濾掉來自其它模塊的調試信息了:

#define qWiFiDebug(format, ...) qDebug("[WiFi] "format" File:%s, Line:%d, Function:%s", \
 	##__VA_ARGS__, __FILE__, __LINE__ , __FUNCTION__);

上面的宏是使用qDebug輸出調試信息,在非Qt的程序中也可以改爲printf,守護進程則可以改爲syslog等等… 其中,決竅其實就是這幾個宏 ##__VA_ARGS__, __FILE__, __LINE__ 和__FUNCTION__,下面介紹一下這幾個宏:
  1) __VA_ARGS__是一個可變參數的宏,很少人知道這個宏,這個可變參數的宏是新的C99規範中新增的,目前似乎只有gcc支持(VC6.0的編譯器不支持)。宏前面加上 ## 的作用在於,當可變參數的個數爲0時,這裏的 ## 起到把前面多餘的","去掉的作用,否則會編譯出錯, 你可以試試。
  2) __FILE__宏在預編譯時會替換成當前的源文件名
  3) __LINE__ 宏在預編譯時會替換成當前的行號
  4) __FUNCTION__宏在預編譯時會替換成當前的函數名稱
  有了以上這幾個宏,特別是有了__VA_ARGS__
,調試信息的輸出就變得靈活多了。
  有時,我們想把調試信息輸出到屏幕上,而有時則又想把它輸出到一個文件中,可參考下面的例子:

//debug.c
#include 
#include
//開啓下面的宏表示程序運行在調試版本, 否則爲發行版本, 這裏假設只有調試版本才輸出調試信息
#define _DEBUG
#ifdef _DEBUG
    //開啓下面的宏就把調試信息輸出到文件,註釋即輸出到終端
    #define DEBUG_TO_FILE
    #ifdef DEBUG_TO_FILE
        //調試信息輸出到以下文件
        #define DEBUG_FILE "/tmp/debugmsg"
        //調試信息的緩衝長度
        #define DEBUG_BUFFER_MAX 4096
        //將調試信息輸出到文件中
        #define printDebugMsg(moduleName, format, ...) {\
            char buffer[DEBUG_BUFFER_MAX+1]={0};\
            snprintf( buffer, DEBUG_BUFFER_MAX \
                    , "[%s] "format" File:%s, Line:%d\n", moduleName, ##__VA_ARGS__, __FILE__, __LINE__ );\
            FILE* fd = fopen(DEBUG_FILE, "a");\
            if ( fd != NULL ) {\
                fwrite( buffer, strlen(buffer), 1, fd );\
                fflush( fd );\
                fclose( fd );\
            }\
        }
    #else
        //將調試信息輸出到終端
        #define printDebugMsg(moduleName, format, ...) \
                  printf( "[%s] "format" File:%s, Line:%d\n", moduleName, ##__VA_ARGS__, __FILE__, __LINE__ );
    #endif //end for #ifdef DEBUG_TO_FILE
#else
    //發行版本,什麼也不做
    #define printDebugMsg(moduleName, format, ...)
#endif  //end for #ifdef _DEBUG
int main(int argc, char** argv)
{
    int data = 999;
    printDebugMsg( "TestProgram", "data = %d", data );
    return 0;
}

上面也說了,只有支持C99規範的gcc編譯器纔有__VA_ARGS__這個宏,如果不是gcc編譯器,或者所用的gcc編譯器版本不支持__VA_ARGS__宏怎麼辦? 可參考下面的代碼片段,我們換一種做法,可先將可變參數轉換成字符串後,再進行輸出即可:

void printDebugMsg( const char* format, ...)
{
    char buffer[DEBUG_BUFFER_MAX_LENGTH + 1]={0};
    va_list arg;
    va_start (arg, format);
    vsnprintf(buffer, DEBUG_BUFFER_MAX_LENGTH, format, arg);
    va_end (arg);
    printf( "%s", buffer );
}

 本人在工作中的碰到的一些樣例:

使用VS2010 測試下面代碼:

#include <stdio.h>
#define ExampleIndex  9

#define _Dbg(...)  printf(__VA_ARGS__)
#define Dbg(fmt, ...) _Dbg("[Example:%d]  "fmt" \n", ExampleIndex, __VA_ARGS__)

int main()
{
	Dbg("hello world");  //compiler error C2059: 語法錯誤:“)”

	return 0;
}

上面這個例子VS2010編譯出錯:error C2059: 語法錯誤:“)”

原因上面也有講到,

#define TEST(...)  XXX(__VA_ARGS__)

如果宏定義是上面這種格式,對__VA_ARGS__無任何特殊要求。

#define TEST(fmt, ...) XXX(fmt, __VA_ARGS__)

如果宏定義是上面這種格式,對__VA_ARGS__有限制,__VA_ARGS__不能爲空,即至少有一個參數,否則宏展開後,宏體裏面的 fmt 後面會有個多餘的逗號' , '   這時VS2010編譯器會報錯:error C2059: 語法錯誤:“)” 。

根據往常資料和本人測試,上面例子有三種解決方案:

1. 在 TEST(fmt, ...) 使用時,' ... '必須至少有一個參數,沒有就手動添加一個參數

#include <stdio.h>

#define ExampleIndex  9

#define _Dbg(...)  printf(__VA_ARGS__)
#define Dbg(fmt, ...) _Dbg("[Example:%d]  "fmt" \n", ExampleIndex, __VA_ARGS__)

int main()
{
	Dbg("hello world", 999);// "hello world" 後面隨便增加一個參數就行,實際打印並不輸出這個參數

	return 0;
}

編譯通過,運行結果如下: 

 

2. 在宏定義中  __VA_ARGS__前面添加操作符 ##

網上資料說,如果可變參數__VA_ARGS__被忽略或爲空,‘##’操作將使預處理器(preprocessor)去除掉它前面的那個逗號。格式如下:

#define TEST(fmt, ...) XXX(fmt, ##__VA_ARGS__)

然而VS2010 實際測試中 並沒卵用,還是報錯:error C2059: 語法錯誤:“)”


#include <stdio.h>

#define ExampleIndex  9

#define _Dbg(...)  printf(__VA_ARGS__)
#define Dbg(fmt, ...) _Dbg("[Example:%d]  "fmt" \n", ExampleIndex, ##__VA_ARGS__)

int main()
{
	Dbg("hello world"); // compiler error C2059: 語法錯誤:“)”

	getchar();
	return 0;
}

 編譯失敗報錯:error C2059: 語法錯誤:“)”

3. 將嵌套宏的每層宏定義中的可變參數都 由 TEST( ...) 改成 TEST(fmt, ...) 格式

#include <stdio.h>

#define ExampleIndex  9

#define _Dbg(fmt, ...)  printf(fmt, __VA_ARGS__)
#define Dbg(fmt, ...) _Dbg("[Example:%d]  "fmt" \n", ExampleIndex, __VA_ARGS__)

int main()
{
	Dbg("hello world");

	getchar();
	return 0;
}

編譯通過,運行結果如下: 

 

PS:Dbg(fmt, ...)  此類宏定義中字符串連接有兩種方式:

#define TEST(fmt, ...) XXX("[TEST]"fmt"", __VA_ARGS__)

#define TEST(fmt, ...) XXX("[TEST]"##fmt, __VA_ARGS__)

注意,在C語言中字符串中的二個相連的雙引號會被自動忽略

VS2010樣例: 

#include <stdio.h>

#define ExampleIndex  9

#define _Dbg(fmt, ...)  printf(fmt, __VA_ARGS__)
#define Dbg(fmt, ...)  _Dbg("[Dbg ][Example:%d]"fmt" \n", ExampleIndex, __VA_ARGS__)
#define Dbg2(fmt, ...) _Dbg("[Dbg2][Example:%d]"##fmt, ExampleIndex, __VA_ARGS__)

int main()
{
	Dbg("hello world");
	Dbg2("hello world");
	getchar();
	return 0;
}

編譯運行結果如下:

PS:Dbg(...)  此類宏定義中字符串連接只有一種方式:

#define TEST(...) XXX("[TEST]"##__VA_ARGS__)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章