【C语言】format属性 | 变参函数

本篇文章主要讲解了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属性是非常推荐的,因为它可以帮助静态分析工具理解函数参数的预期用途,从而提高代码的安全性和可维护性。

  • 20
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值