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)相关推荐

  1. c语言高级程序设计第五版PDF,C语言高级编程.pdf

    C语言高级编程 概述 由几个测试程序说开去 预编译与宏 高级预编译介绍 宏的高级用法 变量 变量分类详细解析 我的变量去哪儿了? 大小端对变量的影响 内存与指针 常见内存使用错误大观 指针,又是指针! ...

  2. 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 ...

  3. 鼠标绘图 c语言,c语言高级编程技术教程 图形显示方式与鼠标输入.doc

    c语言高级编程技术教程 图形显示方式与鼠标输入 c语言高级编程技术教程 图形显示方式和鼠标输入 图形显示方式和鼠标输入 问题的提出编写程序,使用鼠标进行如下操作:按住鼠标器的任意键并移动,十字光 标将 ...

  4. 高级编程中C语言属于,c语言高级编程

    c语言高级编程 C高级编程 责任编辑:admin 更新日期:2005-8-6 深入了解C语言(函数的参数传递和函数使用参数的方法) tangl_99(原作) 关键字 C语言,汇编,代码生成,编译器 C ...

  5. 《go语言圣经》+《Mastering.GO-cn》+《go语言高级编程》PDF下载

    公众号[爱吃橙子的搬砖小徐]开通啦,后续将会同步更新,欢迎订阅 回复[java面试]获得两套面试宝典 回复[golang]获得go语言学习三部曲 <go语言圣经>+<Masterin ...

  6. matlab高级教程教材,MATLAB语言高级编程 PDF_IT教程网

    资源名称:MATLAB语言高级编程 PDF 本书共分8章,主要介绍了matlab的概述.matlab安装与工作桌面:matlab的编程基础,包括matlab的变量.matlab的运算符.矩阵的创建及运 ...

  7. 【嵌入式】C语言高级编程-可变参数宏(12)

    00. 目录 文章目录 00. 目录 01. 可变参数宏概述 02. ##符号 03. 可变参宏另外一种写法 04. 内核中的可变参数宏 05. 附录 01. 可变参数宏概述 #include < ...

  8. 【嵌入式】C语言高级编程-内建函数(11)

    00. 目录 文章目录 00. 目录 01. 内建函数概述 02. 常用内建函数 03. C 标准库的内建函数 04. 内核中的 likely 和 unlikely 05. 附录 01. 内建函数概述 ...

  9. 【嵌入式】C语言高级编程-内联函数(10)

    00. 目录 文章目录 00. 目录 01. 属性声明 02. 内联函数概述 03. 内联函数与宏 04. 编译器对内联函数的处理 05. static修饰内联函数 06. 附录 01. 属性声明 a ...

最新文章

  1. 如何搭建数据中台?行业AI独角兽:一手AI,一手Know-How
  2. instanceof与typeof 运算符
  3. Runnable接口介绍(中文文档)
  4. 成功解决TypeError: __init__() got an unexpected keyword argument 'serialized_options'
  5. 技术与管理并重才能走的更远
  6. iis7 运行 php5.5 的方法
  7. android中的多媒体应用camera
  8. javascript window.document
  9. 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题 1
  10. 关于iBase4J使用的一点心得体会
  11. 【数学建模】因子分析
  12. oracle报错imp报错00008,imp导入时遭遇IMP-00032,IMP-00008错误.
  13. 中国地产商寻找下一个春天
  14. 计算机设备图形符号,常用一次设备的图形符号和文字符号
  15. 【C++】如何释放vector的内存空间及std::vector::shrink_to_fit用法简介
  16. 最新网狐旗舰版整理、编译和搭建教程
  17. Eclipse Button按钮样式简单样式
  18. 关于Knuth 的搞笑8卦
  19. 软件下载官网系统源码
  20. ATmega8熔丝设置

热门文章

  1. Start here: portal to the lectures
  2. mysql 5.0存储过程学习总结
  3. java创建一个程序把输入字符串的大小写互换_8 编写程序,从键盘接收一个字符串,对字符串中的字母进行大小写互转...
  4. python高级应用_Python高级编程技巧
  5. vue-cli3 本地代理配置
  6. mysql 远程连接
  7. Python之迭代器
  8. Oracle导入导出数据
  9. 20155307 实验四 Android程序设计
  10. 3.【练习题】构造方法与重载 定义一个网络用户类,要处理的信息有用户ID、用户密码、email地址。拓展:判断密码长度