1. 程式人生 > >Windows下C/C++可變引數巨集實現技巧

Windows下C/C++可變引數巨集實現技巧

WindowsC/C++可變引數巨集實現技巧

在開發過程中,有很多階段,每個階段可能會注重不同的重點,我們可能會在不同階段讓程式輸出或者列印不同的資訊以反應執行的情況,所以我們必須分階段的使得程式輸出我們在每個階段所要關心的資訊,甚至在最後讓程式不再輸出資訊。這就要用到了巨集定義!

我們知道,在linux下很方便的就能實現可變引數巨集的定義,

比如:

#define myprint(fmt, a...)    printf("%s,%s(),%d:" fmt "/n", __FILE__,__FUNCTION__,__LINE__, ##a)就定義了自己的輸出巨集,當不必再輸出這些可能是調式,跟蹤,斷言,日誌

...的資訊時,可以再定義巨集為空:

#define myprintf(fmt,a...)

這樣,重新編譯後,這些巨集引用的地方將全部沒有語句,從而省去這些開銷。

但是,在windows下,一般我們採用的VC6.0,VS2003,VS2005,VS2008(待定)編輯器中自帶的C/C++編譯器並不支援變參巨集的定義,gcc編譯器支援,據說最新版本的C99也支援。

可以在windows下這樣定義巨集:

#define myprint printf

但是,當後期不想再要巨集輸出了,只能定義 #define myprint為空,在那些有巨集呼叫的程式碼區會留下類似 ("DEBUG:>> %d,%s,%f",idx,"weide001",99.001);這樣的語句,它應該會被程式運算一次,應該會像函式引數那樣被壓棧,出棧一次,從而增加了程式的執行開銷,不是一個上策。

所以,在windows下需要變通一下,以下四種方式可以作為今後windows下定義可變參巨集定義的參考:

1)引用系統變參函式:

#include <stdarg.h>

#ifdef _WIN32

  #define vsnprinf _vsnprintf

  #define vsprinf  _vsprintf

#endif

int my_print(const char* file,cosnt char* fun,const char* line,const char *fmt, ...)

{

  int t = 0;

  char out[1024]="";

  va_list

ap;

/*
  Test Env:
    gcc version 3.4.5 20051201 (Red Hat 3.4.5-2)
  Result:
  
使用
vsprintf時:fmt的長度大於1024時出現: 段錯誤fmt的長度小於1024時出現: 正常使用vsnprintf時:fmt的長度大於1024時出現: 多餘的字元不儲存到outfmt的長度小於1024時出現: 正常
  vsnprintf
的返回值同snprintf很相似

  ---------------------------------------------------
  Test Env:
    Microsoft Windows XP [
版本 5.1.2600]
    Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8168 for 80x86
  Result:
  
使用_vsprintf時:fmt的長度大於1024時出現: 段錯誤fmt的長度小於1024時出現: 正常使用_vsnprintf時:fmt的長度大於1024時出現: 多餘的字元不儲存到outfmt的長度小於1024時出現: 正常
  _vsnprintf
的返回值同_snprintf很相似
   */

  va_start(ap, fmt);

  t = vsnprintf(out, 1024, fmt, ap);

  va_end(ap);

  printf("%s,%s(),%d: %s/n", file,fun,line,out);

  return (t);

}

#define myprint(str)  my_print(__FILE__,__FUNCTION__,__LINE__,str)

只能輸出一個字串引數。啥也別說了,這個肯定很爛,這裡主要是記住這個變參函式的實現方式。

2)新的C99規範支援了可變引數的巨集,具體使用如下:

#include   <stdarg.h>

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

3)這個很雷人:(‘_’來代替‘,’,否則報錯或者警告實參太多或者實參個數不一致)

#define   _   , 

#define   mysprintf(geter, fmt, args)   sprintf(geter, fmt, args)  

{

   char str[128] = "";

   mysprintf(str, "name=%s age=%d weight=%f" _ "weide001" _ 23 _ 57.9);//呼叫很累,和以往的方式出入太大

}

4)使用##

#define   mysprintf(geter)   sprintf##geter 

{

  char str[128] = "";

  mysprintf(  (str,"name=%s age=%d weight=%f","weide001",23,57.9)  );//也有點雷人,呼叫時多出來了一層括弧

}

可變引數的巨集裡的 ## 作用

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

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

但是在debug可變引數為0的時候,debug("hello /n"),編譯會出錯,採用這樣的方式:

#define debug(format, ...) fprintf (stderr, format, ##args)就可以

##的用法,文中是這樣解釋的:“這裡,假如可變引數被忽略或為空,‘##’操作將使前處理器(preprocessor)去除掉它前面的那個逗號。””這句話不明白,是##的新功能,還是原有連線的功能的應用?

其實,## 是粘連符

比如windows #define __Ttext L##text

就是在text前面加上了一個L

__T("abc")就成了 L"abc"

可變引數及可變引數巨集的使用

我們在C語言程式設計中會遇到一些引數個數可變的函式,例如printf()這個函式,這裡將介紹可變函式的寫法以及原理.

* 1. 可變引數的巨集

