C/C++ 宏詳解

來源:互聯網
上載者:User

http://tech.e800.com.cn/articles/2009/727/1248665385863_1.html

眾多C++書籍都忠告我們C語言宏是萬惡之首,但事情總不如我們想象的那麼壞,就如同goto一樣。宏有
一個很大的作用,就是自動為我們產生代碼。如果說模板可以為我們產生各種型別的代碼(型別替換),
那麼宏其實可以為我們在符號上產生新的代碼(即符號替換、增加)。

關於宏的一些文法問題,可以在google上找到。相信我,你對於宏的瞭解絕對沒你想象的那麼多。如果你
還不知道#和##,也不知道prescan,那麼你肯定對宏的瞭解不夠。

我稍微講解下宏的一些文法問題(說文法問題似乎不妥,macro只與preprocessor有關,跟語義分析又無關):

1. 宏可以像函數一樣被定義,例如:
   #define min(x,y) (x    但是在實際使用時,只有當寫上min(),必須加括弧,min才會被作為宏展開,否則不做任何處理。
  
2. 如果宏需要參數,你可以不傳,編譯器會給你警告(宏參數不夠),但是這會導致錯誤。如C++書籍中所描
   述的,編譯器(前置處理器)對宏的語法檢查不夠,所以更多的檢查性工作得你自己來做。

3. 很多程式員不知道的#和##
   #符號把一個符號直接轉換為字串,例如:
   #define STRING(x) #x
   const char *str = STRING( test_string ); str的內容就是"test_string",也就是說#會把其後的符號
   直接加上雙引號。
   ##符號會串連兩個符號,從而產生新的符號(詞法層次),例如:
   #define SIGN( x ) INT_##x
   int SIGN( 1 ); 宏被展開後將成為:int INT_1;

4. 變參宏,這個比較酷,它使得你可以定義類似的宏:
   #define LOG( format, ... ) printf( format, __VA_ARGS__ )
   LOG( "%s %d", str, count );
   __VA_ARGS__是系統預定義宏,被自動替換為參數列表。

5. 當一個宏自己調用自己時,會發生什嗎?例如:
   #define TEST( x ) ( x + TEST( x ) )
   TEST( 1 ); 會發生什嗎?為了防止無限制遞迴展開,文法規定,當一個宏遇到自己時,就停止展開,也就是
   說,當對TEST( 1 )進行展開時,展開過程中又發現了一個TEST,那麼就將這個TEST當作一般的符號。TEST(1)
   最終被展開為:1 + TEST( 1) 。

6. 宏參數的prescan,
   當一個宏參數被放進宏體時,這個宏參數會首先被全部展開(有例外,見下文)。當展開後的宏參數被放進宏體時,
   前置處理器對新展開的宏體進行第二次掃描,並繼續展開。例如:
   #define PARAM( x ) x
   #define ADDPARAM( x ) INT_##x
   PARAM( ADDPARAM( 1 ) );
   因為ADDPARAM( 1 ) 是作為PARAM的宏參數,所以先將ADDPARAM( 1 )展開為INT_1,然後再將INT_1放進PARAM。
  
   例外情況是,如果PARAM宏裡對宏參數使用了#或##,那麼宏參數不會被展開:
   #define PARAM( x ) #x
   #define ADDPARAM( x ) INT_##x
   PARAM( ADDPARAM( 1 ) ); 將被展開為"ADDPARAM( 1 )"。

   使用這麼一個規則,可以建立一個很有趣的技術:列印出一個宏被展開後的樣子,這樣可以方便你分析代碼:
   #define TO_STRING( x ) TO_STRING1( x )
   #define TO_STRING1( x ) #x
   TO_STRING首先會將x全部展開(如果x也是一個宏的話),然後再傳給TO_STRING1轉換為字串,現在你可以這樣:
   const char *str = TO_STRING( PARAM( ADDPARAM( 1 ) ) );去一探PARAM展開後的樣子。

7. 一個很重要的補充:就像我在第一點說的那樣,如果一個像函數的宏在使用時沒有出現括弧,那麼前置處理器只是
   將這個宏作為一般的符號處理(那就是不處理)。

我們來見識一下宏是如何協助我們自動產生代碼的。如我所說,宏是在符號層次產生代碼。我在分析Boost.Function
模組時,因為它使用了大量的宏(宏嵌套,再嵌套),導致我壓根沒看明白代碼。後來發現了一個小型的模板庫ttl,說的
是開發一些小型組件去取代部分Boost(這是一個好理由,因為Boost確實太大)。同樣,這個庫也包含了一個function庫。
這裡的function也就是我之前提到的functor。ttl.function庫裡為了自動產生很多類似的代碼,使用了一個宏:

#define TTL_FUNC_BUILD_FUNCTOR_CALLER(n)  \
 template< typename R, TTL_TPARAMS(n) > \
 struct functor_caller_base##n \
        ///...
該宏的最終目的是:通過類似於TTL_FUNC_BUILD_FUNCTOR_CALLER(1)的調用方式,自動產生很多functor_caller_base模板:
template struct functor_caller_base1
template struct functor_caller_base2
template struct functor_caller_base3
///...
那麼,核心部分在於TTL_TPARAMS(n)這個宏,可以看出這個宏最終產生的是:
typename T1
typename T1, typename T2
typename T1, typename T2, typename T3
///...
我們不妨分析TTL_TPARAMS(n)的整個過程。分析宏主要把握我以上提到的一些要點即可。以下過程我建議你翻著ttl的代碼,
相關代碼檔案:function.hpp, macro_params.hpp, macro_repeat.hpp, macro_misc.hpp, macro_counter.hpp。

so, here we go

分析過程,逐層分析,逐層展開,例如TTL_TPARAMS(1):

#define TTL_TPARAMS(n) TTL_TPARAMSX(n,T) 
=> TTL_TPARAMSX( 1, T )
#define TTL_TPARAMSX(n,t) TTL_REPEAT(n, TTL_TPARAM, TTL_TPARAM_END, t)
=> TTL_REPEAT( 1, TTL_TPARAM, TTL_TPARAM_END, T )
#define TTL_TPARAM(n,t) typename t##n,
#define TTL_TPARAM_END(n,t) typename t##n
#define TTL_REPEAT(n, m, l, p) TTL_APPEND(TTL_REPEAT_, TTL_DEC(n))(m,l,p) TTL_APPEND(TTL_LAST_REPEAT_,n)(l,p)
注意,TTL_TPARAM, TTL_TPARAM_END雖然也是兩個宏,他們被作為TTL_REPEAT宏的參數,按照prescan規則,似乎應該先將
這兩個宏展開再傳給TTL_REPEAT。但是,如同我在前面重點提到的,這兩個宏是function-like macro,使用時需要加括弧,
如果沒加括弧,則不當作宏處理。因此,展開TTL_REPEAT時,應該為:
=> TTL_APPEND( TTL_REPEAT_, TTL_DEC(1))(TTL_TPARAM,TTL_TPARAM_END,T) TTL_APPEND( TTL_LAST_REPEAT_,1)(
TTL_TPARAM_END,T)
這個宏體看起來很複雜,仔細分析下,可以分為兩部分:
TTL_APPEND( TTL_REPEAT_, TTL_DEC(1))(TTL_TPARAM,TTL_TPARAM_END,T)以及
TTL_APPEND( TTL_LAST_REPEAT_,1)(TTL_TPARAM_END,T)
先分析第一部分:
#define TTL_APPEND( x, y ) TTL_APPEND1(x,y) //先展開x,y再將x,y串連起來
#define TTL_APPEND1( x, y ) x ## y
#define TTL_DEC(n) TTL_APPEND(TTL_CNTDEC_, n)
根據先展開參數的原則,會先展開TTL_DEC(1)
=> TTL_APPEND(TTL_CNTDEC_,1) => TTL_CNTDEC_1
#define TTL_CNTDEC_1 0  注意,TTL_CNTDEC_不是宏,TTL_CNTDEC_1是一個宏。
=> 0 , 也就是說,TTL_DEC(1)最終被展開為0。回到TTL_APPEND部分:
=> TTL_REPEAT_0 (TTL_TPARAM,TTL_TPARAM_END,T)
#define TTL_REPEAT_0(m,l,p)
TTL_REPEAT_0這個宏為空白,那麼,上面說的第一部分被忽略,現在只剩下第二部分:
TTL_APPEND( TTL_LAST_REPEAT_,1)(TTL_TPARAM_END,T)
=> TTL_LAST_REPEAT_1 (TTL_TPARAM_END,T) // TTL_APPEND將TTL_LAST_REPEAT_和1合并起來
#define TTL_LAST_REPEAT_1(m,p) m(1,p)
=> TTL_TPARAM_END( 1, T )
#define TTL_TPARAM_END(n,t) typename t##n
=> typename T1  展開完畢。

雖然我們分析出來了,但是這其實並不是我們想要的。我們應該從那些宏裡去擷取作者關於宏的編程思想。很好地使用宏
看上去似乎是一些偏門的奇技淫巧,但是他確實可以讓我們編碼更自動化。

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.