用可变参数扩展printf

本文介绍了C语言中使用VA_LIST、VA_START、VA_ARG和VA_END宏处理可变参数的方法,详细解析了printf的实现原理,并展示了如何扩展printf功能,以包含文件名、函数名和行号等调试信息。此外,还探讨了在Linux应用程序中控制日志输出的简单策略,包括通过文件标志位动态控制日志打印,并分析了其效率问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

VA_LIST 是在C语言中解决变参问题的一组宏,所在头文件#include <stdarg.h>,用于获取不确定个数的参数。首先,我们来看看C语言中是如何定义。先看其中比较简单的一种定义,分析完一种,其他实现也可触类旁通。
va_list,定义为字符串指针:

typedef char * va_list;

VA_START宏,获取可变参数列表的第一个参数的地址(ap是类型为va_list的指针,v是可变参数最左边的参数):

#define va_start(ap,v) ( ap = (va_list)&v + 4 )

因为第一个参数是固定的,第一个参数的地址加上4(一个地址的长度),就等于第二个参数的地址。如果要深究,就要研究C语言参数入栈相关知识,参数压栈的方向是从右往左,这里简单提下。

VA_ARG宏,获取可变参数的当前参数,返回指定类型并将指针指向下一参数,实际应用中经常用格式化函数vsprintf代替VA_ARG宏。

VA_END宏,清空va_list可变参数列表:

#define va_end(ap) ( ap = (va_list)0 )

下面展示一个简单的程序流程

    char buffer[80] = {0};
    int cnt; 
    va_list argptr; //初始化va_list变量argptr
    va_start(argptr, fmt); //获取可变参数列表的第一个参数的地址argptr
    cnt = vsprintf(buffer, fmt, argptr);//格式化输出数组buffer
    va_end(argptr);//清空va_list可变参数列表

有了上面基础知识,再来看看printf的一种实现

int printf(const char *fmt, ...) 
{ 
    int i; 
    char buf[256]; 
    va_list arg = (va_list)((char*)(&fmt) + 4);//
    i = vsprintf(buf, fmt, arg); 
    write(buf, i); 
    return i; 
}

从上面的程序中可以看出,buf保存着格式化后的数据,系统再调用write函数输出到某一个地方。其实跟我们上一个例子非常相似,不同之处是输出到哪里。我们在调试程序的时候,需要用打印log出来帮我们解决bug。有了上面的知识,我们可以打造一个方便自己调试程序的函数。如下面的例子。

void printf_my_log(char *file, int line, const char *func, char *fmt, ...)
{    
    char argvStr[1024] = {0};    
    va_list argptr;    
    memset(&argptr, 0, sizeof(va_list));    
    printf("[%s(Line=%d):%s]",file,line,func);    
    va_start (argptr, fmt);
    vsprintf(argvStr, fmt, argptr);    
    va_end (argptr);    
    printf("%s\n", argvStr);
}

因为实际项目是很庞大的,我们经常需要定位到哪个文件,哪个函数,哪一行出问题了。所以,我们可以用__FILE__,__LINE__,__FUNCTION__这几个宏来简单扩展一下printf的功能。像上面的例子,我们可以这样调用:

printf_my_log(__FILE__,__LINE__,__FUNCTION__,"%d %f %s", a,b,c);

既然讨论到这里了,我们再试试看怎么把log按照你的意图在终端显示出来。在linux系统中,我们可以通过设置printk的打印级别来控制log,在linux应用程序中,我们又是怎么实现类似的机制呢?
好,下面来讨论分析一下如何在应用中控制log的输出。
我们假设一个情景,一个长期运行的Linux程序,想在不退出运行的情况下,通过某种机制,可以让程序知道要不要打印出log
最简单的实现方式可以这样:
1、创建一个文件,写进标志位
2、然后每次要打印log之前先读取这个文件,按照标志位是什么来决定要不要打印log
这样子我们在linux系统上,如果不想打印出这些log,可以向这个文件写其他标志位。
代码示例如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdarg.h>
void printf_my_log(char *file, int line, const char *func, char *fmt, ...)
{
    int fd;
    char filename[] = "printlog";
    char readbuf[8] = {0};
    int read_count = 0;
    int logFlag = 0;
    //打开文件printlog,如果文件不存在,则创建新文件
    if((fd = open(filename,O_RDWR|O_CREAT, 0777))<0)
    {  
        perror("open");
    } 
    //读取文件一个字符到readbuf数组中
    read_count = read(fd,readbuf,1);
    //把字符串转化为数字。
    logFlag = atoi(readbuf);
    //没有向文件写入数据或者读取到的标志位为1,则打印log
    if (read_count < 1 || logFlag == 1)
    {
        char argvStr[1024] = {0};    
        va_list argptr;        
        printf("[%s(Line=%d):%s]",file,line,func);    
        va_start (argptr, fmt);
        vsprintf(argvStr, fmt, argptr);    
        va_end (argptr);    
        printf("%s\n", argvStr);
    }
    close(fd); 
}

int main(void)
{  
    int a = 10;
    printf_my_log(__FILE__,__LINE__,__FUNCTION__,"%d",a);
    return 0;  
}

在终端实验,
编译:

ubuntu:~/test/69test$ gcc creatfile.c -o creatfile
ubuntu:~/test/69test$ ls
creatfile  creatfile.c  printlog

运行应用程序:(默认打开log)

ubuntu:~/test/69test$ ./creatfile
[creatfile.c(Line=54):main]10

关闭log:

ubuntu:~/test/69test$ echo 0 > printlog 
ubuntu:~/test/69test$ ./creatfile

打开log:

ubuntu:~/test/69test$ echo 1 > printlog  
ubuntu:~/test/69test$ ./creatfile       
[creatfile.c(Line=54):main]10

上面这种做法,实践学习还可以,但是应用到项目中,效率太低,不建议使用。本节先介绍比较低效的方法,然后在接下来的博客中会继续思考如何改进这些问题。在分析存在的问题之后再思考如何改进这些问题的方法,有助于加深对改进机智的理解,同时也能更深刻的学习总结知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值