目录
本篇文章主要讲解了C语言中的format属性和变参函数,在此之前,先对 __ attribute __ 关键字进行简单说明。
🎄GNC C编译器扩展关键字:__attribute__
在GNU C中扩展了一个__attribute__
关键字用来声明一个函数、变量或类型的特殊属性,指导编译器在编译程序时进行特定方面的优化或代码检查。该关键字的使用如下:
__attribute__((ATTRIBUTE))
括号里面的ATTRIBUTE表示要声明的属性,如下是常用的几种属性:
- section: 在程序编译时,将一个函数或变量放到指定的段,即放段指定的section中。
- aligned: 指定一个变量或类型的对齐方式,一般用来增大变量的地址对齐。
- packed: 指定一个变量或类型的对齐方式,一般用来减小变量的地址对齐。
- weak: 将一个强符号转换为弱符号。
- noinline: 在编译时,对指定的函数内联不展开。
- …
📓 注意:__attribute__
后面是两对小括号,不能只写一对。
📝补充
GNU C编译器是GNU Compiler Collection(GCC)中专门用于处理C语言的部分。GCC是一个由GNU项目开发的开源编译器套件,它不仅支持C语言,还支持C++、Objective-C、Fortran、Ada、Go等多种编程语言。GCC对于C语言的支持非常全面,遵循C标准,同时也提供了一些GNU特有的扩展。它在开源社区和多个操作系统平台上被广泛使用,包括Linux、FreeBSD、Mac OS X(通过Xcode的Clang作为替代)和Windows(通过MinGW或Cygwin等)。
除了GNU C编译器(GCC)之外,还有其他几个知名的C编译器,包括但不限于:Clang、Microsoft Visual C++ Compiler、Intel C Compiler、TinyCC (TCC)、PCC (Portable C Compiler)等。
🎄format属性
GNU通过__attribute__
扩展的format
属性,来指定变参函数的函数格式检查,其使用如下:
void __attribute__((format(printf, 1, 2))) LOG(const char *fmt, ...)
在上面的示例代码中,定义了一个LOG()
变参函数,用来实现日志打印功能。通过__attribute__((format(printf, 1, 2)))
属性声明告诉编译器:你是怎样对printf()
函数进行参数格式检查,就按照同样的方法对LOG()
进行参数检查。
其中,属性format(printf, 1, 2)
有三个参数,第一个参数printf
告诉编译器,按照printf()
函数的标准进行参数检查; 第二个参数1
表示格式字符串是函数LOG()
的第一个参数;第三个参数2
表示编译器要从函数LOG()
的第二个参数开始进行参数检查。
📝补充
格式字符串,顾名思义,如果一个字符串中含有格式匹配符,那么这个字符串就是格式字符串。
int age = 10; printf("I am LiMing, and i am %d years old! \n", age);
在上面的代码中,字符串“I am LiMing, and i am %d years old! \n”里含有格式匹配符%d,也叫占位符,打印的时候,后面变参的值会代替这个占位符,在屏幕上显示出来,这样的字符串就是格式字符串。
🎄变参函数
变参函数,故名思意,和printf
一样,其参数的个数、类型都不固定,所以在函数体内,需要先解析传进来的的实参,保存起来,然后才能像普通函数一样对实参进行各种操作。变参函数的参数存储由一个连续的参数列表组成,列表里存放的就是每个参数的地址。
🐞 编写一个简单的变参函数
我们编写一个简单的变参函数,实现功能:打印传进来的实参值。
#include <stdio.h>
void myprintf(int count, ...)
{
int i;
int *args;
args = &count + 1; /* 获取第一个可变参数的地址 */
/* 逐个打印可变参数 */
for(i = 0; i < count; i++)
{
printf("*args: %d\n", *args);
args++;
}
}
int main()
{
myprintf(5, 1, 2, 3, 4, 5);
return 0;
}
函数myprintf()
有一个固定的参数count
,表示可变参数的数量。在该函数内部,首先获得参数count
的地址,然后&count+1
就可以获得下一个参数的地址,保存至指针变量args
中,通过依次访问下一个地址,即可打印传进来的各个实参值了。
🐞 变参函数改进
在上面的程序中,指针变量args
的类型为int *
, 所以每次访问的实参大小固定为4字节。接下来我们使用char *
类型的指针变量来保存实参地址,使之兼容更多的参数类型。
#include <stdio.h>
void myprintf(int count, ...)
{
int i;
char *args;
args = (char *)&count + 4; /* 获取第一个可变参数的地址 */
/* 逐个打印可变参数 */
for(i = 0; i < count; i++)
{
printf("*args: %d\n", *(int *)args);
args += 4;
}
}
int main()
{
myprintf(5, 1, 2, 3, 4, 5);
return 0;
}
如上所示,我们使用的指针变量args
的类型为char *
, 所以获取下一个实参的地址是(char *)&count + 4
;打印int
类型的实参时,需要对args
进行强制转换*(int *)args
。
那为什么说args
的类型改为char *
后可以兼容更多的参数类型呢?
如果我们要打印的实参类型为short
类型时,仅需对myprintf()
函数做如下改进:
void myprintf(int count, ...)
{
...
/* 逐个打印可变参数 */
for(i = 0; i < count; i++)
{
printf("*args: %hd\n", *(short *)args); /* 强制转换为short* 类型 */
args += 2; /* 取下一个地址 */
}
}
🐞 使用宏来改进变参函数
对于变参函数,编译器或这操作系统一搬会提供一些宏给程序员使用,用来解析函数的参数列表。编译器提供的宏有3种,如下:
va_list
: 定义在编译器头文件stdarg.h
中,如typedef char* va_list;
。va_start(fmt, args)
: 根据参数args
的地址,获取args
后面参数的地址,并保存在fmt
指针变量中。va_end(args)
: 释放args
指针,将其赋值为NULL
。
利用以上宏,对前面的代码进行改进,如下:
#include <stdio.h>
#include <stdarg.h>
void myprintf(int count, ...)
{
int i;
va_list args;
va_start(args, count) /* 获取count后参数的地址,保存至args */
/* 逐个打印可变参数 */
for(i = 0; i < count; i++)
{
printf("*args: %d\n", *(int *)args);
args += 4;
}
va_end(args);
}
int main()
{
myprintf(5, 1, 2, 3, 4, 5);
return 0;
}
🐞 使用vprintf()函数改进变参函数
在上面我们使用了编译器提供的三个宏改进了变参函数,省去了解析参数的麻烦。但是打印的过程,我们还必须自己实现,且当我们打印的实参的类型发生改变时,我们还需要修改源码。因此,我们可以使用vprintf()
函数做进一步的改进。
首先简单介绍一下vprintf()
函数。vprintf()
函数的声明在头文件stdio.h
中,该函数有两个参数,一个是格式字符串指针, 一个是变参列表。如下所示,是一个简化的vprintf()
函数实现框架。
#include <stdio.h>
#include <stdarg.h>
int my_vprintf(const char *format, va_list ap) {
int result = 0;
while (*format != '\0') {
if (*format == '%') {
// 跳过%
format++;
// 解析格式化参数,如宽度、精度、标志等
int width = 0, precision = -1, flags = 0;
while (*format == '-' || *format == '+' || *format == ' ' || *format == '#' || *format == '0') {
// 处理格式标志
flags |= (*format == '-') ? LEFT : 0;
// ...其他标志处理
format++;
}
while (*format >= '0' && *format <= '9') {
width = width * 10 + (*format - '0');
format++;
}
if (*format == '.') {
format++;
while (*format >= '0' && *format <= '9') {
precision = precision * 10 + (*format - '0');
format++;
}
}
// 根据类型处理参数
switch (*format++) {
case 'd': // 整数
result += print_integer(ap, width, precision, flags);
break;
case 'f': // 浮点数
result += print_float(ap, width, precision, flags);
break;
case 's': // 字符串
result += print_string(ap, width, precision, flags);
break;
// 处理其他类型...
default:
// 未知格式字符处理
break;
}
} else {
// 非格式字符直接输出
result += putchar(*format++);
}
}
return result; // 返回输出的字符数
}
// 假设的辅助函数,实际实现需要处理va_list和格式化输出细节
int print_integer(va_list ap, int width, int precision, int flags) { /*...*/ }
int print_float(va_list ap, int width, int precision, int flags) { /*...*/ }
int print_string(va_list ap, int width, int precision, int flags) { /*...*/ }
// 使用示例
int custom_printf(const char *fmt, ...) {
va_list args;
va_start(args, fmt);
int result = my_vprintf(fmt, args);
va_end(args);
return result;
}
可以看出,vprintf()
的核心在于解析格式字符串,并根据该字符串中的格式说明符(如%d
, %s
, %f
等)从va_list
参数中获取相应的值,进行格式化处理后输出到标准输出流。
了解了vprintf()
函数后,我们对myprintf()
做进一步的改进:
#include <stdio.h>
#include <stdarg.h>
void __attribute__((printf, 1, 2)) myprintf(char *fmt, ...)
{
va_list args;
va_start(args, fmt) /* 获取count后参数的地址,保存至args */
vprintf(fmt, args);
va_end(args);
}
int main()
{
int age = 10;
myprintf("I am LiMing, and i am %d years old! \n", age);
return 0;
}
这样,我们的myprintf()
基本上实现了和printf()
函数相同的功能:支持变参,支持多种格式的数据打印。
🎄总结
在C语言中,变参函数并不强制要求声明format
属性,但这取决于你的用途。如果你的变参函数是用来处理格式化字符串的,比如类似于printf
这样的函数,那么使用format
属性是非常推荐的,因为它可以帮助静态分析工具理解函数参数的预期用途,从而提高代码的安全性和可维护性。