属性声明: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 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

变参函数 V3.0 版本

对于变参函数,编译器或计算机系统一般会提供一些宏给程序员使用,用来解析函数的参数。这样程序员就不用自己解析参数了,直接使用封装好的宏即可。编译器提供的宏有:

  • va_list:定义在编译器头文件中 typedef char* va_list;
  • va_start(fmt,args):根据参数 fmt 的地址,获取 fmt 后面参数的地址,并保存在 args 指针变量中。
  • 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;
}

变参函数 V4.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

变参函数 V5.0 版本

上面的 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 DEBUGva_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_debugpr_info等。

除此之外,你可以通过宏,设置一些打印等级。比如可以分为 ERROR、WARNNING、INFO、LOG 等级,根据你设置的打印等级,模块打印的 log 信息也会不一样。这个功能就不展开了,有兴趣你可以试一下。

linux内核C -- 第08课:变参函数的格式检查——format相关推荐

  1. 嵌入式C语言自我修养 08:变参函数的格式检查

    8.1 属性声明:format GNU 通过 __atttribute__ 扩展的 format 属性,用来指定变参函数的参数格式检查. 它的使用方法如下: __attribute__(( forma ...

  2. Linux 内核 C 语言深度解析

    课程简介 在看一些 GNU 开源软件,或者阅读 Linux 内核.驱动源码时,相信大家和我一样,经常会遇到一些"看似熟悉",但一仔细分析又不是很懂的 C 语言"稀奇古怪& ...

  3. Linux内核的时钟中断

    前言  时间在一个操作系统内核中占据着重要的地位,它是驱动一个OS内核运行的"起博器".一般说来,内核主要需要两种类型的时间:  1. 在内核运行期间持续记录当前的时间与日期,以便 ...

  4. linux内核期中总结

    20135132陈雨鑫 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000  ...

  5. Linux内核如何装载和启动一个可执行程序-----实验7

    2015108 李泽源 Linux内核如何装载和启动一个可执行程序 理解编译链接的过程和ELF可执行文件格式,详细内容参考本周第一节: 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程 ...

  6. Linux内核分析 笔记二 操作系统是如何工作的 ——by王玥

    一.知识要点 1.计算机是如何工作的?(总结)--三个法宝 存储程序计算机工作模型,计算机系统最最基础性的逻辑结构: 函数调用堆栈,高级语言得以运行的基础,只有机器语言和汇编语言的时候堆栈机制对于计算 ...

  7. linux内核开机显示企鹅logo,批改linux内核kernel开机logo(小企鹅)

    修改linux内核kernel开机logo(小企鹅) 修改linux内核kernel的开机图片(原为小企鹅图片). 转载请注明出处:http://blog.csdn.net/wang_zheng_ka ...

  8. Linux内核版本 uname命令 GNU项目 Linux发行版

    1.内核版本由linux内核社区统一编码和发布,格式如下图: major.minor.patch-build.desc 主版本号.次版本号.对次版本号的修订次数-编译次数.当前版本的特殊信息 次版本号 ...

  9. 详解神秘Linux内核

    在开始步入Linux设备驱动程序的神秘世界之前,让我们从驱动程序开发人员的角度看几个内核构成要素,熟悉一些基本的内核概念. 我们将学习内核定时器.同步机制以及内存分配方法.不过,我们还是得从头开始这次 ...

  10. 十天学Linux内核之第七天---电源开和关时都发生了什么

    十天学Linux内核之第七天---电源开和关时都发生了什么 原文:十天学Linux内核之第七天---电源开和关时都发生了什么 说实话感觉自己快写不下去了,其一是有些勉强跟不上来,其二是感觉自己越写越差 ...

最新文章

  1. 常见的IaaS安全问题与缓解方法
  2. 使用JVisualVM远程监控Tomcat
  3. IE安全系列:脚本先锋(4)
  4. Python 海龟画图(turtle)常用的颜色单词
  5. 「 每日一练,快乐水题 」917. 仅仅反转字母
  6. t4b代码生成_用T4模板实现简单的代码生成器
  7. ssis 计划任务_SSIS FTP任务概述
  8. pyinstaller打包tensorflow+python程序成.exe各种坑(持续添加)
  9. python3.9.5安装教程加汉化
  10. stm32 带通滤波器_带通滤波器详解_带通滤波器工作原理_带通滤波器原理图
  11. 重写JS保留小数-去尾法
  12. python二手房使用教程_python爬取安居客二手房网站数据方法分享
  13. 申请德国农工大学计算机案例,【2014】德州农工大学:腼腆男的春天
  14. 图像处理的相关数学知识
  15. 贷超分销系统的模式!
  16. 代码文档生成工具Doxygen教程及实例
  17. 2020牛客暑期多校训练营(第八场) Enigmatic Partition
  18. 12星座的出生年月日性格_十二个星座的出生年月日
  19. 网页部分第四次培训——JavaScript
  20. GANSYNTH(基于对抗性神经网络的音频合成)

热门文章

  1. struts2 获取前台表单的值?? 原理??
  2. Grails 基础环境搭建及HelloWorld
  3. docker compose启动服务超时重启记录
  4. 数百亿的新疆安防市场,集成巨头告诉你如何才能从中分杯羹
  5. 高逼格!程序猿的表白也可以这么浪漫
  6. Python UDP broadcast PermissionError: [Errno 13] Permission denied
  7. 两款开发辅助工具介绍
  8. tomcat监听activemq jms配置
  9. 关于弱电工程图纸的几个常见问题
  10. Python命令行参数选择