首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >《不定参数与不定宏参数:C++程序员必须掌握的灵活编程技巧》

《不定参数与不定宏参数:C++程序员必须掌握的灵活编程技巧》

作者头像
IsLand1314
发布2025-05-01 21:29:29
发布2025-05-01 21:29:29
4150
举报
文章被收录于专栏:学习之路学习之路

一、不定参函数

在 C++ 中,不定参数函数(Variadic Functions) 是一种可以接受数量不确定的参数的函数。这种机制常用于像 printf 这样的标准库函数,也广泛应用于日志系统、格式化输出、通用容器构造等场景

1. C语言不定参数

🧱C语言中需要引入stdarg.h的头文件,使用其中的va_list、va_start、va_arg和va_end,适用于兼容性要求较高的项目

  • 对于需要严格兼容 C 或嵌入式环境,才考虑使用 <cstdarg>

基本语法

代码语言:javascript
复制
#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
  1. va_list:是一个类型,用于声明一个变量,用该变量来存储不定参数的信息。
  2. va_start:用于初始化va_list,让va_list指向不定参数的起始位置,可以接受两个参数,第一个是va_list对象,第二个是用于确定不定参数的起始位置。
  3. va_arg:用于获取当前位置的值,在每一次使用以后,会将指针移动到下一个可变参数的位置,可以接受两个参数,一个是va_list,一个是要获取的参数的类型。
  4. 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

代码语言:javascript
复制
#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

代码语言:javascript
复制
#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

代码语言:javascript
复制
#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++ 编程风格

基本语法

代码语言:javascript
复制
template<typename... Args>
void printValues(Args&&... args){
    (std::cout << ... << args) << std::endl;
}

printValues(10, "hello", 3.14); // 10hello3.14
  • 💡 支持任意类型、任意数量的参数,包括类类型、引用、右值等

📝 更复杂的调用(带分隔符)

代码语言:javascript
复制
// 基础版本:处理单个参数
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

🧠 核心逻辑解析

第一次调用

代码语言:javascript
复制
printWithSep(10, "world", true);
  • 匹配到 可变参数模板T = int, Args = const char*, bool
  • 输出 10,
  • 调用 printWithSep("world", true);

第二次调用

代码语言:javascript
复制
printWithSep("world", true);
  • 匹配到 可变参数模板T = const char*, Args = bool
  • 输出 "world",
  • 调用 printWithSep(true);

第三次调用

代码语言:javascript
复制
printWithSep(true);
  • 匹配到 基础版本T = bool

✅ 如果使用 std::cout << value;(版本 A):

  • 输出 1(因为 std::cout 默认不启用 std::boolalphatrue 输出为 1
  • 最终输出:10, world, 1

❌ 如果使用 std::cout << std::endl;(版本 B):

  • 不输出 value,只输出换行符 \n
  • 最终输出:10, world, (注意这里缺少最后一个值 true 的输出)

那么我有个问题:为什么需要一个无参函数呢?

⚠️ 实际问题

当你调用 printWithSep(std::forward<Args>(rest)...); 时,如果 rest... 是空的,那么这行代码会变成:

代码语言:javascript
复制
printWithSep();
  • 也就是说,它试图调用一个 不带任何参数的 printWithSep() 函数

🧱 出错原因

  • C++ 是 静态类型语言 ,模板函数的实例化发生在 编译期 。编译器会根据你写的模板函数自动生成相应的函数体。
  • 如果你没有定义一个 无参函数版本 来匹配 printWithSep(); 这个调用,编译器就会报错,提示找不到合适的函数。

🧪需要无参函数原因

说明

✅ 模板实例化要求

所有路径上的函数调用都必须在编译期找到匹配的函数签名

✅ 避免编译错误

当rest...为空时,printWithSep(rest...)会调用printWithSep()

❌ if 判断无法绕过编译检查

即使条件为 false,编译器仍需验证所有语法路径

✅ 提供清晰的递归终止条件

明确定义递归终点,提高代码可读性和维护性

那么我还有个问题:下面的输出是什么呢?

代码语言:javascript
复制
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 分支

代码语言:javascript
复制
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;
    }
}

合并逻辑,避免双重递归

代码语言:javascript
复制
// 终止条件
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)...);
}

使用折叠表达式简化逻辑,完全避免手动递归

代码语言:javascript
复制
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 支持折叠表达式,极大简化了不定参展开逻辑

代码语言:javascript
复制
template<typename... Args>
void sum(Args... args) {
    std::cout << (args + ... + 0) << std::endl;  // 计算总和
}

完美转发(Perfect Forwarding)

代码语言:javascript
复制
template<typename... Args>
void forwardToFunction(Args&&... args) {
    someOtherFunction(std::forward<Args>(args)...);
}

检查参数是否为空

代码语言:javascript
复制
if constexpr (sizeof...(Args) == 0) {
    // 处理无参数的情况
}

避免展开顺序陷阱:递归展开或折叠表达式的执行顺序应明确,否则可能引发副作用问题。

2. 不定参宏函数

在 C/C++ 编程中,不定参宏函数(Variadic Macros) 是一种允许你定义带有可变数量参数的宏的技术。它主要用于 日志输出、调试辅助、格式化输出 等场景,尤其在嵌入式开发、跨平台项目中非常常见。

  • 基本概念:不定参宏函数本质上是使用了预处理器的 __VA_ARGS__ 关键字,这个关键字表示宏定义中未命名的参数部分。它可以匹配任意数量的参数,并在宏展开时替换为实际传入的参数列表。

✅ 定义方式(C99 标准引入)

代码语言:javascript
复制
#define MACRO_NAME(arg1, arg2, ..., __VA_ARGS__) ...
  • __VA_ARGS__ 表示可变参数部分
  • 前面至少有一个固定参数(C99 要求),C++ 中可以省略(GCC 扩展支持空参数)
1. 基本使用
代码语言:javascript
复制
#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 日志级别控制(带条件判断)
代码语言:javascript
复制
#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 结合函数封装(推荐做法)

为了提高类型安全性,通常会将宏作为包装器,调用一个真正的函数:

代码语言:javascript
复制
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 及以上

推荐用途

调试、日志、平台适配

实际功能实现、通用组件

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-04-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、不定参函数
    • 1. C语言不定参数
      • 代码样例
    • 2. C++不定参函数 – 模板参数包(Variadic Templates)
    • 3. 两种方式对比⚖️
    • 4. 进阶使用
  • 2. 不定参宏函数
    • 1. 基本使用
    • 2. 进阶用法 🛠️
      • 2.1 日志级别控制(带条件判断)
      • 2.2 结合函数封装(推荐做法)
  • 4. 注意事项与限制⚠️
  • 5. 不定参宏 vs 不定参函数✅
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档