溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

嵌入式C語(yǔ)言自我修養(yǎng) 08:變參函數(shù)的格式檢查

發(fā)布時(shí)間:2020-06-16 03:58:48 來(lái)源:網(wǎng)絡(luò) 閱讀:223 作者:宅學(xué)部落 欄目:系統(tǒng)運(yùn)維

8.1 屬性聲明:format

GNU 通過(guò) attribute 擴(kuò)展的 format 屬性,用來(lái)指定變參函數(shù)的參數(shù)格式檢查。

它的使用方法如下:

__attribute__(( format (archetype, string-index, first-to-check)))
void LOG(const char *fmt, ...)  __attribute__((format(printf,1,2)));

我們經(jīng)常實(shí)現(xiàn)一些自己的打印調(diào)試函數(shù)。這些打印函數(shù)往往是變參函數(shù),那編譯器編譯程序時(shí),怎么知道我們的參數(shù)格式對(duì)不對(duì)呢?因?yàn)槲覀儗?shí)現(xiàn)的是變參函數(shù),參數(shù)的個(gè)數(shù)和格式都不確定。所以編譯器表示壓力很大,不知道該如何處理。

辦法總是有的。這不,attribute 的format屬性這時(shí)候就自帶 BGM,隆重出場(chǎng)了。如上面的示例代碼,我們定義一個(gè) LOG 變參函數(shù),用來(lái)實(shí)現(xiàn)打印功能。那編譯器編譯程序時(shí),如何檢查我們參數(shù)的格式是否正確呢?其實(shí)很簡(jiǎn)單,通過(guò)給 LOG 函數(shù)添加 attribute((format(printf,1,2))) 這個(gè)屬性聲明,就是告訴編譯器:你知道printf函數(shù)不?你怎么對(duì)這個(gè)函數(shù)參數(shù)格式檢查的,就按同樣的方法,對(duì) LOG 函數(shù)進(jìn)行檢查。

屬性 format(printf,1,2) 有三個(gè)參數(shù)。第一個(gè)參數(shù) printf 是告訴編譯器,按照 printf 函數(shù)的檢查標(biāo)準(zhǔn)來(lái)檢查;第2個(gè)參數(shù)表示在 LOG 函數(shù)所有的參數(shù)列表中,格式字符串的位置索引;第3個(gè)參數(shù)是告訴編譯器要檢查的參數(shù)的起始位置。是不是沒(méi)看明白?舉個(gè)例子大家就明白了。

LOG("I am litao\n");
LOG("I am litao, I have %d houses!\n",0);
LOG("I am litao, I have %d houses! %d cars\n",0,0);

上面代碼,是我們的 LOG 函數(shù)使用示例。變參函數(shù),其參數(shù)個(gè)數(shù)跟 printf 函數(shù)一樣,是不固定的。那么編譯器如何檢查我們的打印格式是否正確呢?很簡(jiǎn)單,我們只需要將格式字符串的位置告訴編譯器就可以了,比如在第2行代碼中:

LOG("I am litao, I have %d houses!\n",0);

在這個(gè) LOG 函數(shù)中有2個(gè)參數(shù),第一個(gè)是格式字符串,第2個(gè)是要打印的一個(gè)常量值0,用來(lái)匹配格式字符串中的格式符。

什么是格式字符串呢?顧名思義,如果一個(gè)字符串中含有格式符,那這個(gè)字符串就是格式字符串。比如這個(gè)格式字符串:"I am litao, I have %d houses!\n",里面含有格式符%,我們也可以叫它占位符。打印的時(shí)候,后面變參的值會(huì)代替這個(gè)占位符,在屏幕上顯示出來(lái)。

我們通過(guò) format(printf,1,2) 屬性聲明,告訴編譯器:LOG 函數(shù)的參數(shù),格式字符串的位置在所有參數(shù)列表中的索引是1,即第一個(gè)參數(shù);要編譯器幫忙檢查的參數(shù),在所有的參數(shù)列表里索引是2。知道了 LOG 參數(shù)列表中格式字符串的位置和要檢查的參數(shù)位置,編譯器就會(huì)按照檢查 printf 的格式打印一樣,對(duì) LOG 函數(shù)進(jìn)行參數(shù)檢查。

如果我們的 LOG 函數(shù)定義為下面形式:

void LOG(int num, char *fmt, ...)  __attribute__((format(printf,2,3)));

在這個(gè)函數(shù)定義中,多了一個(gè)參數(shù) num,格式字符串在參數(shù)列表中的位置發(fā)生了變化(在所有的參數(shù)列表中,索引為2),要檢查的第一個(gè)變參的位置也發(fā)生了變化(索引為3),那我們使用 format 屬性聲明時(shí),就要寫(xiě)成 format(printf,2,3) 的形式了。

以上就是 format 屬性的使用方法,鑒于很多同學(xué),可能對(duì)變參函數(shù)研究得不多,接下來(lái)我們就一起研究下變參函數(shù)的設(shè)計(jì)與實(shí)現(xiàn),加深對(duì)本節(jié)知識(shí)的理解。

