工欲善其事必先利其器-C语言拓展–嵌入式C语言(七)
文章内容全部来自–>《嵌入式C语言自我修养——从芯片、编译器到操作系统》 王利涛前辈的,超级推荐
__attribute__扩展的format属性,来指定变参函数的参数格式检查。
__attribute__((format (archetype, string-index, first-to-check))) void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));
这个作用其实就是自定义打印。因为我们在项目中需要实现一些自定义的调试函数。
用户在调用这些接口函数时参数往往不固定,所以就需要用到这个format属性
现在来解释解释上面的代码:
定义一个LOG()变参函数,用来实现日志打印功能。
编译器在编译程序时,如何检查LOG()函数的参数格式是否正确呢?
方法其实很简单,通过给LOG()函数添加__attribute__((format(printf,1,2)))属性声明就可以了。
这个属性声明告诉编译器:你知道printf()函数不?你怎么对printf()函数进行参数格式检查的,就按照同样的方法,对LOG()函数进行检查。
属性format(printf,1,2)有3个参数:
第1个参数printf是告诉编译器,按照printf()函数的标准来检查; 第2个参数表示在LOG()函数所有的参数列表中格式字符串的位置索引; 第3个参数是告诉编译器要检查的参数的起始位置。
稍微有点米糊糊?整个栗子:
拿第二个展开说说
void LOG(const char *fmt, ...) __attribute__((format(printf,1,2)));
在这个LOG()函数中有2个参数:
第1个参数是格式字符串,
第2个参数是要打印的一个常量值0,用来匹配格式字符串中的占位符。
这个格式字符串是什么哇?
一个字符串中含有格式匹配符,那么这个字符串就是格式字符串。 这个%d就是格式匹配符,也叫做占位符。
打印的时候,参数的值会代替这个占位符显示出来。
我们通过format(printf,1,2) 属性声明,告诉编译器:
LOG()函数的参数,其格式字符串的位置在所有参数列表中的索引是1,即第一个参数;
要编译器帮忙检查的参数,在所有的参数列表里索引是2。
知道了LOG()参数列表中格式字符串的位置和要检查的参数位置,编译器就会按照检查printf的格式打印一样,对LOG()函数进行参数检查了。
换个方式定义一下LOG()
void LOG(int num, char *fmt, ...) __attribute__((format(printf,2,3)))
在这个函数定义中,多了一个参数num,格式字符串在参数列表中的位置发生了变化(在所有的参数列表中,索引由1变成了2),要检查的第一个变参的位置也发生了变化(索引从原来的2变成了3),那么我们使用format属性声明时,就要写成format(printf,2,3)的形式了。
…在这里就是指的参数不确定。
其实我刚刚开始看这里很疑惑,那么直接用printf不行吗? 留着,我们先接着下面来了解一下关于变参函数
变参函数的设计与实现
普通的函数,实参与形参一一匹配。
而变参函数,和printf函数一样,参数的个数和类型都是不固定的。
要首先解析实际传进来的实参,保存起来,然后才能像普通函数那样,对实参进行各种操作。
这个用过printf()函数的都知道的,无所不能。
整个栗子瞅瞅:
在上面的函数中,有一个固定的参数count,这个固定参数的存储地址后面,就是一系列参数的地址。
在print_num()函数中,首先获取count参数地址,然后使用&count+1就可以获取下一个参数的地址,使用指针变量args保存这个地址,并依次访问下一个地址,就可以直接打印传进来的各个实参值了。(让我想起了零长数组,其实第一个参数占个位,这很关键,args = &count+1,就是指向了后续的参数地址)
修改一下:
在这个程序中,我们使用char类型的指针。涉及指针运算,一定要注意,因为每一个参数的地址都是4字节大小,所以我们获取下一个参数的地址是(char)&count+4;(指针的大小记住是固定的4字节,这也是为什么文件传递的时候尽量传递指针,这样可以提高效率。)
不同类型的指针加1操作,转换为实际的数值运算是不一样的。对于一个指向int类型的指针变量p,p+1表示p+1sizeof(int)。对于一个指向char类型的指针变量,p+1表示p+1sizeof(char)因此这里整了4.
再变变?
对于变参函数,编译器或操作系统一般会提供一些宏给程序员使用,用来解析函数的参数列表,编译器提供的宏有以下3种。
● va_list:定义在编译器头文件stdarg.h中,如typedef char*va_list;。
● va_start(fmt,args):根据参数args的地址,获取args后面参数的地址,并保存在fmt指针变量中。
● va_end(args):释放args指针,将其赋值为NULL。
变参函数V4.0
上面使用编译器提供的三个宏,这样可以让我们不用自己去解析参数。
现在打印的功能也不自己实现了。
使用vprintf()函数完成打印功能。vprintf()函数的声明在stdio.h头文件中。
vprintf()函数有两个参数:一个是格式字符串指针,一个是变参列表。
接下来,我们需要对其添加format属性声明,让编译器在编译时,像检查printf()一样,检查myprintf()函数的参数格式。
我就纳闷了,为啥有现成的还要自己去实现?
就是想实现自己的日志打印函数,原因其实很简单,自己实现的打印函数,**除了可以实现自己需要的打印格式,**还有很多优点,可以实现打印开关控制和优先级控制,还可以根据需要不断添加功能。
你在调试的模块或系统中,可能有好多文件。如果在每个文件里都添加printf()函数打印,调试完成后再删掉,是不是很麻烦?而使用我们自己实现的打印函数,通过一个宏开关,就可以直接关掉或打开,维护起来更加方便,如下面的代码。
当我们在程序中定义一个DEBUG开关宏时,LOG()函数实现正常的打印功能;当我们删掉这个DEBUG宏时,LOG()函数就是一个空函数。通过这个宏,我们实现了打印函数的开关功能。在Linux内核的各个模块或子系统中,你会经常看到各种自定义的打印函数或宏,如pr_debug、pr_info、pr_err等。
顿时这个意义就来了,豁然舒服了。
除此之外,你还可以通过宏来设置一些打印等级。如可以分为ERROR、WARNNING、INFO等打印等级,根据设置的打印等级,模块打印的日志信息也不一样。
牛啊牛啊
在上面的程序中,我们封装了3个打印函数:INFO()、WARN()和ERR(),分别打印不同优先级的日志信息。在实际调试中,我们可以根据自己需要的打印信息,设置合适的打印等级,就可以分级控制这些打印信息了。
酷的 bro