【嵌入式】C语言高级编程-可变参数宏(12)
00. 目錄
文章目錄
- 00. 目錄
- 01. 可變參數宏概述
- 02. ##符號
- 03. 可變參宏另外一種寫法
- 04. 內核中的可變參數宏
- 05. 附錄
01. 可變參數宏概述
#include <stdarg.h>void va_start(va_list ap, last); type va_arg(va_list ap, type); void va_end(va_list ap); void va_copy(va_list dest, va_list src);Linux平臺可能的實現
/** Storage alignment properties*/ #define _AUPBND (sizeof (acpi_native_int) - 1) #define _ADNBND (sizeof (acpi_native_int) - 1)/** Variable argument list macro definitions*/ #define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd))) #define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) #define va_end(ap) (void) 0 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))其實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", );宏展開后,在第一個字符串參數的后面還有一個逗號,所以就產生了一個語法錯誤。我們需要繼續對這個宏進行改進,使用宏連接符 ##,來避免這個語法錯誤。
02. ##符號
宏連接符 ## 的主要作用就是連接兩個字符串,我們在宏定義中可以使用 ## 來連接兩個字符。預處理器在預處理階段對宏展開時,會將 ## 兩邊的字符合并,并刪除 ## 這兩個字符。
#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 后面的逗號刪除掉,這樣宏也就可以正常使用了。
03. 可變參宏另外一種寫法
當我們定義一個變參宏時,除了使用預定義標識符 __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__ 看起來更加直觀和方便。
04. 內核中的可變參數宏
可變參數宏在內核中主要用于日志打印。一些驅動模塊或子系統有時候會定義自己的打印宏,可以支持打印開關、打印格式、優先級控制等。如在 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");elseDEBUG();return 0; }運行結果
hello if else理論情況下,else 分支是執行不到的。但通過運行結果可以看到,程序也執行了 else 分支的一部分代碼。這是因為我們定義的宏由多條語句組成,直接展開后,就變成了下面這樣。
int main(void){if(1)printf("hello if\n");elseprintf("hello ");printf("else\n");return 0;}多條語句在宏調用處直接展開,就破壞了程序原來的 if-else 分支結構,導致程序邏輯發生變化,所以你才會看到 else 分支的非正常打印。而采用 do{ … }while(0) 這種結構,可以將我們宏定義中的復合語句包起來,宏展開后,就是一個代碼塊,就避免了這種邏輯錯誤。
一個小小的宏,暗藏各個知識點,綜合使用各種技巧,仔細分析下來,就能學到很多知識。大家在以后的工作和學習中,可能會接觸到各種各樣、形形色色的宏,只要我們有牢固的 C 語言基礎,熟悉 GNU C 的常用擴展語法,再遇到這樣類似的宏,我們都可以慢慢去分析了。不用怕,只有自己真正分析過,才算真正掌握,才能轉化為自己的知識和能力,才能領略它的精妙之處。
05. 附錄
參考:C語言嵌入式Linux高級編程
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的【嵌入式】C语言高级编程-可变参数宏(12)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【嵌入式】C语言高级编程-内建函数(11
- 下一篇: 【C语言】枚举类型