8.2 變參函數(shù)的設(shè)計(jì)與實(shí)現(xiàn)

對(duì)于一個(gè)普通函數(shù),我們?cè)诤瘮?shù)實(shí)現(xiàn)中,不用關(guān)心實(shí)參,只需要在函數(shù)體內(nèi)對(duì)形參直接引用即可。當(dāng)函數(shù)調(diào)用時(shí),傳遞的實(shí)參和形參個(gè)數(shù)和格式是匹配的。

變參函數(shù),顧名思義,跟 printf 函數(shù)一樣:參數(shù)的個(gè)數(shù)、類(lèi)型都不固定。我們?cè)诤瘮?shù)體內(nèi)因?yàn)轭A(yù)先不知道傳進(jìn)來(lái)的參數(shù)類(lèi)型和個(gè)數(shù),所以實(shí)現(xiàn)起來(lái)會(huì)稍微麻煩一點(diǎn)。首先要解析傳進(jìn)來(lái)的實(shí)參,保存起來(lái),然后才能接著像普通函數(shù)一樣,對(duì)實(shí)參進(jìn)行處理。

變參函數(shù)初體驗(yàn)

我們接下來(lái),就定義一個(gè)變參函數(shù),實(shí)現(xiàn)的功能很簡(jiǎn)單,即打印傳進(jìn)來(lái)的實(shí)參值。

void print_num(int count, ...)
{
    int *args;
    args = &count + 1;
    for( int i = 0; i < count; i++)
    {
        printf("*args: %d\n", *args);
        args++;
    }
}
int main(void)
{
    print_num(5,1,2,3,4,5);
    return 0;
}

變參函數(shù)的參數(shù)存儲(chǔ)其實(shí)跟 main 函數(shù)的參數(shù)存儲(chǔ)很像,由一個(gè)連續(xù)的參數(shù)列表組成,列表里存放的是每個(gè)參數(shù)的地址。在上面的函數(shù)中,有一個(gè)固定的參數(shù) count,這個(gè)固定參數(shù)的存儲(chǔ)地址后面,就是一系列參數(shù)的指針。在 print_num 函數(shù)中,先獲取 count 參數(shù)地址,然后使用 &count + 1 就可以獲取下一個(gè)參數(shù)的指針地址,使用指針變量 args 保存這個(gè)地址,并依次訪(fǎng)問(wèn)下一個(gè)地址,就可以直接打印傳進(jìn)來(lái)的各個(gè)實(shí)參值了。程序運(yùn)行結(jié)果如下。

*args:1
*args:2
*args:3
*args:4
*args:5

變參函數(shù)改進(jìn)版

上面的程序使用一個(gè) int 的指針變量依次去訪(fǎng)問(wèn)實(shí)參列表。我們接下來(lái)把程序改進(jìn)一下,使用 char 類(lèi)型的指針來(lái)實(shí)現(xiàn)這個(gè)功能,使之兼容更多的參數(shù)類(lèi)型。

void print_num2(int count,...)
{
    char *args;
    args = (char *)&count + 4;
    for(int i = 0; i < count; i++)
    {
        printf("*args: %d\n", *(int *)args);
        args += 4;
    }
}
int main(void)
{
    print_num2(5,1,2,3,4,5);
    return 0;
}

在這個(gè)程序中,我們使用char 類(lèi)型的指針。涉及到指針運(yùn)算,一定要注意每一個(gè)參數(shù)的地址都是4字節(jié)大小,所以我們獲取下一個(gè)參數(shù)的地址是:(char )&count + 4;。不同類(lèi)型的指針加1操作,轉(zhuǎn)換為實(shí)際的數(shù)值運(yùn)算是不一樣的。對(duì)于一個(gè)指向 int 類(lèi)型的指針變量 p,p+1表示 p + 1 sizeof(int),對(duì)于一個(gè)指向 char 類(lèi)型的指針變量,p + 1 表示 p + 1 sizeof(char)。兩種不同類(lèi)型的指針,其運(yùn)算細(xì)節(jié)就體現(xiàn)在這里。當(dāng)然,程序最后的運(yùn)行結(jié)果跟上面的程序是一樣的,如下所示。

*args:1
*args:2
*args:3
*args:4
*args:5

變參函數(shù) V3.0 版本

對(duì)于變參函數(shù),編譯器或計(jì)算機(jī)系統(tǒng)一般會(huì)提供一些宏給程序員使用,用來(lái)解析函數(shù)的參數(shù)。這樣程序員就不用自己解析參數(shù)了,直接使用封裝好的宏即可。編譯器提供的宏有:

  • va_list:定義在編譯器頭文件中 typedef char* va_list;。
  • va_start(args,fmt):根據(jù)參數(shù) fmt 的地址,獲取 fmt 后面參數(shù)的地址,并保存在 args 指針變量中。
  • va_end(args):釋放 args 指針,將其賦值為 NULL。有了這些宏,我們的工作就簡(jiǎn)化了很多。我們就不用擼起袖子,自己解析了。

    void print_num3(int count,...)
    {
    va_list args;
    va_start(args,count);
    for(int i = 0; i < count; i++)
    {
    printf("args: %d\n", (int *)args);
    args += 4;
    }
    va_end(args);
    }
    int main(void)
    {
    print_num3(5,1,2,3,4,5);
    return 0;
    }

