📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
一、不定参函数
在 C++ 中,不定参数函数(Variadic Functions) 是一种可以接受数量不确定的参数的函数。这种机制常用于像 printf
这样的标准库函数,也广泛应用于日志系统、格式化输出、通用容器构造等场景
1. C语言不定参数
🧱C语言中需要引入stdarg.h的头文件,使用其中的va_list、va_start、va_arg和va_end,适用于兼容性要求较高的项目
- 对于需要严格兼容 C 或嵌入式环境,才考虑使用
<cstdarg>
✅ 基本语法:
#include <cstdarg>
#include <iostream>
void printValues(int count, ...) {
va_list args; // 定义 va_list 类型的变量,用于存储可变参数列表信息
va_start(args, count); // 初始化可变参数列表,count 为最后一个固定参数,表示可变参数的数量
for (int i = 0; i < count; ++i) {
std::cout << va_arg(args, int) << " ";
}
va_end(args); // 结束对可变参数列表的使用,释放相关资源
std::cout << std::endl;
}
printValues(3, 10, 20, 30); // 输出: 10 20 30
-
va_list
:是一个类型,用于声明一个变量,用该变量来存储不定参数的信息。 -
va_start
:用于初始化va_list,让va_list指向不定参数的起始位置,可以接受两个参数,第一个是va_list对象,第二个是用于确定不定参数的起始位置。 -
va_arg
:用于获取当前位置的值,在每一次使用以后,会将指针移动到下一个可变参数的位置,可以接受两个参数,一个是va_list,一个是要获取的参数的类型。 -
va_end
:用于清理va_list对象,确保在使用完不定参以后正确的释放资源。
⚠️ 注意事项:
- 必须显式指定参数个数(如
count
),不能自动推断 - 类型必须显式转换为某种具体类型(如
va_arg(args, int)
),否则行为未定义 - 不支持类类型(如
std::string
、自定义类),除非做类型转换或封装 - 容易引发类型不匹配导致的错误
代码样例
- 补充:
vasprintf
:动态分配内存来存储格式化之后的字符串,但可以接受可变参数,int vasprintf (char **buf, const char *format, va_list ap)
buf
分别表示指向char指针的指针,用来存储格式化后的字符串地址format
是一个格式化字符串,包含要打印的文本和格式说明符ap
表示可变参数列表
vasprintf
会根据format
字符串和可变参数列表ap的内容动态的分配足够的内存来存储格式化后的字符串,并将地址存储在buf
指针中,如果成功,就会返回格式化后的字符串的长度
代码样例1:
#include <iostream>
#include <cstdarg>
void printNum(int n, ...)
{
va_list al; //定义一个变量,后面用来存储不定参数的信息
va_start(al, n); // 初始化va_list,让va_list指向不定参数列表的起始位置(让al和不定参产生联系)
for (int i = 0; i < n; i++)
{
int num = va_arg(al, int); // 此时al与不定参就产生了绑定,使用va_arg来取出当前位置的参数,取完以后会自动往后移一步
std::cout << num << std::endl;
}
va_end(al); // 清空可变参数列表--其实是将al置空
}
int main()
{
printNum(3, 11, 22, 33);
printNum(5, 44, 55, 66, 77, 88);
return 0;
}
代码样例2:
#include <iostream>
#include <cstdarg>
void myprintf(const char *fmt, ...)
{
// int vasprintf(char **strp, const char *fmt, va_list ap);
char *res;
va_list al; //1.初始化
va_start(al, fmt); //2.绑定,设置起始位置
int len = vasprintf(&res, fmt, al); //3.按照fmt格式来打印al中的内容,fmt是用户传入的
va_end(al); //4.结束
std::cout << res << std::endl;
free(res);
}
int main()
{
myprintf("%s-%d", "⼩明", 18);
return 0;
}
可以这样来理解:
va_list
只是定义了一个变量来存储,是个空壳。当使用va_start
以后,会让这个空壳与不定参产生联系,也就可以理解成将不定参的变量都存储在这个空壳里了,此时也就不是空壳了。此时我只需要不断地使用va_arg
去从va_list
中取出数据即可。使用完毕以后就用va_end
关闭。
代码样例3:模拟实现 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;
}
2. C++不定参函数 – 模板参数包(Variadic Templates)
这是 C++11 引入的一种更现代、类型安全、灵活的方式
✅ 特点:
- 类型安全 :每个参数都保留了其原始类型。
- 支持类模板、函数模板、完美转发 。
- 支持递归展开、折叠表达式(Fold Expressions) 等高级特性。
- 适合现代 C++ 编程风格
✅ 基本语法:
template<typename... Args>
void printValues(Args&&... args){
(std::cout << ... << args) << std::endl;
}
printValues(10, "hello", 3.14); // 10hello3.14
- 💡 支持任意类型、任意数量的参数,包括类类型、引用、右值等
📝 更复杂的调用(带分隔符):
// 基础版本:处理单个参数
template<typename T>
void printWithSep(T value) {
std::cout << value; // 版本 A:输出 value
// std::cout << std::endl; // 版本 B:仅输出换行
}
// 可变参数版本:处理多个参数
template<typename T, typename... Args>
void printWithSep(T first, Args&&... rest) {
std::cout << first << ", ";
printWithSep(std::forward<Args>(rest)...);
}
printWithSep(10, "world", true); // 输出: 10, world, 1
🧠 核心逻辑解析
① 第一次调用 :
printWithSep(10, "world", true);
- 匹配到 可变参数模板 (
T = int
,Args = const char*, bool
) - 输出
10,
- 调用
printWithSep("world", true);
② 第二次调用 :
printWithSep("world", true);
- 匹配到 可变参数模板 (
T = const char*
,Args = bool
) - 输出
"world",
- 调用
printWithSep(true);
③ 第三次调用 :
printWithSep(true);
- 匹配到 基础版本 (
T = bool
)
✅ 如果使用 std::cout << value;
(版本 A):
- 输出
1
(因为std::cout
默认不启用std::boolalpha
,true
输出为1
) - 最终输出:
10, world, 1
❌ 如果使用 std::cout << std::endl;
(版本 B):
- 不输出
value
,只输出换行符\n
- 最终输出:
10, world,
(注意这里缺少最后一个值true
的输出)
那么我有个问题:为什么需要一个无参函数呢?
⚠️ 实际问题:
当你调用
printWithSep(std::forward<Args>(rest)...);
时,如果rest...
是空的,那么这行代码会变成:printWithSep();
- 也就是说,它试图调用一个 不带任何参数的
printWithSep()
函数🧱 出错原因:
- C++ 是 静态类型语言 ,模板函数的实例化发生在 编译期 。编译器会根据你写的模板函数自动生成相应的函数体。
- 如果你没有定义一个 无参函数版本 来匹配
printWithSep();
这个调用,编译器就会报错,提示找不到合适的函数。
🧪需要无参函数原因 | 说明 |
---|---|
✅ 模板实例化要求 | 所有路径上的函数调用都必须在编译期找到匹配的函数签名 |
✅ 避免编译错误 | 当rest... 为空时,printWithSep(rest...) 会调用printWithSep() |
❌ if 判断无法绕过编译检查 | 即使条件为 false,编译器仍需验证所有语法路径 |
✅ 提供清晰的递归终止条件 | 明确定义递归终点,提高代码可读性和维护性 |
那么我还有个问题:下面的输出是什么呢?
template<typename T>
void printWithSep(T value) {
std::cout << value;
}
void printWithSep() {
std::cout << "printWithSep" << std::endl;
}
template<typename T, typename... Args>
void printWithSep(T first, Args&&... rest) {
std::cout << first << ", ";
/* printWithSep(std::forward<Args>(rest)...);*/
if ((sizeof...(rest)) > 0) {
printWithSep(std::forward<Args>(rest)...);
}
else {
printWithSep();
cout << "------------" << endl;
}
}
printWithSep(10, "world", true); // 仍然输出 10, world, 1
为什么不可以用 if 判断代替呢??
- 即使你在运行时判断
sizeof...(rest)
是否为 0,编译器仍然会检查所有路径下的函数调用是否合法 。 - 换句话说,即使
if
条件为假,只要代码中存在printWithSep(...)
调用,编译器就会要求该函数存在并匹配。 - 因此就不会走到 else 的语句里面去
如何修复
-
把模板化的无参函数删除即可,就会保证递归调用最后会进入 else 分支
void printWithSep() { std::cout << "printWithSep" << std::endl; } template<typename T, typename... Args> void printWithSep(T first, Args&&... rest) { std::cout << first << ", "; /* printWithSep(std::forward<Args>(rest)...);*/ if ((sizeof...(rest)) > 0) { printWithSep(std::forward<Args>(rest)...); } else { printWithSep(); cout << "------------" << endl; } }
-
合并逻辑,避免双重递归
// 终止条件 void printWithSep() { std::cout << "------" << std::endl; } // 递归处理 template<typename T, typename... Args> void printWithSep(T first, Args&&... rest) { std::cout << first << ", "; printWithSep(std::forward<Args>(rest)...); }
-
使用折叠表达式简化逻辑,完全避免手动递归
template<typename... Args> void printWithSep(Args&&... args) { ((std::cout << args << ", "), ...); std::cout << "\b\b \n"; // 回退两个字符,替换最后的 ", " 为 " " }
3. 两种方式对比⚖️
特性 | C 风格 (<cstdarg> ) | C++ 模板参数包 |
---|---|---|
类型安全 | ❌ 否 | ✅ 是 |
参数类型 | 必须显式转换 | 任意类型 |
可扩展性 | 有限 | 极高(支持模板元编程) |
性能 | 一般 | 更优(编译期展开) |
兼容性 | ✅ 广泛支持(包括 C) | C++11 及以上 |
代码简洁性 | 一般 | ✅ 高 |
4. 进阶使用
-
折叠表达式(Fold Expressions):C++17 支持折叠表达式,极大简化了不定参展开逻辑
template<typename... Args> void sum(Args... args) { std::cout << (args + ... + 0) << std::endl; // 计算总和 }
-
完美转发(Perfect Forwarding)
template<typename... Args> void forwardToFunction(Args&&... args) { someOtherFunction(std::forward<Args>(args)...); }
-
检查参数是否为空
if constexpr (sizeof...(Args) == 0) { // 处理无参数的情况 }
-
避免展开顺序陷阱:递归展开或折叠表达式的执行顺序应明确,否则可能引发副作用问题。
2. 不定参宏函数
在 C/C++ 编程中,不定参宏函数(Variadic Macros) 是一种允许你定义带有可变数量参数的宏的技术。它主要用于 日志输出、调试辅助、格式化输出 等场景,尤其在嵌入式开发、跨平台项目中非常常见。
- 基本概念:不定参宏函数本质上是使用了预处理器的
__VA_ARGS__
关键字,这个关键字表示宏定义中未命名的参数部分。它可以匹配任意数量的参数,并在宏展开时替换为实际传入的参数列表。
✅ 定义方式(C99 标准引入)
#define MACRO_NAME(arg1, arg2, ..., __VA_ARGS__) ...
__VA_ARGS__
表示可变参数部分- 前面至少有一个固定参数(C99 要求),C++ 中可以省略(GCC 扩展支持空参数)
1. 基本使用
#include <stdio.h>
// 定义一个不定参宏
#define DEBUG_PRINT(fmt, ...) printf("[DEBUG] " fmt "\n", ##__VA_ARGS__)
int main() {
int a = 10;
float b = 3.14f;
DEBUG_PRINT("a = %d", a); // 输出: [DEBUG] a = 10
DEBUG_PRINT("a = %d, b = %f", a, b); // 输出: [DEBUG] a = 10, b = 3.140000
}
注意:
-
##__VA_ARGS__
的写法是为了在没有额外参数时自动去掉前面的逗号,防止编译错误 -
fmt
是格式字符串,__VA_ARGS__
是对应的变量列表 -
基本概念:不定参宏函数本质上是使用了预处理器的
__VA_ARGS__
关键字,这个关键字表示宏定义中未命名的参数部分。它可以匹配任意数量的参数,并在宏展开时替换为实际传入的参数列表。
2. 进阶用法 🛠️
2.1 日志级别控制(带条件判断)
#define LOG(level, fmt, ...) \
do { \
if (log_level <= level) { \
printf("[%s] " fmt "\n", #level, ##__VA_ARGS__); \
} \
} while(0)
// 使用
LOG(INFO, "User login succeeded");
LOG(ERROR, "Database connection failed");
这种模式广泛应用于日志系统中,可以控制不同级别的信息是否输出。
2.2 结合函数封装(推荐做法)
为了提高类型安全性,通常会将宏作为包装器,调用一个真正的函数:
void log_message(const char* level, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
printf("[%s] ", level);
vprintf(fmt, args);
printf("\n");
va_end(args);
}
#define LOG(level, fmt, ...) log_message(#level, fmt, ##__VA_ARGS__)
优点:宏只做参数转发,底层函数负责处理逻辑,提升可维护性和安全性。
4. 注意事项与限制⚠️
限制 | 说明 |
---|---|
无类型检查 | 宏不会进行类型检查,容易因参数类型不匹配引发运行时错误(如%d 匹配float ) |
格式字符串必须匹配参数 | 否则行为未定义(可能导致崩溃或乱码) |
不能直接用于类成员函数 | 需要特殊处理this 指针或封装为静态方法 |
编译期展开 | 宏在预处理阶段被替换,无法进行动态绑定或泛型编程 |
可读性差 | 复杂宏容易造成代码难以理解和维护 |
5. 不定参宏 vs 不定参函数✅
特性 | 不定参宏 | 不定参函数 |
---|---|---|
类型安全 | ❌ 否 | ✅ 是(尤其是 C++ 模板) |
参数类型 | 必须手动指定 | 可自动推断 |
性能 | 更低开销(仅文本替换) | 略高(函数调用) |
可扩展性 | 有限 | 极高(支持递归、折叠表达式) |
兼容性 | ✅ 广泛支持 | C++11 及以上 |
推荐用途 | 调试、日志、平台适配 | 实际功能实现、通用组件 |