可变参 理解

C/C++可变参数详解
原文来自:http://www.dutor.net/index.php/2011/08/variadic/

使用变参

  C/C++提供了函数的可变参数(variadic)机制,大部分人写的第一个C程序恐怕就是Hello World吧,使用的应该也是printf(“Hello, World\n”)。printf就是一个使用可变参数的典型,它的原型声明为,

int printf(const char *fmt, ...);

  其中返回值为实际输出字符个数,fmt为格式控制字符串,而”…”便声明了一个可变参数,你可以根据传递0个或多个参数给printf。printf内部会根据格式控制串中的格式指定符号(d, f, p等等)来逐个解析通过可变参数传进的实参变量。
  为解析可变参数,C语言提供了一个va_list类型和四个宏,分别是va_start, va_arg, va_end, 和va_copy,这些宏声明在stdarg.h中。为了方便描述,下面实现一个简单的类似printf的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void mockprintf(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    for (const char *s = fmt; *s; ++s)
    {
        switch(*s)
        {
            case 'd':
                printf("meet d\n");
                int d = va_arg(ap, int);
                printf("%d\n", d);
                break;
            case 's':
                printf("meet s\n");
                const char *str = va_arg(ap, char*);
                printf("%s\n", str);
                break;
            case 'c':
                printf("meet c\n");
                char c = va_arg(ap, int);
                printf("%c\n", c);
                break;
            default:
                printf("unknown format specifier\n");
        }
    }
    va_end(ap);
}
int
main()
{
    mockprintf("cdfs", 'A', 0x45, "string");
    return 0;
}

  va_list的实现与编译器和平台相关,通常是一个指向参数栈的指针。va_start使用变参列表前的最后一个命名参数(named argument)作为参数,以此定位变参列表的第一个参数的地址,并将ap指向该参数(此处假设va_list实现为指针)。宏va_arg需要两个参数,va_list变量和下一个预期的参数的类型,该宏以指定类型返回(展开为)对应的参数值,并调整va_list指向下一个参数。最后,每一个va_start需要一个va_end作为结束。另外,示例函数中没有用到va_copy,这是一个用来复制va_list变量到另一个va_list变量的宏,目的是应对平台间va_list实现的差异。
  值得一提的是,va_start和va_end可以重复调用,用以多次对变参列表进行解析。

printf家族

  C的printf家族包含8个成员,原型如下,

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int printf(const char *fmt, ...);
int fprintf(FILE *stream, const char *fmt, ...);
int sprintf(char *str, const char *fmt, ...);
int snprintf(char *str, size_t size, const char *fmt, ...);
 
#include <stdarg.h>
int vprintf(const char *fmt, va_list ap);
int vfprintf(FILE *stream, const char *fmt, va_list ap);
int vsprintf(char *str, const char *fmt, va_list ap);
int vsnprintf(char *str, size_t size, const char *fmt, va_list ap);

  前四个函数没有什么特殊的。后面四个v系列可以接受va_list变量,通常用在对可变参数输出的包装,在日志记录系统中较为常用。比如下面代码,

1
2
3
4
5
6
7
8
9
enum LogLevel { ERROR, WARN, INFO, DEBUG }
void log(LogLevel level, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    //~ write buf to file, or do something else.
}

  v系列函数并不会调用va_end宏,因此在这些函数返回后,需要调用函数自己进行va_end。若要再次解析变参列表,就需要重新va_start, va_end。

宏变参

  除了函数,在C/C++中,带参宏定义也可以接受变参,使用方法和函数类似。比如,若将上面的log函数的某个级别的日志输入定义成宏,

#define log_warn(fmt, ...) log(WARN, fmt, __VA_ARGS__)

  __VA_ARGS__只是被预处理器简单的展开为传递给宏log_warn的变参列表,包括逗号分隔符。若想使用具有鲜明意义的名字,而不是统一的__VA_ARGS__,可以这样,