一般在除錯列印Debug 資訊的時候, 需要可變引數的巨集. C99開始可以使編譯器標準支援可變引數巨集(variadic macros), 另外GCC 也支援可變引數巨集, 但是兩種在細節上可能存在區別.

1. __VA_ARGS__

__VA_ARGS__ "..." 傳遞給巨集.
#define debug(format, ...) fprintf(stderr, fmt, __VA_ARGS__)

GCC中也支援這類表示, 但是在G++ 中不支援這個表示.

2. GCC 的複雜巨集

GCC使用一種不同的語法從而可以使你可以給可變引數一個名字,如同其它引數一樣。

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

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

3. ##__VA_ARGS__

上面兩個定義的巨集, 如果出現debug("A Message") 的時候, 由於巨集展開後有個多餘的逗號, 所以將導致編譯錯誤. 為了解決這個問題,CPP使用一個特殊的‘##’操作。

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

這裡,如果可變引數被忽略或為空,‘##’操作將使前處理器(preprocessor)去除掉它前面的那個逗號。如果你在巨集呼叫時,確實提供了一些可變引數,GNU CPP也會工作正常,它會把這些可變引數放到逗號的後面。

4. 其他方法

一種流行的技巧是用一個單獨的用括弧括起來的的 "引數" 定義和呼叫巨集, 引數在巨集擴充套件的時候成為類似 printf() 那樣的函式的整個引數列表。

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

* 2. 可變引數的函式

寫可變引數的C函式要在程式中用到以下這些巨集:
void va_start( va_list arg_ptr, prev_param )
type va_arg( va_list arg_ptr, type )
void va_end( va_list arg_ptr )

va在這裡是variable-argument(可變引數)的意思,這些巨集定義在stdarg.h.下面我們寫一個簡單的可變引數的函式,該函式至少有一個整數引數,第二個引數也是整數,是可選的.函式只是列印這兩個引數的值.
void simple_va_fun(int i, ...)
{
   va_list arg_ptr;
   int j=0;
  
   va_start(arg_ptr, i);
   j=va_arg(arg_ptr, int);
   va_end(arg_ptr);
   printf("%d %d/n", i, j);
   return;
}

在程式中可以這樣呼叫:
simple_va_fun(100);
simple_va_fun(100,200);

從這個函式的實現可以看到,使用可變引數應該有以下步驟:
1)
首先在函式裡定義一個va_list型的變數,這裡是arg_ptr,這個變數是指向引數的指標.
2)
然後用va_start巨集初始化變數arg_ptr,這個巨集的第二個引數是第一個可變引數的前一個引數,是一個固定的引數.
3)
然後用va_arg返回可變的引數,並賦值給整數j. va_arg的第二個引數是你要返回的引數的型別,這裡是int.
4)
最後用va_end巨集結束可變引數的獲取.然後你就可以在函式裡使用第二個引數了.如果函式有多個可變引數的,依次呼叫va_arg獲取各個引數.

如果我們用下面三種方法呼叫的話,都是合法的,但結果卻不一樣:
1)simple_va_fun(100);
結果是:100 -123456789(會變的值)
2)simple_va_fun(100,200);
結果是:100 200
3)simple_va_fun(100,200,300);
結果是:100 200

我們看到第一種呼叫有錯誤,第二種呼叫正確,第三種調用盡管結果正確,但和我們函式最初的設計有衝突.下面一節我們探討出現這些結果的原因和可變引數在編譯器中是如何處理的.
* 3.
可變引數函式原理

va_start,va_arg,va_end是在stdarg.h中被定義成巨集的,由於硬體平臺的不同,編譯器的不同,所以定義的巨集也有所不同,下面以VC++stdarg.hx86平臺的巨集定義摘錄如下:

typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )

定義_INTSIZEOF(n)主要是為了記憶體對齊,C語言的函式是從右向左壓入堆疊的(設資料進棧方向為從高地址向低地址發展,即首先壓入的資料在高地址). 下圖是函式的引數在堆疊中的分佈位置:

低地址|-----------------------------|<-- &v

       |n-1個引數(最後一個固定引數)|

       |-----------------------------|<--va_startap指向

       |n個引數(第一個可變引數) |

       |-----------------------------|

       |....... |

       |-----------------------------|
       |
函式返回地址 |

高地址|-----------------------------|

1. va_list 被定義為char *
2. va_start
將地址ap定義為 &v+_INTSIZEOF(v),&v是固定引數在堆疊的地址,所以va_start(ap, v)以後,ap指向第一個可變引數在堆疊的地址
3. va_arg
取得型別t的可變引數值,int型為例,va_argint型的返回值:
   j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
4. va_end
使ap不再指向堆疊,而是跟NULL一樣.這樣編譯器不會為va_end產生程式碼.

在不同的作業系統和硬體平臺的定義有些不同,但原理卻是相似的.

* 4. 小結

對於可變引數的函式,因為va_start, va_arg, va_end等定義成巨集,所以它顯得很愚蠢,可變引數的型別和個數需要在該函式中由程式程式碼控制;另外,編譯器對可變引數的函式的原型檢查不夠嚴格,對程式設計查錯不利.
所以我們寫一個可變函式的C函式時,有利也