• 正文
    • 追本溯源
    • 實(shí)際應(yīng)用
    • 更進(jìn)一步
  • 相關(guān)推薦
申請入駐 產(chǎn)業(yè)圖譜

【LeafC】C語言之宏魔法2:NARG

01/22 09:03
1416
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

追本溯源

這期主題是NARG宏,它被用于計(jì)算宏的變長參數(shù)的個數(shù)。所以這篇文章我想從C語言的變長參數(shù)聊起,先說說C語言變長參數(shù)應(yīng)用,以及函數(shù)和宏的變長參數(shù)的原理,這樣在介紹NARG時,我們不僅能夠知道它是如何實(shí)現(xiàn)的,還能更好地學(xué)以致用。

| 函數(shù)的變長參數(shù)

變長參數(shù)看起來很高級,但當(dāng)C語言初學(xué)者第一次敲下Hello World時,就已經(jīng)在不知不覺中使用了這個重要的特性。

printf("Hello World");

printf函數(shù)是一個帶變長參數(shù)的函數(shù),它的定義如下:

static char buffer[BUFF_SIZE];
int printf(const char *fmt, ...)
{
    va_list args;
    int n;
    va_start(args, fmt);
    n = vsprintf(buffer, fmt, args);
    va_end(args);
    // OUTPUT TO DEVICE
    return n;
}

讓我們逐行解析這個函數(shù):

  1. ...:變長參數(shù)形參
  2. va_list args:定義一個指向形參列表的指針
  3. va_start(args, fmt):根據(jù)fmt形參獲取函數(shù)棧幀中可變參數(shù)列表的地址,并初始化args

當(dāng)調(diào)用printf函數(shù)時,其函數(shù)棧幀中包含形參列表,完整函數(shù)棧幀如下:
局部變量【棧頂】
形參1
...
形參n
函數(shù)返回地址
相關(guān)寄存器備份
函數(shù)返回值【棧底】

  1. vsprintf:將args變長參數(shù)列表按照fmt字符串中的規(guī)則解析,并將結(jié)果填到buffer中,返回值為填入buffer的字節(jié)數(shù)

拓展知識:
vsprintf也可以使用安全函數(shù)vsnprintf,后者多一個表示buffer大小的參數(shù)。
嵌入式環(huán)境中,通常在OUTPUT TO DEVICE位置會將buffer中數(shù)據(jù)輸出到串口或者其他輸出設(shè)備;在帶文件系統(tǒng)的操作系統(tǒng)中,vsprintf函數(shù)會被vfprintf取代,數(shù)據(jù)輸出結(jié)果不填入buffer,而是填入stdout標(biāo)準(zhǔn)輸出流中(如命令行程序打印到屏幕上)

  1. va_end(args):將args置為NULL

細(xì)心的讀者可能會提出疑問,printf函數(shù)使用變長參數(shù)時,為何不需要知道變長參數(shù)的個數(shù)呢?

其實(shí)C語言中函數(shù)的變長參數(shù)個數(shù)是需要通過入?yún)魅氲?,函?shù)的棧幀中也確實(shí)不會自動生成變長參數(shù)個數(shù)的信息,只不過printf函數(shù)的入?yún)?span style="color: #ff9900;">fmt中已間接包含了變長參數(shù)個數(shù)信息,即有多少個%。

如果要實(shí)現(xiàn)一個不定長參數(shù)的my_sum求和函數(shù),那就只能多加一個代表變長參數(shù)個數(shù)的入?yún)⒘耍?/p>

// 約束:變長入?yún)閕nt類型
int my_sum(int num, ...)
{
    va_list args;
    int sum = 0;
    va_start(args, num);
    while (num--) {
        sum += va_arg(args, int);
    }
    va_end(args);
    return sum;
}

// 調(diào)用
int sum = my_sum(3, 1, 2, 3); // sum = 6

但是,有沒有辦法優(yōu)化這個my_sum函數(shù),讓程序在編譯時計(jì)算變長參數(shù)個數(shù),而非手動填寫呢?

| 宏的變長參數(shù)