變參函數(shù) V4.0 版本

在 V3.0 版本中,我們使用編譯器提供的三個(gè)宏,省去了解析參數(shù)的麻煩。但打印的時(shí)候,我們還必須自己實(shí)現(xiàn)。在 V4.0 版本中,我們繼續(xù)改進(jìn),使用 vprintf 函數(shù)實(shí)現(xiàn)我們的打印功能。vprintf 函數(shù)的聲明在 stdio.h 頭文件中。

CRTIMP int __cdecl __MINGW_NOTHROW    \
    vprintf (const char*, __VALIST);

vprintf 函數(shù)有2個(gè)參數(shù),一個(gè)是格式字符串指針,一個(gè)是變參列表。在下面的程序里,我們可以將,使用 va_start 解析后的變參列表,直接傳遞給 vprintf 函數(shù),實(shí)現(xiàn)打印功能。

void  my_printf(char *fmt,...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
}
int main(void)
{
    int num = 0;
    my_printf("I am litao, I have %d car\n", num);
    return 0;
}

運(yùn)行結(jié)果如下。

I am litao, I have 0 car

變參函數(shù) V5.0 版本

上面的 my_printf() 函數(shù),基本上實(shí)現(xiàn)了跟 printf() 函數(shù)相同的功能:支持變參,支持多種格式的數(shù)據(jù)打印。接下來(lái),我們還需要對(duì)其添加 format 屬性聲明,讓編譯器在編譯時(shí),像檢查 printf 一樣,檢查 my_printf() 函數(shù)的參數(shù)格式。V5.0 版本如下:

void __attribute__((format(printf,1,2))) my_printf(char *fmt,...)
{
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
}
int main(void)
{
    int num = 0;
    my_printf("I am litao, I have %d car\n", num);
    return 0;
}

8.3 實(shí)現(xiàn)自己的日志打印函數(shù)

如果你堅(jiān)持看到了這里,可能會(huì)問(wèn),有現(xiàn)成的打印函數(shù)可用,為什么還要費(fèi)這么大的勁,去實(shí)現(xiàn)自己的打印函數(shù)?原因其實(shí)很簡(jiǎn)單。自己實(shí)現(xiàn)的打印函數(shù),除了可以實(shí)現(xiàn)自己需要的打印格式,還有2個(gè)優(yōu)點(diǎn),即可以實(shí)現(xiàn)打印開(kāi)關(guān)控制、優(yōu)先級(jí)控制。

閉上迷茫的雙眼,好好想象一下。你在調(diào)試一個(gè)模塊,或者一個(gè)系統(tǒng),有好多個(gè)文件。如果你在每個(gè)文件里添加 printf 打印,調(diào)試完成后再刪掉,是不是很麻煩?我們自己實(shí)現(xiàn)的打印函數(shù),通過(guò)一個(gè)宏開(kāi)關(guān),就可以直接關(guān)掉或打開(kāi),比較方便。比如下面的代碼。

#define DEBUG //打印開(kāi)關(guān)

void __attribute__((format(printf,1,2))) LOG(char *fmt,...)
{
#ifdef DEBUG
    va_list args;
    va_start(args,fmt);
    vprintf(fmt,args);
    va_end(args);
#endif
}
int main(void)
{
    int num = 0;
    LOG("I am litao, I have %d car\n", num);
    return 0;
}

當(dāng)我們定義一個(gè) DEBUG 宏時(shí),LOG 函數(shù)實(shí)現(xiàn)普通的打印功能;當(dāng)這個(gè) DEBUG 宏沒(méi)有定義,LOG 函數(shù)就是個(gè)空函數(shù)。通過(guò)這個(gè)宏,我們就實(shí)現(xiàn)了打印函數(shù)的開(kāi)關(guān)功能,在實(shí)際調(diào)試中比較實(shí)用,非常方便。在 Linux 內(nèi)核的各個(gè)模塊中,你會(huì)經(jīng)??吹酱罅康淖远x打印函數(shù)或宏,如 pr_debug、pr_info 等。

除此之外,你可以通過(guò)宏,設(shè)置一些打印等級(jí)。比如可以分為 ERROR、WARNNING、INFO、LOG 等級(jí),根據(jù)你設(shè)置的打印等級(jí),模塊打印的 log 信息也會(huì)不一樣。這個(gè)功能就不展開(kāi)了,有興趣你可以試一下。

本教程根據(jù) C語(yǔ)言嵌入式Linux高級(jí)編程視頻教程 第05期 改編,電子版書(shū)籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關(guān)注:
微信公眾號(hào):宅學(xué)部落(armlinuxfun)
51CTO學(xué)院-王利濤老師:http://edu.51cto.com/sd/d344f

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI