序言
不知道大家有没有细细研究过在 C 语言 中的 printf 函数,也许我们经常使用他,但是我们可能并不是那么了解他。先看一下调用格式:int printf ( const char * format, ... );
,在这里的 format
代表我们的输出格式,后面的 ...
省略号这又是什么呢,这代表 可变参数
,你可以传递任意数量的参数。这是怎么实现的呢?
1. C 中的可变参数
1.1 可变参数的概念
可变参数是指在函数定义中,允许传入不定数量的参数的一种机制
。在编程语言中,可变参数使得函数能够更加灵活地处理不同数量的输入。
1.2 实现可变参数
在实现可变参数的函数之前,我们先认识几个函数:
stdarg.h
头文件
为了处理可变参数,C 语言 标准库提供了 stdarg.h
头文件,它定义了一组宏来访问这些参数。这些宏包括:
va_list
:一个类型,用于声明一个变量,该变量可以用来遍历函数的参数列表。va_start(ap, last_arg)
:初始化va_list
变量ap
,last_arg
是最后一个固定参数的名字,ap
将用来遍历所有后续的可变参数。va_arg(ap, type)
:返回ap
指向的下一个参数,并将ap
更新为指向下一个参数的指针。type
参数指定了期望的参数类型。va_end(ap)
:清理va_list
变量ap
,结束对可变参数列表的遍历。
现在我们实现一个简单的打印数字的可变参数函数:
void PrintNums(int cnt, ...)
{
va_list ap;
// 初始化
va_start(ap, cnt);
// 遍历
for (int i = 0; i < cnt; i++)
{
// 遍历参数包中的所有参数
int num = va_arg(ap, int);
std::cout << num << ' ';
}
std::cout << std::endl;
// 释放
va_end(ap);
}
int main()
{
// 第一个参数代表可变参数的个数
PrintNums(5, 1, 2, 3, 4, 5);
return 0;
}
最后的输出结果也和我们的预期一致:
1 2 3 4 5
1.3 可变参数的原理
首先我们传递我们的参数时,是 从右到左依次入栈
,如下图:
注意:在这里不要被图像误导,参数占的空间其实很小,只是为了美观画的大一点
通过查看 va_list
的定义 — typedef char* va_list;
,我们发现其实他就是一个 char*
指针。现在该指针需要指向可变参数的起始部分,所以我们需要传递 cnt
过去,对该指针进行初始化后自然就指向了可变参数的起始地址:
之后我们取数据的时候告诉指针,这是一个 int
类型,你一次性要取 4 / 8 个字节
才是完整的数据。之后该指针一直重复取数据的参数,直至遇到结束条件(在这里是 i == cnt
)。
原理似乎也没有那么复杂,但是当可变参数遇到模板时…
2. C++ 中的可变参数模板
2.1 可变参数模板
在 C++ 中,可变参数模板允许你定义可以 接受任意数量模板参数的模板函数或模板类
。这是通过使用模板参数包(template parameter pack
)和函数参数包(function parameter pack
)来实现的。
2.2 实现可变参数模板
可变参数模板实际使用起来是比较别扭的,比如这里我就简单实现一个打印多个类型的函数:
void MyPrint()
{
std::cout << std::endl;
}
template <class T, class... Args>
void _MyPrint(const T &val, Args... args)
{
std::cout << val << ' ';
_MyPrint(args...);
}
template <class... Args>
void MyPrint(Args... args)
{
_MyPrint(args...);
}
int main()
{
MyPrint(1, 1.2, 'A', "ABCD");
return 0;
}
输出结果也没有任何的问题:
1 1.2 A ABCD
这个方案的逻辑是递归的去解析参数包,每次取出一个参数直至参数取为空。
接下来还有一个方案,实现的方式稍微简单一些:
template <class T>
int _MyPrint(const T& val)
{
std::cout << val << " ";
return 0;
}
template <class... Args>
void MyPrint(Args... args)
{
int arr[] = {_MyPrint(args)...};
std::cout << std::endl;
}
int main()
{
MyPrint(1, 1.2, 'A', "ABCD");
return 0;
}
当然结果肯定和方案一是一致的,但是我们又该怎么理解呢:
- 当我们编译程序时需要为这个数组申请指定大小的空间
- 但是怎么获取数组中有多少元素呢
- 数组中的函数执行一次就有一个返回值,所以执行多少次就有多少元素
- 那么函数具体执行多少次呢
- 该函数需要一个参数,所以参数包里的参数数量决定执行次数
- 执行该函数时我们就会处理一个参数直至参数被使用完
但是很少有让我们实现可变参数模板的场景,大家当作了解一下。
3. 可变参数模板的应用
就拿容器 vector
举例子,它常使用 insert,push_back
这两个方法添加元素,为了提高效率通过可变参数模板,新的插入元素的方法 emplace, emplace_back
由此而生。
他的怎么高效的呢?举个栗子来比较一下:
// 方式一
std::string s = "ABC";
vec.push_back(s);
// 方式二
vec.push_back(std::string("ABC"));
// 方式三
vec.emplace_back("ABC");
我们在这里来比较三者的效率:
- 方案一:构造函数 + 拷贝构造
- 方案二:构造函数 + 移动构造
- 方案三:构造函数
emplace_back直接在容器内构造对象可以避免多余的复制或移动操作,提高性能。
并且 emplace_back
是兼容 push_back
的使用的,所以在使用时大家尽量使用前者。
4. 总结
在这篇文中我们首先介绍了在 C 语言 中的可变参数,之后简单讲解了 C++ 中的可变参数模板的使用以及应用。