#define log_warn(fmt, args...) log(WARN, fmt, args)

  上述宏定义中,有一个问题值得注意,就是当变参列表为空时,log函数调用的参数列表会有一个结尾的逗号,这在某些编译器中会被诊断为错误(据说MSVC不会),这种情况下可以将fmt也纳入变参列表,

#define log_warn(...) log(WARN, __VA_ARGS__)
Tags: C, Cpp, 可变参数.
你好!除了代码,此处没有多少原创之物,皆为本人搜集、整理、总结之记录与心得,欢迎转载分享!转载时请尽量注明出处,将不胜感激。祝你健康、快乐!

RFC: Request For Comments. Orz..


### 定义和使用可变长参函数 #### C/C++ 中的可变长参函数 在 C 和 C++ 中,可以通过标准库 `<cstdarg>` 或 `<stdarg.h>` 来实现可变长参函数。这种机制允许函数接受不确定数量的参数。以下是定义和使用的具体方法: 1. **头文件引入** 需要包含 `<cstdarg>` 头文件。 2. **宏声明** 使用 `va_list` 类型来存储参数列表,并通过以下三个宏操作它: - `va_start`: 初始化 va_list 对象。 - `va_arg`: 获取下一个参数。 - `va_end`: 清理 va_list 对象。 3. **示例代码** ```cpp #include <iostream> #include <cstdarg> double average(int count, ...) { double sum = 0; va_list args; // 声明变量长度参数列表 va_start(args, count); // 初始化args并指向第一个可变参数 for (int i = 0; i < count; ++i) { sum += va_arg(args, int); // 提取当前参数 } va_end(args); // 结束处理 return sum / count; } int main() { std::cout << "Average: " << average(4, 10, 20, 30, 40) << std::endl; return 0; } ``` 此代码展示了如何计算一组整数的平均值[^1]。 --- #### Python 中的可变长参函数 Python 支持两种形式的可变长参:位置参数 (`*args`) 和关键字参数 (`**kwargs`)。 1. **位置参数 (*args)** 接收任意数量的位置参数并将它们打包成一个元组。 2. **关键字参数 (**kwargs)** 接收任意数量的关键字参数并将它们打包成一个字典。 3. **混合使用** 如果需要同时支持固定参数、位置参数和关键字参数,则需注意顺序:普通参数 -> *args -> 关键字参数或默认值 -> **kwargs。 4. **示例代码** ```python def example_function(a, b, *args, **kwargs): print(f"Fixed parameters: a={a}, b={b}") print(f"Variable arguments (*args): {args}") print(f"Keyword arguments (**kwargs): {kwargs}") example_function(1, 2, 3, 4, key1="value1", key2="value2") ``` 运行结果将是: ``` Fixed parameters: a=1, b=2 Variable arguments (*args): (3, 4) Keyword arguments (**kwargs): {'key1': 'value1', 'key2': 'value2'} ``` 此外,如果可变参数不是最后的一个参数,则后续的普通参数必须以关键字形式传递[^2]。 --- #### Go 语言中的可变长参函数 Go 语言也支持可变长参函数,其语法类似于数组切片。 1. **定义方式** 在形参前加上省略号 `...` 即表示该参数为可变长参数。 2. **内部原理** 实际上传入的是一个切片(slice),因此可以直接对其进行迭代或其他切片操作。 3. **优点对比** 相较于手动创建切片再传入的方式,使用可变参数可以减少不必要的内存分配,从而提升性能和可读性[^4]。 4. **示例代码** ```go package main import ( "fmt" ) func sum(nums ...int) int { total := 0 for _, num := range nums { total += num } return total } func main() { fmt.Println(sum(1, 2, 3, 4)) // 输出: 10 } ``` --- #### 总结 不同编程语言对于可变长参的支持各有特点。C/C++ 的实现较为底层,适合高性能场景;Python 则提供了灵活且易于理解的接口设计;而 Go 语言则兼顾了简洁性和效率。无论哪种语言,合理利用可变长参都能让代码更加紧凑高效[^3][^5]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值