C 工具
变长参数列表
这部分解释了旧的 C 风格变长参数列表。了解这些内容很重要,因为你可能会在遗留代码中遇到它们。然而,在新代码中,你应该使用变参模板来实现类型安全的变长参数列表。
考虑 C 函数 printf(),来自 <cstdio>。你可以用任意数量的参数调用它:
printf("int %d\n", 5);
printf("String %s and int %d\n", "hello", 5);
printf("Many ints: %d, %d, %d, %d, %d\n", 1, 2, 3, 4, 5);
C/C++ 提供了语法和一些实用宏,用于编写你自己的变长参数函数。这些函数通常看起来很像 printf()。尽管你不经常需要这个特性,但偶尔你会遇到它相当有用的情况。例如,假设你想编写一个快速而简单的调试函数,当设置了调试标志时,该函数将字符串打印到 stderr,但如果没有设置调试标志,则不执行任何操作。就像 printf() 一样,这个函数应该能够打印具有任意数量和任意类型参数的字符串。一个简单的实现如下:
#include <cstdio>
#include <cstdarg>
bool debug { false };
void debugOut(const char* str, ...) {
va_list ap;
if (debug) {
va_start(ap, str);
vfprintf(stderr, str, ap);
va_end(ap);
}
}
首先,请注意 debugOut() 的原型包含一个类型化且命名的参数 str,后面跟着 ...(省略号)。它们代表任意数量和类型的参数。要访问这些参数,你必须使用 <cstdarg> 中定义的宏。你声明一个 va_list 类型的变量,并用 va_start 调用进行初始化。va_start() 的第二个参数必须是参数列表中最右边的命名变量。所有具有变长参数列表的函数都至少需要一个命名参数。debugOut() 函数简单地将这个列表传递给 vfprintf()(<cstdio> 中的标准函数)。vfprintf() 调用返回后,debugOut() 调用 va_end() 来终止访问变量参数列表。在调用 va_start() 后,你必须始终调用 va_end(),以确保函数以一致的堆栈状态结束。你可以如下方式使用该函数:
debug = true;
debugOut("int %d\n", 5);
debugOut("String %s and int %d\n", "hello", 5);
debugOut("Many ints: %d, %d, %d, %d, %d\n", 1, 2, 3, 4, 5);
访问参数
如果你想自己访问实际参数,你可以使用 va_arg() 来做到这一点。它接受 va_list 作为第一个参数,以及要解释的参数的类型。不幸的是,除非你提供明确的方式,否则无法知道参数列表的结尾。例如,你可以使第一个参数是参数数量的计数。或者,在你有一组指针的情况下,你可能需要最后一个指针是 nullptr。有许多方法,但它们都对程序员来说是繁琐的。
下面的示例演示了调用者在第一个命名参数中指定提供了多少个参数的技术。该函数接受任意数量的 int 并打印出来:
void printInts(size_t num, ...) {
va_list ap;
va_start(ap, num);
for (size_t i { 0 }; i < num; ++i) {
int temp { va_arg(ap, int) };
cout << temp << " ";
}
va_end(ap);
cout << endl;
}
你可以按以下方式调用 printInts()。请注意,第一个参数指定将跟随多少个整数。
printInts(5, 5, 4, 3, 2, 1);
为什么不应使用 C 风格的变长参数列表
访问风险
使用 C 风格的变长参数列表访问参数并不安全。这种方法存在几个风险,从 printInts() 函数可以看出:
- 不知道参数的数量:在 printInts() 的情况下,你必须信任调用者作为第一个参数传递正确数量的参数。在 debugOut() 的情况下,你必须信任调用者在字符数组后传递的参数数量与字符数组中的格式化代码数量相同。
- 不知道参数的类型:va_arg() 接受一个类型,用它来解释其当前位置的值。然而,你可以告诉 va_arg() 将值解释为任何类型。它无法验证正确的类型。
警告:避免使用 C 风格的变长参数列表。建议传递一个 std::array 或 vector 的值、使用初始化列表,或者使用类型安全的变参模板来实现变长参数列表。