C++ 日志系统实战第二步:不定参数函数解析
全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~
相关技术知识补充
不定参宏函数
在 C 语言中,不定参宏函数是一种强大的工具,它允许宏接受可变数量的参数,类似于不定参函数,不过宏是在预处理阶段展开的。下面详细介绍不定参宏函数的使用,以自定义日志打印宏为例。
代码示例
#include <stdio.h>// 定义不定参宏函数,用于日志打印#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)int main() {int number = 42;char name[] = "Alice";// 使用不定参宏函数进行日志打印LOG("The number is %d.\n", number);LOG("The name is %s.\n", name);LOG("Combined: %s's lucky number is %d.\n", name, number);return 0;}
代码解释
1. 宏定义部分
#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, __VA_ARGS__)
- LOG 是宏的名称,它模拟了一个简单的日志打印功能。
- fmt 是一个固定参数,它代表了 printf 函数的格式控制字符串。
- ... 表示可变参数部分,意味着在调用 LOG 宏时可以传入任意数量的额外参数。
- __FILE__ 和 __LINE__ 是预定义的宏,__FILE__ 会在预处理时被替换为当前源文件的文件名,__LINE__ 会被替换为宏调用所在的代码行号。
- __VA_ARGS__ 是一个特殊的预定义宏,它会被替换为调用宏时传入的可变参数列表。
2. 主函数部分
int main() {int number = 42;char name[] = "Alice";LOG("The number is %d.\n", number);LOG("The name is %s.\n", name);LOG("Combined: %s's lucky number is %d.\n", name, number);return 0;}
- 首先定义了一个整数变量 number 和一个字符数组 name。
- 然后多次调用 LOG 宏进行日志打印,每次调用传入不同数量的参数。
- 第一次调用 LOG("The number is %d.\n", number); 时,fmt 被替换为 "The number is %d.\n",__VA_ARGS__ 被替换为 number。
- 第二次调用 LOG("The name is %s.\n", name); 时,fmt 被替换为 "The name is %s.\n",__VA_ARGS__ 被替换为 name。
- 第三次调用 LOG("Combined: %s's lucky number is %d.\n", name, number); 时,fmt 被替换为 "Combined: %s's lucky number is %d.\n",__VA_ARGS__ 被替换为 name, number。
输出结果
假设上述代码保存为 main.c,编译运行后可能的输出如下:
[main.c:11] The number is 42.[main.c:12] The name is Alice.[main.c:13] Combined: Alice's lucky number is 42.
这样,通过不定参宏函数,我们可以方便地在日志中记录文件名和行号信息,同时灵活处理不同数量的参数。
若要让 LOG 宏函数支持像 LOG("A charmer") 这种只传入一个参数的情况,就需要处理可变参数为空的情形。在 C 语言里,当可变参数为空时,__VA_ARGS__ 会在宏展开时产生一个多余的逗号,这会引发编译错误。为解决此问题,可借助 ## 操作符,它能在可变参数为空时去除多余的逗号。
#include <stdio.h>// 定义支持空可变参数的不定参宏函数#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__)int main() {// 传入多个参数LOG("The name is %s.\n", "A charmer");// 只传入一个参数LOG("A charmer\n");return 0;}
- 宏定义:#define LOG(fmt, ...) printf("[%s:%d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__),这里的 ## 操作符是关键,当可变参数 __VA_ARGS__ 为空时,它会移除前面多余的逗号,避免编译错误。
C风格不定参函数
在 C 语言里,不定参函数指的是可以接受可变数量参数的函数。为了实现不定参函数,需要用到 <stdarg.h>
头文件里的一些宏,这些宏能够帮助你访问可变参数列表。下面为你提供一个打印整数的不定参函数示例,同时会详细解释代码的逻辑。
#include <stdio.h>
#include <stdarg.h>// 不定参函数,用于打印多个整数
void print_integers(int count, ...) {// 定义 va_list 类型的变量,用于存储可变参数列表信息va_list args;// 初始化可变参数列表,count 为最后一个固定参数,表示可变参数的数量va_start(args, count);for (int i = 0; i < count; i++) {// 从可变参数列表中取出一个 int 类型的参数int num = va_arg(args, int);// 打印取出的整数printf("%d ", num);}// 换行printf("\n");// 结束对可变参数列表的使用,释放相关资源va_end(args);
}int main() {// 调用 print_integers 函数,传入可变参数的数量 5 以及 5 个整数print_integers(5, 1, 2, 3, 4, 5);return 0;
}
代码解释
-
头文件包含:
#include <stdio.h>
:提供标准输入输出函数,例如printf
。#include <stdarg.h>
:提供处理可变参数列表所需的宏和类型。
-
print_integers
函数:va_list args
:定义一个va_list
类型的变量args
,用于存储可变参数列表的信息。va_start(args, count)
:初始化可变参数列表,count
是最后一个固定参数,它表示后续可变参数的数量。va_arg(args, int)
:从可变参数列表中取出一个int
类型的参数。va_end(args)
:结束对可变参数列表的使用,释放相关资源。
-
main
函数:- 调用
print_integers
函数,传入可变参数的数量5
以及5
个整数1
、2
、3
、4
、5
。程序会将这些整数依次打印出来。
- 调用
这个示例展示了如何使用不定参函数来打印多个整数,你可以根据需求修改调用时传入的参数数量和具体整数值。
模拟实现printf
对字符串处理
下面是一个模拟实现 printf
函数的 C 语言代码示例,该示例支持 %d
(打印整数)、%s
(打印字符串)和 %c
(打印字符)这几种常见的格式说明符。
#include <stdio.h>
#include <stdarg.h>// 模拟实现 printf 函数
void my_printf(const char *format, ...) {va_list args;va_start(args, format);while (*format) {if (*format == '%') {format++;switch (*format) {case 'd': {int num = va_arg(args, int);printf("%d", num);break;}case 's': {char *str = va_arg(args, char *);printf("%s", str);break;}case 'c': {int ch = va_arg(args, int);printf("%c", (char)ch);break;}default:putchar(*format);break;}} else {putchar(*format);}format++;}va_end(args);
}int main() {int num = 123;char *str = "Hello";char ch = 'A';my_printf("Number: %d, String: %s, Character: %c\n", num, str, ch);return 0;
}
代码解释
-
头文件包含:
#include <stdio.h>
:提供标准输入输出函数,如printf
和putchar
。#include <stdarg.h>
:提供处理可变参数列表所需的宏和类型。
-
my_printf
函数:va_list args
:定义一个va_list
类型的变量args
,用于存储可变参数列表的信息。va_start(args, format)
:初始化可变参数列表,format
是最后一个固定参数。- 遍历
format
字符串:- 当遇到
%
时,检查下一个字符:- 如果是
d
,使用va_arg(args, int)
取出一个int
类型的参数并打印。 - 如果是
s
,使用va_arg(args, char *)
取出一个字符串指针并打印。 - 如果是
c
,使用va_arg(args, int)
取出一个字符(以int
类型存储)并打印。 - 对于其他字符,直接输出该字符。
- 如果是
- 如果不是
%
,直接输出该字符。
- 当遇到
va_end(args)
:结束对可变参数列表的使用,释放相关资源。
-
main
函数:- 定义一个整数
num
、一个字符串str
和一个字符ch
。 - 调用
my_printf
函数,传入格式化字符串和相应的参数。
- 定义一个整数
这个示例只是一个简单的模拟实现,真正的 printf
函数支持更多的格式说明符和复杂的功能。
用vasprintf接口⭐
vasprintf
是一个标准 C 库函数,它可以根据格式化字符串和可变参数列表动态分配内存并生成格式化后的字符串。下面是一个使用 vasprintf
接口模拟实现 printf
功能的示例代码:
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>// 模拟实现 printf 函数,使用 vasprintf
void my_printf(const char *format, ...) {va_list args;va_start(args, format);char *output = NULL;// 使用 vasprintf 动态分配内存并格式化字符串if (vasprintf(&output, format, args) == -1) {perror("vasprintf");va_end(args);return;}// 输出格式化后的字符串printf("%s", output);// 释放动态分配的内存free(output);va_end(args);
}int main() {int num = 42;const char *str = "World";char ch = '!';my_printf("Hello, %s %d%c\n", str, num, ch);return 0;
}
代码解释
1. 头文件包含
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
stdio.h
:提供标准输入输出函数,如printf
和perror
。stdarg.h
:提供处理可变参数列表所需的宏和类型,如va_list
、va_start
、va_end
。stdlib.h
:提供动态内存分配和释放函数,如malloc
、free
,vasprintf
也依赖此头文件。
2. my_printf
函数
void my_printf(const char *format, ...) {va_list args;va_start(args, format);char *output = NULL;if (vasprintf(&output, format, args) == -1) {perror("vasprintf");va_end(args);return;}printf("%s", output);free(output);va_end(args);
}
va_list args
:定义一个va_list
类型的变量args
,用于存储可变参数列表的信息。va_start(args, format)
:初始化可变参数列表,format
是最后一个固定参数。vasprintf(&output, format, args)
:根据格式化字符串format
和可变参数列表args
动态分配内存并生成格式化后的字符串,将结果存储在output
指针指向的内存区域。如果分配内存失败,vasprintf
返回 -1。perror("vasprintf")
:如果vasprintf
失败,使用perror
输出错误信息。printf("%s", output)
:输出格式化后的字符串。free(output)
:释放vasprintf
动态分配的内存,避免内存泄漏。va_end(args)
:结束对可变参数列表的使用,释放相关资源。
3. main
函数
int main() {int num = 42;const char *str = "World";char ch = '!';my_printf("Hello, %s %d%c\n", str, num, ch);return 0;
}
- 定义一个整数
num
、一个字符串str
和一个字符ch
。- 调用
my_printf
函数,传入格式化字符串和相应的参数,模拟printf
函数的功能。
通过这种方式,我们利用 vasprintf
接口实现了一个简单的 printf
模拟函数。
C++风格不定参函数
#include <iostream>// 无参的 xprintf 函数,用于递归终止
void xprintf() {std::cout << std::endl;
}// 可变参数模板的 xprintf 函数
template<typename T, typename... Args>
void xprintf(const T &v, Args &&...args) {std::cout << v;if ((sizeof...(args)) > 0) {xprintf(std::forward<Args>(args)...);} else {xprintf();}
}int main() {xprintf(1);xprintf(1, 2, 3);return 0;
}
代码功能概述
这段 C++ 代码实现了一个名为 xprintf
的不定参数函数,其功能是将传入的不定数量的参数依次输出到控制台,每个 xprintf
调用结束后会换行。
- 模板参数:
template<typename T, typename... Args>
:定义了一个可变参数模板。T
代表第一个参数的类型,typename... Args
是可变参数包,它能容纳零个或多个不同类型的参数。- 函数参数:
const T &v
:第一个参数的常量引用,采用常量引用可避免不必要的拷贝,提高性能。Args &&...args
:可变参数包,使用右值引用(也叫万能引用),它既可以绑定左值,也可以绑定右值,并且结合std::forward
能实现完美转发。- 函数体逻辑:
std::cout << v;
:输出第一个参数。if ((sizeof...(args)) > 0)
:sizeof...(args)
用于获取可变参数包中参数的数量。若数量大于 0,说明还有剩余参数,就递归调用xprintf
函数,同时使用std::forward
对剩余参数进行完美转发,以保留参数的左值或右值属性。else
分支:若可变参数包为空,调用无参的xprintf()
函数,输出换行符,结束递归。
为什么要xprintf(),而不是直接cout<<endl?
当你调用
xprintf(1)
时,编译器会进行模板实例化的过程。在这个过程中:
- 模板参数推导:对于
template<typename T, typename... Args>
,根据传入的参数1
(类型为int
),T
被推导为int
。而Args
被推导为空参数包,因为此时只有一个参数1
传入,没有其他参数来构成可变参数包了。- 函数体执行:
std::cout << v;
这行代码会将v
(也就是1
)输出到标准输出流。- 接下来判断
if ((sizeof...(args)) > 0)
,由于Args
被推导为空参数包,sizeof...(args)
的值为0
,所以这个条件不成立,会执行else
分支。- 在
else
分支中,代码是std::cout << std::endl;
,这一步本身不会有问题,它会输出一个换行符。但是,这里存在一个潜在的问题,就是在这个函数模板中,
xprintf
函数本身是递归调用的(xprintf(std::forward<Args>(args)...);
这一行)。当编译器处理函数调用时,它需要知道在所有可能的情况下,xprintf
调用都有对应的函数定义可以匹配。在
xprintf(1)
这种情况下,虽然当前这次调用不会触发递归调用(因为可变参数包为空),但编译器在编译时并不能保证未来不会在其他情况下触发递归调用到一个无参数的xprintf
调用。也就是说,从编译器的角度来看,为了保证函数调用的完整性和正确性,它需要找到一个无参数的xprintf
函数定义,以便在递归过程中可能出现的无参数调用时能够正确解析。由于在没有定义无参
xprintf
函数的情况下,当编译器遇到这种可能的无参数调用情况(即使在当前调用中不会实际发生),它找不到合适的函数定义来匹配,就会报错,提示没有找到匹配的xprintf
函数。简单来说,就是编译器为了确保函数调用在各种情况下都能正确解析,要求有一个无参的
xprintf
函数定义来应对递归调用中可能出现的无参数调用场景,即使当前这次调用不会触发这个情况。
如果你对日志系统感到兴趣,欢迎关注我👉【A charmer】
后续我将继续带你实现日志系统~