C語言宏定義的特殊用法以及避坑指南

2021-02-16 技術讓夢想更偉大

總結一下C語言中宏的一些特殊用法和幾個容易踩的坑。由於本文主要參考GCC文檔,某些細節(如宏參數中的空格是否處理之類)在別的編譯器可能有細微差別,請參考相應文檔。

宏基礎

宏僅僅是在C預處理階段的一種文本替換工具,編譯完之後對二進位代碼不可見。基本用法如下:

#define BUFFER_SIZE 1024

預處理階段

foo = (char *) malloc (BUFFER_SIZE);

會被替換成

foo = (char *) malloc (1024);

宏體換行需要在行末加反斜槓\

#define NUMBERS 1, \
                2, \
                3

預處理階段

int x[] = { NUMBERS };

會被擴展成

int x[] = { 1, 2, 3 };

宏名之後帶括號的宏被認為是宏函數。用法和普通函數一樣,只不過在預處理階段,宏函數會被展開。

優點是沒有普通函數保存寄存器和參數傳遞的開銷,展開後的代碼有利於CPU cache的利用和指令預測,速度快。

缺點是可執行代碼體積大。

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

那麼

y = min(1, 2);

會被擴展成

y = ((1) < (2) ? (1) : (2));

宏特殊用法

在宏體中,如果宏參數前加個#,那麼在宏體擴展的時候,宏參數會被擴展成字符串的形式。如:

#define WARN_IF(EXP) \
     do { if (EXP) \
             fprintf (stderr, "Warning: " #EXP "\n"); } \
     while (0)
WARN_IF (x == 0);

會被擴展成:

do { if (x == 0)
    fprintf (stderr, "Warning: " "x == 0" "\n"); }
while (0);

這種用法可以用在assert中,如果斷言失敗,可以將失敗的語句輸出到反饋信息中

在宏體中,如果宏體所在標示符中有##,那麼在宏體擴展的時候,宏參數會被直接替換到標示符中。如:

#define COMMAND(NAME)  { #NAME, NAME ## _command }

那麼

struct command
{
    char *name;
    void (*function) (void);
};

在宏擴展的時候

struct command commands[] =
{
    COMMAND (quit),
    COMMAND (help),
    ...
};

會被擴展成:

struct command commands[] =
{
    { "quit", quit_command },
    { "help", help_command },
    ...
};

這樣就節省了大量時間,提高效率。

幾個坑

由於是純文本替換,C預處理器不對宏體做任何語法檢查,像缺個括號、少個分號什麼的預處理器是不管的。

這裡要格外小心,由此可能引出各種奇葩的問題,一下還很難找到根源。

不僅宏體是純文本替換,宏參數也是純文本替換。有以下一段簡單的宏,實現乘法:

#define MULTIPLY(x, y) x * y

MULTIPLY(1, 2)沒問題,會正常展開成1 * 2。

有問題的是這種表達式MULTIPLY(1+2, 3),展開後成了1+2 * 3,顯然優先級錯了。

在宏體中,給引用的參數加個括號就能避免這問題。

#define MULTIPLY(x, y) (x) * (y)

MULTIPLY(1+2, 3)就會被展開成(1+2) * (3),優先級正常了。

其實這個問題和下面要說到的某些問題都屬於由於純文本替換而導致的語義破壞問題,要格外小心。

有如下宏定義:

#define SKIP_SPACES(p, limit)  \
     { char *lim = (limit);         \
       while (p < lim) {            \
         if (*p++ != ' ') {         \
           p--; break; }}}

假設有如下一段代碼:

if (*p != 0)
   SKIP_SPACES (p, lim);
else ...

一編譯,GCC報

error: 『else』 without a previous 『if』

原來這個看似是一個函數的宏被展開後是一段大括號括起來的代碼塊,加上分號之後這個if邏輯塊就結束了,所以編譯器發現這個else沒有對應的if。

這個問題一般用do ... while(0)的形式來解決:

#define SKIP_SPACES(p, limit)     \
     do { char *lim = (limit);         \
          while (p < lim) {            \
            if (*p++ != ' ') {         \
              p--; break; }}}          \
     while (0)

展開後就成了

if (*p != 0)
    do ... while(0);
else ...

這樣就消除了分號吞噬問題。

這個技巧在Linux內核源碼裡很常見,比如這個置位宏

#define SET_REG_BIT(reg, bit) \
do {                          \
  (reg |= (1 << (bit)));      \
} while (0)

位於arch/mips/include/asm/mach-pnx833x/gpio.h

有如下宏定義:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

當有如下調用時

next = min (x + y, foo (z));

宏體被展開成

next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));

可以看到,foo(z)被重複調用了兩次,做了重複計算。

更嚴重的是,如果foo是不可重入的(foo內修改了全局或靜態變量),程序會產生邏輯錯誤。

所以,儘量不要在宏參數中傳入函數調用。

有如下宏定義:

#define foo (4 + foo)

按前面的理解,(4 + foo)會展開成(4 + (4 + foo)),然後一直展開下去,直至內存耗盡。

但是,預處理器採取的策略是只展開一次。也就是說,foo只會展開成(4 + foo),而展開之後foo的含義就要根據上下文來確定了。

對於以下的交叉引用,宏體也只會展開一次。

#define x (4 + y)
#define y (2 * x)

x展開成(4 + y) -> (4 + (2 * x));

y展開成(2 * x) -> (2 * (4 + y))。

注意,這是極不推薦的寫法,程序可讀性極差。

宏參數中若包含另外的宏,那麼宏參數在被代入到宏體之前會做一次完全的展開,除非宏體中含有#或##。

有如下宏定義:

#define AFTERX(x) X_ ## x
#define XAFTERX(x) AFTERX(x)
#define TABLESIZE 1024
#define BUFSIZE TABLESIZE

AFTERX(BUFSIZE)會被展開成X_BUFSIZE。因為宏體中含有##,宏參數直接代入宏體。

XAFTERX(BUFSIZE)會被展開成X_1024。

因為XAFTERX(x)的宏體是AFTERX(x),並沒有#或##,所以BUFSIZE在代入前會被完全展開成1024,然後才代入宏體,變成X_1024。

參考資料:

http://gcc.gnu.org/onlinedocs/cpp/Macros.html

相關焦點

  • C語言中幾種特殊標準定義和用法
    除了大家說的PHP,其實,C語言也是世界上最好的語言。C語言已經連續幾個月佔比 TIOBE 榜首,成為最受歡迎的程式語言了。 C語言之所以那麼受歡迎,除了歷史悠久之外,還有他它具有一些程式語言沒有的功能。那麼,今年我們就來了解一下C語言的一些特殊功能。
  • 《邊學邊用攻破C語言》第17集 宏定義define的用法
    《邊學邊用攻破C語言》是專門為單片機初學者準備的C語言基礎視頻教學課程,是科技老頑童
  • C語言宏定義
    #define N 100就是宏定義,N為宏名,100是宏的內容。在編譯預處理時,對程序中所有出現的「宏名」,都用宏定義中的字符串去代換,這稱為「宏代換」或「宏展開」。宏定義是由源程序中的宏定義命令#define完成的,宏代換是由預處理程序完成的。
  • 快速上手系列-C語言之預編譯命令、宏定義及條件編譯
    上一篇寫了C語言中變量的存儲類別,提到普通局部變量、普通全局變量和靜態局部變量及靜態全局變量,這裡簡單了解一下C語言的預編譯命令、宏定義和條件編譯。預編譯命令(預編譯處理--->編譯---->彙編--->連接)1、預處理:預處理是C語言的一個重要功能,如文件包含、常量定義都屬於預處理命令,C語言提供的預處理功能主要有以下三種:1)文件包含 #include2)宏定義 #define3)條件編譯 #if #endif4)防止頭文件重複包含2、文件包含處理
  • C語言中宏定義的用法
    說到宏定義,我們應該先了解什麼是預處理指令,相信大家並不會陌生,之前我們編程時,程序的開頭#include指令,#define指令都是預處理指令,它能使我們的編譯更加的高效,便捷,因此C語言中是允許用戶自己加入一些特定的預處理指令的。
  • C語言陷阱與技巧第18節,函數式宏定義的「缺陷」,沒有參數類型檢查...
    在之前的文章裡,我們曾討論C語言程序開發中 define 宏定義的「陷阱」之一就是可能會產生多次「副作用」,這也是C語言中函數式宏定義與真正函數的主要區別之一。顯然,define 宏定義的這種「陷阱」會導致程序存在隱患,而且這種隱患造成的危害不亞於「野指針」。
  • 《C語言入門指南》中篇
    話所在前面:《C語言入門指南》,全文分為3篇。
  • C語言#define和typedef的用法區別,以及陷阱
    ,typedef 和 #define是最常用語句,可能很多工作過幾年的工程師都沒有去深究過它們的一些用法和區別。在C/C++語言中,typedef常用來定義一個標識符及關鍵字的別名,它是語言編譯過程的一部分,但它並不實際分配內存空間,比如:typedef int INT;typedef  (int*)   pINT;typedef unsigned int uint32_ttypedef可以增強程序的可讀性,以及標識符的靈活性,但它也有
  • 剖析c語言結構體的高級用法(二)
    (還有vs,這兩個編譯裡不能申明一個空的結構體,必須要有一個結構體成員來才行)寫成c語言程序空結構體的話,它會報錯,在新一點的編譯器裡面就不會報錯(比如dev,gcc)。說明(這裡是c++裡才這樣,在c語言裡輸出的結果不一樣的):
  • STM32中C語言知識點:初學者必看,老鳥複習(長文總結)
    STM32中的assert_param默認是不使用的,即:如果要使用,需要定義USE_FULL_ASSERT宏,並且需要自己實現assert_failed函數。特別的,使用STM32CubeMX生成代碼的話,會在main.c生成:
  • 《C語言入門指南》上篇
    話所在前面:《C語言入門指南》,全文分為3篇。此為上篇,涵蓋知識點為:發展史、快速入門、程序運行機制、基礎知識、常量、運算符、二進位和位運算、程序的控制結構、枚舉,上篇全文共計20000餘字,適用初學者入門C語言,非初學者也可以通過本文複習C語言相關知識點,強化記憶!十三發布這篇筆記也是為了複習C語言!
  • 如何寫出高效優美的單片機C語言代碼?
    8、其它比如使用在線彙編及將字符串和一些常量保存在程序存儲器中,均有利於優化C語言宏定義技巧(常用宏定義)寫好C語言,漂亮的宏定義很重要,使用宏定義可以防止出錯,提高可移植性,可讀性,方便性 等等。下面列舉一些成熟軟體中常用得宏定義。。。。。。
  • C語言strcmp和strcpy的用法
    一、c語言strcmp()用法原型:int strcmp(const char *s1, const char *s2);頭文件:#include
  • 長文 | 花了兩天時間整理了STM32中的一些C語言知識點,初學者福利!老鳥複習
    STM32中的assert_param默認是不使用的,即:如果要使用,需要定義USE_FULL_ASSERT宏,並且需要自己實現assert_failed函數。特別的,使用STM32CubeMX生成代碼的話,會在main.c生成:
  • C語言編程規範 clean code
    參考該規範之前,希望您具有相應的C語言基礎能力,而不是通過該文檔來學習C語言。了解C語言的ISO標準;熟知C語言的基本語言特性;了解C語言的標準庫;總體原則代碼需要在保證功能正確的前提下,滿足可讀、可維護、安全、可靠、可測試、高效、可移植的特徵要求。
  • c語言之共用體union、枚舉、大小端模式
    上一個專題我們詳細的分享了c語言裡面的結構體用法,讀者在看這些用法的時候,可以一邊看一邊試驗,掌握了這些基本用法就完全夠用了,當然在以後的工作中
  • OpTaliX 宏語言
    OpTaliX包含宏語言,允許自定義分析和計算。宏語言包括以下領域:在任何需要數字輸入項的地方使用算術表達式。
  • C語言中的 define預處理指令老手都是這樣用,你全都掌握了嗎?
    C語言有許多預處理命令,#define是其預處理命令之一。所有預處理命令以「#」號開頭,如包含命令#include,標準錯誤指令#error,#pragma指令等。#define指令用於宏定義,可以提高原始碼的可讀性,為編程提供方便,一般放在源文件的前面部分。本文簡要總結#define指令的多種用法及其注意事項。
  • C語言之#include用法詳解
    預編譯的時候copy include頭文件的內容到當前行(疑問:預編譯命令 $gcc -E test.c -o test.i)2.#include 「」 與<> 有何區別?如何驗證你的結論?
  • C語言中"#"和"##"的用法
    C語言中"#"和"##"的用法 3. 注意事項 3.1 舉例