與函數(shù)的變長參數(shù)不同的是,宏只作用在預(yù)編譯階段,即對宏的變長參數(shù)的操作在代碼運(yùn)行之前,不會造成運(yùn)行時開銷。

下面是使用NARG宏優(yōu)化后的my_sum函數(shù):

#define ARG_N(_0, _1, _2, _3, _4, _5, ...) _5
#define NARG(...) ARG_N(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define MY_SUM(...) my_sum(NARG(__VA_ARGS__), ...)
// 調(diào)用
int sum = MY_SUM(1, 2, 3); // sum = 6

我們來逐行解析MY_SUM的實(shí)現(xiàn):

  1. ARG_N:定義一個含N+1個固定參數(shù)以及最后一個為變長參數(shù)的宏,其值為最后一個固定參數(shù)_N(這里N取5,最大支持計(jì)算5個參數(shù)數(shù)量)
  2. NARG:利用ARG_N值為最后一個固定參數(shù)的特性,將變長參數(shù)放參數(shù)列表開頭,后續(xù)參數(shù)為從N0遞減,巧妙地讓變長參數(shù)個數(shù)與ARG_N宏的值相等
  3. MY_SUM:利用NARG在預(yù)編譯時計(jì)算出參數(shù)個數(shù),填入my_sum函數(shù)第一個參數(shù)

宏的變長參數(shù)在使用時有2種方式:
1、__VA_ARGS__:可以不搭配固定參數(shù)使用,但變長參數(shù)個數(shù)需大于0
2、##__VA_ARGS__:必須搭配至少一個固定參數(shù)使用,且變長參數(shù)個數(shù)可為0

以下為__VA_ARGS__##__VA_ARGS__合法和非法的使用方式:

#define MACRO1(...) func(__VA_ARGS__)
#define MACRO2(arg, ...) func(__VA_ARGS__)
#define MACRO3(arg, ...) func(##__VA_ARGS__)
// 合法
MACRO1(1)
MACRO2(1,2)
MACRO3(1)
MACRO3(1,2)
// 非法
MACRO1()
MACRO2(1)

實(shí)際應(yīng)用

開源項(xiàng)目googletest中的googlemock就使用了NARG,以及除此之外的超多宏魔法,這是由于其需要對外部輸入做很多的編譯時合法性檢查。在后續(xù)的宏魔法系列文章中,可能會再次引用googlemock中的實(shí)現(xiàn)思路。

更進(jìn)一步

上述MY_SUM宏通過調(diào)用my_sum這個帶變長參數(shù)函數(shù)實(shí)現(xiàn)了不定個數(shù)數(shù)字求和。假如MY_SUM宏的參數(shù)均為常量,有沒辦法繼續(xù)優(yōu)化,使程序完全在編譯時計(jì)算呢?

#define SUM1(a1) (a1)
#define SUM2(a1, a2) (SUM1(a1) + (a2))
#define SUM3(a1, a2, a3) (SUM2(a1, a2) + (a3))
#define SUM4(a1, a2, a3, a4) (SUM3(a1, a2, a3) + (a4))

#define SUM_N(_1, _2, _3, _4, NAME, ...) NAME
#define MY_SUM(...) SUM_N(__VA_ARGS__, SUM4, SUM3, SUM2, SUM1)(__VA_ARGS__)

// 調(diào)用
int sum = MY_SUM(1, 2, 3); // sum = 6

以上MY_SUM實(shí)現(xiàn)原理留給讀者思考,其原理與NARG宏如出一轍。

原理:
將一個前為固定參數(shù),后為變長參數(shù)的前定后變宏,傳入一個前為變長參數(shù),后為固定參數(shù)的前變后定參數(shù)列表,利用前定后變宏固定參數(shù)位置不變的性質(zhì),按照規(guī)則排列好前變后定參數(shù)列表,完成參數(shù)列表的選擇。

相關(guān)推薦

登錄即可解鎖
  • 海量技術(shù)文章
  • 設(shè)計(jì)資源下載
  • 產(chǎn)業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