【嵌入式】C语言高级编程-变参函数(08)
00. 目錄
文章目錄
- 00. 目錄
- 01. format屬性聲明
- 02. 變參函數的設計思路
- 03. 變參函數宏
- 04. 應用示例
- 05. 附錄
01. format屬性聲明
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 tom\n"); LOG("I am tom, I have %d houses!\n",0); LOG("I am tom, I have %d houses! %d cars\n",0,0);上面代碼,是我們的 LOG 函數使用示例。變參函數,其參數個數跟 printf 函數一樣,是不固定的。那么編譯器如何檢查我們的打印格式是否正確呢?很簡單,我們只需要將格式字符串的位置告訴編譯器就可以了,比如在第2行代碼中:
LOG("I am tom, I have %d houses!\n",0);在這個 LOG 函數中有2個參數,第一個是格式字符串,第2個是要打印的一個常量值0,用來匹配格式字符串中的格式符。
什么是格式字符串呢?顧名思義,如果一個字符串中含有格式符,那這個字符串就是格式字符串。比如這個格式字符串:“I am tom, 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 屬性的使用方法。
02. 變參函數的設計思路
變參函數,顧名思義,跟 printf 函數一樣:參數的個數、類型都不固定。我們在函數體內因為預先不知道傳進來的參數類型和個數,所以實現起來會稍微麻煩一點。首先要解析傳進來的實參,保存起來,然后才能接著像普通函數一樣,對實參進行處理。
我們接下來,就定義一個變參函數,實現的功能很簡單,即打印傳進來的實參值。
程序示例
#include <stdio.h>void fun(int count, ...) {int i = 0;int *args = NULL;args = &count + 1;for (i = 0; i < count; i++){printf("args: %d %p\n", *args, args);args++;} }int main(void) {fun(5, 1, 2, 3, 4, 5);return 0; }測試結果
# 根據平臺不同,可能結果不同 deng@itcast:~/tmp$ ./a.out args: 832 0x7ffc05619808 args: 832 0x7ffc05619804 args: 832 0x7ffc05619800 args: 21940 0x7ffc056197fc args: 975176187 0x7ffc056197f8變參函數的參數存儲其實跟 main 函數的參數存儲很像,由一個連續的參數列表組成,列表里存放的是每個參數的地址。在上面的函數中,有一個固定的參數 count,這個固定參數的存儲地址后面,就是一系列參數的指針。在 fun函數中,先獲取 count 參數地址,然后使用 &count + 1 就可以獲取下一個參數的指針地址,使用指針變量 args 保存這個地址,并依次訪問下一個地址,就可以直接打印傳進來的各個實參值了。
上面的程序使用一個 int * 的指針變量依次去訪問實參列表。我們接下來把程序改進一下,使用 char * 類型的指針來實現這個功能,使之兼容更多的參數類型。
程序示例
#include <stdio.h>void fun(int count, ...) {int i = 0;char *args = NULL;args = (void*)&count + 4;for (i = 0; i < count; i++){printf("args: %d %p\n", *(int*)args, args);args += 4;} }int main(void) {fun(5, 1, 2, 3, 4, 5);return 0; }03. 變參函數宏
對于變參函數,編譯器或計算機系統一般會提供一些宏給程序員使用,用來解析函數的參數。這樣程序員就不用自己解析參數了,直接使用封裝好的宏即可。編譯器提供的宏有:
va_list:定義在編譯器頭文件中 typedef char* va_list;。va_start(args,fmt):根據參數 fmt 的地址,獲取 fmt 后面參數的地址,并保存在 args 指針變量中。va_end(args):釋放 args 指針,將其賦值為 NULL。有了這些宏,我們的工作就簡化了很多。我們就不用擼起袖子,自己解析了。程序示例
#include <stdio.h> #include <stdarg.h>void fun(int count, ...) {va_list args;va_start(args, count);for (int i = 0; i < count; i++){printf("*args = %d\n", va_arg(args, int));}va_end(args); }int main(void) {fun(5, 1, 2, 3, 4, 5);return 0; }執行結果
deng@itcast:~/tmp$ gcc test.c deng@itcast:~/tmp$ ./a.out *args = 1 *args = 2 *args = 3 *args = 4 *args = 5我們使用編譯器提供的三個宏,省去了解析參數的麻煩。但打印的時候,我們還必須自己實現。在 V4.0 版本中,我們繼續改進,使用 vprintf 函數實現我們的打印功能。vprintf 函數的聲明在 stdio.h 頭文件中。
# if !(__USE_FORTIFY_LEVEL > 0 && defined __fortify_function) /* Write formatted output to stdout from argument list ARG. */ __STDIO_INLINE int vprintf (const char *__restrict __fmt, __gnuc_va_list __arg) {return vfprintf (stdout, __fmt, __arg); } # endifvprintf 函數有2個參數,一個是格式字符串指針,一個是變參列表。在下面的程序里,我們可以將,使用 va_start 解析后的變參列表,直接傳遞給 vprintf 函數,實現打印功能。
程序示例
#include <stdio.h> #include <stdarg.h>void fun(char *fmt, ...) {va_list args;va_start(args, fmt);vprintf(fmt, args);va_end(args); }int main(void) {int n = 88;fun("hello world %d\n", n);return 0; }執行結果
deng@itcast:~/tmp$ ./a.out hello world 88上一個示例程序基本上實現了跟 printf() 函數相同的功能:支持變參,支持多種格式的數據打印。接下來,我們還需要對其添加 format 屬性聲明,讓編譯器在編譯時,像檢查 printf 一樣,檢查 fun() 函數的參數格式。
程序示例
#include <stdio.h> #include <stdarg.h>void __attribute__((format(printf,1,2))) fun(char *fmt, ...) {va_list args;va_start(args, fmt);vprintf(fmt, args);va_end(args); }int main(void) {int n = 88;fun("hello world %d\n", n);return 0; }執行結果
deng@itcast:~/tmp$ ./a.out hello world 8804. 應用示例
在調試一個模塊,或者一個系統,有好多個文件。如果你在每個文件里添加 printf 打印,調試完成后再刪掉,是不是很麻煩?我們自己實現的打印函數,通過一個宏開關,就可以直接關掉或打開,比較方便。比如下面的代碼。
輸出日志信息程序
#include <stdio.h> #include <stdarg.h>#define DEBUGvoid __attribute__((format(printf,1,2))) LOG(char *fmt, ...) { #ifdef DEBUGva_list args;va_start(args, fmt);vprintf(fmt, args);va_end(args); #endif }int main(void) {int n = 88;LOG("hello world %d\n", n);return 0; }執行結果
deng@itcast:~/tmp$ ./a.out hello world 88 deng@itcast:~/tmp$當我們定義一個 DEBUG 宏時,LOG 函數實現普通的打印功能;當這個 DEBUG 宏沒有定義,LOG 函數就是個空函數。通過這個宏,我們就實現了打印函數的開關功能,在實際調試中比較實用,非常方便。在 Linux 內核的各個模塊中,你會經常看到大量的自定義打印函數或宏,如 pr_debug、pr_info 等。
除此之外,你可以通過宏,設置一些打印等級。比如可以分為 ERROR、WARNNING、INFO、LOG 等級,根據你設置的打印等級,模塊打印的 log 信息也會不一樣。
05. 附錄
參考:C語言嵌入式Linux高級編程
總結
以上是生活随笔為你收集整理的【嵌入式】C语言高级编程-变参函数(08)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【嵌入式】C语言高级编程-地址对齐(07
- 下一篇: 【嵌入式】C语言高级编程-强符号和弱符号