簡體   English   中英

為什么要用C語言中的其他宏遞歸地定義宏?

[英]Why would you recursively define macros in terms of other macros in C?

我想看看arduino函數digitalWrite實際上是如何工作的。 但是當我查找函數的源代碼時,它充滿了宏,這些宏本身是根據其他宏定義的。 為什么會以這種方式構建而不是僅使用函數? 這只是糟糕的編碼風格還是在C中做事的正確方式?

例如, digitalWrite包含宏digitalPinToPort

#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )

pgm_read_byte是一個宏:

#define pgm_read_byte(address_short)    pgm_read_byte_near(address_short)

並且pgm_read_byte_near是一個宏:

#define pgm_read_byte_near(address_short) __LPM((uint16_t)(address_short))

__LPM是一個宏:

#define __LPM(addr)         __LPM_classic__(addr)

__LPM_classic__是一個宏:

#define __LPM_classic__(addr)   \
(__extension__({                \
    uint16_t __addr16 = (uint16_t)(addr); \
    uint8_t __result;           \
    __asm__ __volatile__        \
    (                           \
        "lpm" "\n\t"            \
        "mov %0, r0" "\n\t"     \
        : "=r" (__result)       \
        : "z" (__addr16)        \
        : "r0"                  \
    );                          \
    __result;                   \
}))

與此沒有直接關系,但我也認為雙下划線只應由編譯器使用。 LPM前綴是__正確嗎?

如果您的問題是“為什么會使用多層宏?”,那么:

  • 為什么不? 特別是在沒有inline的C99之前的時代。 一個典型的例子是20世紀80年代時代的getc (IIRC)當時(SunOS3.2,1987)被記錄為一個宏,在man頁中有一個注釋告訴我(我忘了細節)有一些FILE* filearr[]; getc(filearr[i++])錯了(IIRC,當時不存在未定義的行為術語)。 當你查看一些系統頭文件(例如<stdio.h>或它包含的某些頭文件)時,你可以找到這種宏的定義。 那個時候 (計算機運行getc頻率,比現在慢一千倍) 出於效率原因, getc 必須是一個宏(因為inline不存在,編譯器沒有像他們現在能做的那樣進行過程間優化 )。 當然,您可以在自己的宏中使用getc
  • 即使在今天,一些標准也定義了宏。 尤其是今天的waitpid函數(2)系統調用文件WIFEXITEDWEXITSTATUS如宏,這是明智的#define你的一些宏混合兩者。
  • 重點是理解C預處理器的工作原理及其深刻的文本(非常脆弱 )性質。 這在所有關於C的教科書中都有解釋。因此,您需要了解幕后發生的事情。
  • 現代時代C(即C99和C11)的經驗法則是系統地更喜歡將一些static inline函數(在某些頭文件中定義)轉換為等效的宏。 換句話說, #define只有一些宏,當你無法避免它。 並明確記錄這一事實。
  • 幾層宏可能(有時)提高代碼的可讀性。
  • 可以使用#ifdef macroname測試宏,這有時很有用。

當然,當你敢於定義幾層宏(我不會稱之為“遞歸”,閱讀自引用宏 )時,你需要非常小心並理解所有這些宏的結果 (組合和單獨)。 查看預處理的表單很有幫助。

順便說一下,為了調試復雜的宏,我有時會這樣做

gcc -C -E -I/some/directory mysource.c | sed 's:^#://#:' > mysource.i

然后我查看mysource.i ,有時甚至必須編譯它,也許作為gcc -c -Wall mysource.i來獲取位於預處理形式mysource.i (我可以在我的emacs編輯器中查看)。 sed命令是“評論”的開頭的行#這是設置源位置(點菜#line )......有時候我甚至做indent mysource.i

(實際上,我的Makefile有一個特殊的規則)

LPM的前綴是__正確嗎?

順便說一句,以_開頭的名稱(按標准,通常)保留給實現。 原則上,您不允許以_開頭的名稱,因此不會發生任何沖突。

請注意__LPM_classic__宏使用statement-expr擴展名GCCClang

另請參閱其他編程語言。 Common Lisp有一個非常不同的宏模型(更有趣的一個)。 閱讀有關衛生宏的信息 我個人感到遺憾的是,C預處理器從未發展成為更強大的(和Scheme類似)。 為什么沒有發生這種情況(想象一下C預處理器能夠調用Guile代碼進行宏擴展!)可能是出於社會和經濟原因。

有時仍應考慮使用其他預處理器(如m4GPP )來生成 C代碼。 autoconf

為什么會以這種方式構建而不是僅使用函數?

目的很可能是使用前C99編譯器優化函數調用,這些編譯器不支持內聯函數(通過inline關鍵字)。 這樣,整個類似函數的宏堆棧基本上由預處理器合並為單個代碼塊。

每次在C中調用函數時,由於跳過程序代碼,管理堆棧幀和傳遞參數,所以開銷很小。 在大多數應用程序中,此成本可以忽略不計,但如果經常調用該函數,則可能會成為性能瓶頸。

這只是糟糕的編碼風格還是在C中做事的正確方式?

很難給出明確的答案,因為編碼風格是主觀話題。 我會說,考慮使用內聯函數甚至(更好)讓編譯器自己內聯它們。 它們是類型安全的,更易讀的,更可預測的,並且在編譯器的適當幫助下,最終結果基本相同。

相關參考(它適用於C ++,但C的想法通常是相同的):

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM