天天看點

嵌入式C語言自我修養 (12):有一種宏,叫可變參數宏

12.1 什麼是可變參數宏

在上面的教程中,我們學會了變參函數的定義和使用,基本套路就是使用

va_list

va_start

va_end

等宏,去解析那些可變參數清單我們找到這些參數的存儲位址後,就可以對這些參數進行處理了:要麼自己動手,自己處理;要麼繼續調用其它函來處理。

void print_num(int count, ...)
{
    va_list args;
    va_start(args,count);
    for(int i = 0; i < count; i++)
    {
        printf("*args: %d\n",*(int *)args);
        args += 4; 
    }
}
void __attribute__((format(printf,2,3))) LOG(int k,char *fmt,...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
}
           

GNU C 覺得這樣不過瘾,再來個猛錘:幹脆宏定義也支援變參吧!

這一節我們要學習一下可變參數宏的定義和使用。其實,C99 标準已經支援了這個特性,但是其它的編譯器不太給力,對 C99 标準的支援不是很好,隻有 GNU C 支援這個功能,是以有時候我們也把這個可變參數宏看作是 GNU C 的一個文法擴充。 上面的 LOG 函數,如果我們想使用一個變參宏實作,就可以直接這樣定義。

#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)
​
#define DEBUG(...) printf(__VA_ARGS__)
​
int main(void)
{
    LOG("Hello! I'm %s\n","Wanglitao");
    DEBUG("Hello! I'm %s\n","Wanglitao");
    return 0;
}
           

變參宏的實作形式其實跟變參函數差不多:用 ... 表示變參清單,變參清單由不确定的參數組成,各個參數之間用逗号隔開。可變參數宏使用 C99 标準新增加的一個

__VA_ARGS__

預定義辨別符來表示前面的變參清單,而不是像變參函數一樣,使用

va_list

va_start

va_end

這些宏去解析變參清單。預處理器在将宏展開時,會用變參清單替換掉宏定義中的所有

__VA_ARGS__

辨別符。

使用宏定義實作一個變參列印功能,你會發現,它的實作甚至比變參函數還友善!核心中的很多列印宏,經常使用可變參數宏來實作,宏定義一般為下面這個格式。

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

在這個宏定義中,有一個固定參數,通常為一個格式字元串,後面的變參用來列印各種格式的資料,跟前面的格式字元串相比對。這種定義方式有一個漏洞,即當變參為空時,宏展開時就會産生一個文法錯誤。

#define LOG(fmt,...) printf(fmt,__VA_ARGS__)
int main(void)
{
    LOG("hello\n");
    return 0;
}
           

上面這個程式編譯時就會通不過,産生一個文法錯誤。這是因為,我們隻給 LOG 宏傳遞了一個參數,而變參為空。當宏展開後,就變成了下面這個樣子。

printf("hello\n", );
           

宏展開後,在第一個字元串參數的後面還有一個逗号,是以就産生了一個文法錯誤。我們需要繼續對這個宏進行改進,使用宏連接配接符 ##,來避免這個文法錯誤。

12.2 繼續改進我們的宏

我們接下來,使用宏連接配接符 ## 來改進上面的宏。

宏連接配接符 ## 的主要作用就是連接配接兩個字元串,我們在宏定義中可以使用 ## 來連接配接兩個字元。預處理器在預處理階段對宏展開時,會将 ## 兩邊的字元合并,并删除 ## 這兩個字元。

#define A(x) a##x
int main(void)
{
    int A(1) = 2; //int a1 = 2;
    int A() = 3;  //int a=3;
    printf("%d %d\n",a1,a);
    return 0;   
}
           

如上面的程式,我們定義一個宏。

#define A(x) a##x
           

這個宏的功能就是連接配接字元 a 和 x。在程式中,A(1) 展開後就是 a1,A( ) 展開後就是 a。我們使用 printf( ) 函數可以直接列印變量 a1、a 的值,因為宏展開後,就相當于使用 int 關鍵字定義了兩個整型變量 a1 和 a。上面的程式可以編譯通過,運作結果如下。

2  3
           

知道了宏連接配接符 ## 的使用方法,我們接下來就可以就對 LOG 宏做一些修改。

#define LOG(fmt,...) printf(fmt, ##__VA_ARGS__)
int main(void)
{
    LOG("hello\n");
    return 0;
}
           

我們在辨別符

__VA_ARGS__

前面加上宏連接配接符 ##,這樣做的好處是,當變參清單非空時,## 的作用是連接配接 fmt,和變參清單,各個參數之間用逗号隔開,宏可以正常使用;當變參清單為空時,## 還有一個特殊的用處,它會将固定參數 fmt 後面的逗号删除掉,這樣宏也就可以正常使用了。

12.3 可變參數宏的另一種寫法

當我們定義一個變參宏時,除了使用預定義辨別符

__VA_ARGS__

表示變參清單外,還可以使用下面這種寫法。

#define LOG(fmt,args...) printf(fmt, args)
           

使用預定義辨別符

__VA_ARGS__

來定義一個變參宏,是 C99 标準規定的寫法。而上面這種格式是 GNU C 擴充的一個新寫法。我們不再使用

__VA_ARGS__

,而是直接使用 args... 來表示一個變參清單,然後在後面的宏定義中,直接使用 args 代表變參清單就可以了。

跟上面一樣,為了避免變參清單為空時的文法錯誤,我們也需要添加一個連接配接符##。

#define LOG(fmt,args...) printf(fmt,##args)
int main(void)
{
    LOG("hello\n");
    return 0;
}
           

使用這種方式,你會發現這種寫法比使用

__VA_ARGS__

看起來更加直覺和友善。

12.4 核心中的可變參數宏

可變參數宏在核心中主要用于日志列印。一些驅動子產品或子系統有時候會定義自己的列印宏,可以支援列印開關、列印格式、優先級控制等。如在 printk.h 頭檔案中,我們可以看到 pr_debug 宏的定義。

#if defined(CONFIG_DYNAMIC_DEBUG)
#define pr_debug(fmt, ...) \
    dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) \
    printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) \
    no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
​
#define dynamic_pr_debug(fmt, ...)                \
do {                                \
    DEFINE_DYNAMIC_DEBUG_METADATA(descriptor, fmt); \
    if (unlikely(descriptor.flags       \
            & _DPRINTK_FLAGS_PRINT))    \
        __dynamic_pr_debug(&descriptor, pr_fmt(fmt),    \
                   ##__VA_ARGS__);      \
} while (0)
​
static inline __printf(1, 2)
int no_printk(const char *fmt, ...)
{
    return 0;
}
​
#define __printf(a, b)    \   
__attribute__((format(printf, a, b)))
           

看到這個宏定義,不得不佩服宏的作者。一個小小的宏,綜合運用各種技巧和知識點,把 C 語言發揮到極緻!

這個宏定義了三個版本。如果我們在編譯核心時有動态調試選項,那麼這個宏就定義為 dynamicprdebug。如果沒有配置動态調試選項,那我們還可以通過 DEBUG 這個宏,來控制這個宏的打開和關閉。

no_printk() 作為一個内聯函數,定義在 printk.h 頭檔案中,而且通過 format 屬性聲明,訓示編譯器按照 printf 标準去做參數格式檢查。

最有意思的是 dynamicprdebug 宏,宏定義采用 do{ ... }while(0) 結構。這看起來貌似有點多餘,有它沒它,我們的宏都可以工作。反正都是執行一次,為什麼要用這種看似“畫蛇添足”的循環結構呢?道理很簡單,這樣定義就是為了防止宏在條件、選擇等分支結構的語句中展開後,産生宏歧義。

比如我們定義一個宏,由兩條列印語句構成。

#define DEBUG() \
 printf("hello ");printf("else\n")
​
int main(void)
{
    if(1)
        printf("hello if\n");
    else
        DEBUG();
    return 0;
}
           

程式運作結果如下。

hello if
else
           

理論情況下,else 分支是執行不到的。但通過運作結果可以看到,程式也執行了 else 分支的一部分代碼。這是因為我們定義的宏由多條語句組成,直接展開後,就變成了下面這樣。

int main(void)
    {
        if(1)
            printf("hello if\n");
        else
            printf("hello ");
            printf("else\n");
        return 0;
    }
           

多條語句在宏調用處直接展開,就破壞了程式原來的 if-else 分支結構,導緻程式邏輯發生變化,是以你才會看到 else 分支的非正常列印。而采用 do{ ... }while(0) 這種結構,可以将我們宏定義中的複合語句包起來,宏展開後,就是一個代碼塊,就避免了這種邏輯錯誤。

一個小小的宏,暗藏各個知識點,綜合使用各種技巧,仔細分析下來,就能學到很多知識。大家在以後的工作和學習中,可能會接觸到各種各樣、形形色色的宏,隻要我們有牢固的 C 語言基礎,熟悉 GNU C 的常用擴充文法,再遇到這樣類似的宏,我們都可以慢慢去分析了。不用怕,隻有自己真正分析過,才算真正掌握,才能轉化為自己的知識和能力,才能領略它的精妙之處。

本文根據《C語言嵌入式Linux進階程式設計》部分章節改編,視訊學習可通路CSDN學院:https://edu.csdn.net/combo/detail/1038

微信公衆号:宅學部落(armlinuxfun)

QQ群:475504428

更多嵌入式視訊教程:https://wanglitao.taobao.com

電子書籍下載下傳位址:https://pan.baidu.com/s/1a6L0cyIQKKLlmIfRw7U6Dg

繼續閱讀