您好,登錄后才能下訂單哦!
GNU 通過 attribute 擴展的 format 屬性,用來指定變參函數的參數格式檢查。
它的使用方法如下:
__attribute__(( format (archetype, string-index, first-to-check)))
void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));
我們經常實現一些自己的打印調試函數。這些打印函數往往是變參函數,那編譯器編譯程序時,怎么知道我們的參數格式對不對呢?因為我們實現的是變參函數,參數的個數和格式都不確定。所以編譯器表示壓力很大,不知道該如何處理。
辦法總是有的。這不,attribute 的format屬性這時候就自帶 BGM,隆重出場了。如上面的示例代碼,我們定義一個 LOG 變參函數,用來實現打印功能。那編譯器編譯程序時,如何檢查我們參數的格式是否正確呢?其實很簡單,通過給 LOG 函數添加 attribute((format(printf,1,2))) 這個屬性聲明,就是告訴編譯器:你知道printf函數不?你怎么對這個函數參數格式檢查的,就按同樣的方法,對 LOG 函數進行檢查。
屬性 format(printf,1,2) 有三個參數。第一個參數 printf 是告訴編譯器,按照 printf 函數的檢查標準來檢查;第2個參數表示在 LOG 函數所有的參數列表中,格式字符串的位置索引;第3個參數是告訴編譯器要檢查的參數的起始位置。是不是沒看明白?舉個例子大家就明白了。
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 函數使用示例。變參函數,其參數個數跟 printf 函數一樣,是不固定的。那么編譯器如何檢查我們的打印格式是否正確呢?很簡單,我們只需要將格式字符串的位置告訴編譯器就可以了,比如在第2行代碼中:
LOG("I am litao, I have %d houses!\n",0);
在這個 LOG 函數中有2個參數,第一個是格式字符串,第2個是要打印的一個常量值0,用來匹配格式字符串中的格式符。
什么是格式字符串呢?顧名思義,如果一個字符串中含有格式符,那這個字符串就是格式字符串。比如這個格式字符串:"I am litao, I have %d houses!\n",里面含有格式符%,我們也可以叫它占位符。打印的時候,后面變參的值會代替這個占位符,在屏幕上顯示出來。
我們通過 format(printf,1,2) 屬性聲明,告訴編譯器:LOG 函數的參數,格式字符串的位置在所有參數列表中的索引是1,即第一個參數;要編譯器幫忙檢查的參數,在所有的參數列表里索引是2。知道了 LOG 參數列表中格式字符串的位置和要檢查的參數位置,編譯器就會按照檢查 printf 的格式打印一樣,對 LOG 函數進行參數檢查。
如果我們的 LOG 函數定義為下面形式:
void LOG(int num, char *fmt, ...) __attribute__((format(printf,2,3)));
在這個函數定義中,多了一個參數 num,格式字符串在參數列表中的位置發生了變化(在所有的參數列表中,索引為2),要檢查的第一個變參的位置也發生了變化(索引為3),那我們使用 format 屬性聲明時,就要寫成 format(printf,2,3) 的形式了。
以上就是 format 屬性的使用方法,鑒于很多同學,可能對變參函數研究得不多,接下來我們就一起研究下變參函數的設計與實現,加深對本節知識的理解。
對于一個普通函數,我們在函數實現中,不用關心實參,只需要在函數體內對形參直接引用即可。當函數調用時,傳遞的實參和形參個數和格式是匹配的。
變參函數,顧名思義,跟 printf 函數一樣:參數的個數、類型都不固定。我們在函數體內因為預先不知道傳進來的參數類型和個數,所以實現起來會稍微麻煩一點。首先要解析傳進來的實參,保存起來,然后才能接著像普通函數一樣,對實參進行處理。
我們接下來,就定義一個變參函數,實現的功能很簡單,即打印傳進來的實參值。
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;
}
變參函數的參數存儲其實跟 main 函數的參數存儲很像,由一個連續的參數列表組成,列表里存放的是每個參數的地址。在上面的函數中,有一個固定的參數 count,這個固定參數的存儲地址后面,就是一系列參數的指針。在 print_num 函數中,先獲取 count 參數地址,然后使用 &count + 1 就可以獲取下一個參數的指針地址,使用指針變量 args 保存這個地址,并依次訪問下一個地址,就可以直接打印傳進來的各個實參值了。程序運行結果如下。
*args:1
*args:2
*args:3
*args:4
*args:5
上面的程序使用一個 int 的指針變量依次去訪問實參列表。我們接下來把程序改進一下,使用 char 類型的指針來實現這個功能,使之兼容更多的參數類型。
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;
}
在這個程序中,我們使用char 類型的指針。涉及到指針運算,一定要注意每一個參數的地址都是4字節大小,所以我們獲取下一個參數的地址是:(char )&count + 4;。不同類型的指針加1操作,轉換為實際的數值運算是不一樣的。對于一個指向 int 類型的指針變量 p,p+1表示 p + 1 sizeof(int),對于一個指向 char 類型的指針變量,p + 1 表示 p + 1 sizeof(char)。兩種不同類型的指針,其運算細節就體現在這里。當然,程序最后的運行結果跟上面的程序是一樣的,如下所示。
*args:1
*args:2
*args:3
*args:4
*args:5
對于變參函數,編譯器或計算機系統一般會提供一些宏給程序員使用,用來解析函數的參數。這樣程序員就不用自己解析參數了,直接使用封裝好的宏即可。編譯器提供的宏有:
va_end(args):釋放 args 指針,將其賦值為 NULL。有了這些宏,我們的工作就簡化了很多。我們就不用擼起袖子,自己解析了。
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;
}
在 V3.0 版本中,我們使用編譯器提供的三個宏,省去了解析參數的麻煩。但打印的時候,我們還必須自己實現。在 V4.0 版本中,我們繼續改進,使用 vprintf 函數實現我們的打印功能。vprintf 函數的聲明在 stdio.h 頭文件中。
CRTIMP int __cdecl __MINGW_NOTHROW \
vprintf (const char*, __VALIST);
vprintf 函數有2個參數,一個是格式字符串指針,一個是變參列表。在下面的程序里,我們可以將,使用 va_start 解析后的變參列表,直接傳遞給 vprintf 函數,實現打印功能。
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;
}
運行結果如下。
I am litao, I have 0 car
上面的 my_printf() 函數,基本上實現了跟 printf() 函數相同的功能:支持變參,支持多種格式的數據打印。接下來,我們還需要對其添加 format 屬性聲明,讓編譯器在編譯時,像檢查 printf 一樣,檢查 my_printf() 函數的參數格式。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;
}
如果你堅持看到了這里,可能會問,有現成的打印函數可用,為什么還要費這么大的勁,去實現自己的打印函數?原因其實很簡單。自己實現的打印函數,除了可以實現自己需要的打印格式,還有2個優點,即可以實現打印開關控制、優先級控制。
閉上迷茫的雙眼,好好想象一下。你在調試一個模塊,或者一個系統,有好多個文件。如果你在每個文件里添加 printf 打印,調試完成后再刪掉,是不是很麻煩?我們自己實現的打印函數,通過一個宏開關,就可以直接關掉或打開,比較方便。比如下面的代碼。
#define DEBUG //打印開關
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;
}
當我們定義一個 DEBUG 宏時,LOG 函數實現普通的打印功能;當這個 DEBUG 宏沒有定義,LOG 函數就是個空函數。通過這個宏,我們就實現了打印函數的開關功能,在實際調試中比較實用,非常方便。在 Linux 內核的各個模塊中,你會經常看到大量的自定義打印函數或宏,如 pr_debug、pr_info 等。
除此之外,你可以通過宏,設置一些打印等級。比如可以分為 ERROR、WARNNING、INFO、LOG 等級,根據你設置的打印等級,模塊打印的 log 信息也會不一樣。這個功能就不展開了,有興趣你可以試一下。
本教程根據 C語言嵌入式Linux高級編程視頻教程 第05期 改編,電子版書籍可加入QQ群:475504428 下載,更多嵌入式視頻教程,可關注:
微信公眾號:宅學部落(armlinuxfun)
51CTO學院-王利濤老師:http://edu.51cto.com/sd/d344f
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。