【嵌入式】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);
}
# endif
vprintf 函数有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 88
04. 应用示例
在调试一个模块,或者一个系统,有好多个文件。如果你在每个文件里添加 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语言高级程序设计第五版PDF,C语言高级编程.pdf
C语言高级编程 概述 由几个测试程序说开去 预编译与宏 高级预编译介绍 宏的高级用法 变量 变量分类详细解析 我的变量去哪儿了? 大小端对变量的影响 内存与指针 常见内存使用错误大观 指针,又是指针! ...
- Go 学习推荐 —(Go by example 中文版、Go 构建 Web 应用、Go 学习笔记、Golang常见错误、Go 语言四十二章经、Go 语言高级编程)
Go by example 中文版 Go 构建 Web 应用 Go 学习笔记:无痕 Go 标准库中文文档 Golang开发新手常犯的50个错误 50 Shades of Go: Traps, Gotc ...
- 鼠标绘图 c语言,c语言高级编程技术教程 图形显示方式与鼠标输入.doc
c语言高级编程技术教程 图形显示方式与鼠标输入 c语言高级编程技术教程 图形显示方式和鼠标输入 图形显示方式和鼠标输入 问题的提出编写程序,使用鼠标进行如下操作:按住鼠标器的任意键并移动,十字光 标将 ...
- 高级编程中C语言属于,c语言高级编程
c语言高级编程 C高级编程 责任编辑:admin 更新日期:2005-8-6 深入了解C语言(函数的参数传递和函数使用参数的方法) tangl_99(原作) 关键字 C语言,汇编,代码生成,编译器 C ...
- 《go语言圣经》+《Mastering.GO-cn》+《go语言高级编程》PDF下载
公众号[爱吃橙子的搬砖小徐]开通啦,后续将会同步更新,欢迎订阅 回复[java面试]获得两套面试宝典 回复[golang]获得go语言学习三部曲 <go语言圣经>+<Masterin ...
- matlab高级教程教材,MATLAB语言高级编程 PDF_IT教程网
资源名称:MATLAB语言高级编程 PDF 本书共分8章,主要介绍了matlab的概述.matlab安装与工作桌面:matlab的编程基础,包括matlab的变量.matlab的运算符.矩阵的创建及运 ...
- 【嵌入式】C语言高级编程-可变参数宏(12)
00. 目录 文章目录 00. 目录 01. 可变参数宏概述 02. ##符号 03. 可变参宏另外一种写法 04. 内核中的可变参数宏 05. 附录 01. 可变参数宏概述 #include < ...
- 【嵌入式】C语言高级编程-内建函数(11)
00. 目录 文章目录 00. 目录 01. 内建函数概述 02. 常用内建函数 03. C 标准库的内建函数 04. 内核中的 likely 和 unlikely 05. 附录 01. 内建函数概述 ...
- 【嵌入式】C语言高级编程-内联函数(10)
00. 目录 文章目录 00. 目录 01. 属性声明 02. 内联函数概述 03. 内联函数与宏 04. 编译器对内联函数的处理 05. static修饰内联函数 06. 附录 01. 属性声明 a ...
最新文章
- 如何搭建数据中台?行业AI独角兽:一手AI,一手Know-How
- instanceof与typeof 运算符
- Runnable接口介绍(中文文档)
- 成功解决TypeError: __init__() got an unexpected keyword argument 'serialized_options'
- 技术与管理并重才能走的更远
- iis7 运行 php5.5 的方法
- android中的多媒体应用camera
- javascript window.document
- 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题 1
- 关于iBase4J使用的一点心得体会
- 【数学建模】因子分析
- oracle报错imp报错00008,imp导入时遭遇IMP-00032,IMP-00008错误.
- 中国地产商寻找下一个春天
- 计算机设备图形符号,常用一次设备的图形符号和文字符号
- 【C++】如何释放vector的内存空间及std::vector::shrink_to_fit用法简介
- 最新网狐旗舰版整理、编译和搭建教程
- Eclipse Button按钮样式简单样式
- 关于Knuth 的搞笑8卦
- 软件下载官网系统源码
- ATmega8熔丝设置
热门文章
- Start here: portal to the lectures
- mysql 5.0存储过程学习总结
- java创建一个程序把输入字符串的大小写互换_8 编写程序,从键盘接收一个字符串,对字符串中的字母进行大小写互转...
- python高级应用_Python高级编程技巧
- vue-cli3 本地代理配置
- mysql 远程连接
- Python之迭代器
- Oracle导入导出数据
- 20155307 实验四 Android程序设计
- 3.【练习题】构造方法与重载 定义一个网络用户类,要处理的信息有用户ID、用户密码、email地址。拓展:判断密码长度