在 C/C++ 中,可变参数列表以省略号运算符 ... 表示。它具有许多用途,具体取决于具体的使用场景。 最初在 C 中用作可变函数参数列表的抽象声明符;在 C++ 中,可用于异常处理 catch 块中;而在 C++11 中,则用于可变参数模板。
printf("Hello, World! \n");
说起可变参数,printf() 可以说是最经典的应用之一。
int printf(
const char *format [,
argument]...
);
从接触 C/C++ 开始,‘printf()’ 们可谓如影随形。在实际的应用中,界面数据的格式化输出、日志的格式化输出,这些都和可变参数密不可分。
可变参数列表的格式化,主要有两种方式: 1. 可变参数列表va_list和vsprintf 2. 可变参数模板和sprintf (C++11)
接下来,以 format_string() 函数为例进行说明。以下是主体部分:
#include <iostream>
#include <stdio.h>
#include <stdarg.h>
void format_string(const char* format, ...)
{
}
int main()
{
format_string("%s, %s", "hello", "this is a string.");
return 0;
}
1. 可变参数列表va_list和vsprintf
1.1 va_start、va_end、vsprintf 的使用
#define MAX_BUFFER 128
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);
va_end(args);
puts(buffer);
}
输出:hello, this is a string.(加上结束符’\0’,共25个字符)
以上便是基于 va_list 的可变参数格式化实现。args 是指向参数列表的指针,通过 va_start 将 args 设置为传递给函数的参数列表中的第一个可选参数,然后使用 vsprintf 进行参数检索和格式化输出。 有没有很眼熟?vsprintf 比我们常用的 sprintf 前面多一个v,表示是用于可变参数列表(variable-argument list) 的。它们的区别在于,sprintf 的入参是可变参数列表(省略号),而 vsprintf 的入参是可变参数列表指针。
1.2 “warning C4996”的前因后果
在编译过程中,会有这样一个警告:warning C4996: 'vsprintf': This function or variable may be unsafe. Consider using vsprintf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. 也可能是 error C4996, 这取决于VS项目属性配置中 “C/C++” 下是否开启了SDL检查。
通常情况下,我们会选择忽略或屏蔽这个警告。一般情况下是可以这样操作,但是会存在潜在问题,后面会进行说明。出现C4996 的提示,是由于微软为了增强安全机制,弃用了部分C运行时库(CRT)函数,以_s 结尾的同名函数替代。微软认为,这些函数是安全错误的常见来源,因为它们不会阻止覆盖内存的操作。这个怎么理解呢?接下来看看错误是如何出现的。
首先,将输出缓冲区 MAX_BUFFER 大小调整为 20,看看会发生什么。
#define MAX_BUFFER 20
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
vsprintf(buffer, format, args);
va_end(args);
puts(buffer);
}
输出:hello, this is a string.
但是此时出现了异常提示:Run-Time Check Failure #2 - Stack around the variable 'buffer' was corrupted. 表示堆栈缓冲区溢出。看看 vsprintf 前后 buffer 的内存变化。
格式化前:  格式化后:  能看到内存溢出了,一共写了25个字符 (超出5个),其中第25位是字符串结束符 ‘\0’,所以输出时以结束符为准,将字符串完整输出。
1.3 vsprintf_s并不安全
现在,根据 warning C4996 的提示,使用安全版本的vsprintf_s 。
int vsprintf_s(
char *buffer,
size_t numberOfElements,
const char *format,
va_list argptr
);
第二个参数 numberOfElements 表示目标缓冲区的大小(以字符为限)。代码如下:
#define MAX_BUFFER 128
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
vsprintf_s(buffer, MAX_BUFFER, format, args);
va_end(args);
puts(buffer);
}
输出:hello, this is a string.
现在VS安静了,编译时也没有提示C4996。在缓冲区足够的情况下,执行正常。现在,同样把缓冲区大小改为20,看看会发生什么。
#define MAX_BUFFER 20
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
vsprintf_s(buffer, MAX_BUFFER, format, args);
va_end(args);
puts(buffer);
}
编译运行,引发异常中断的弹窗提示: Debug Assertion Failed! Expression:("Buffer too small", 0)
有点意料之外,没有按实际大小接收数据。虽然确保了不会缓冲区溢出,但是直接异常中断,而前面内存溢出至少还能有个输出结果。那么,如果缓冲区大小是足够的,但指定的大小不够会怎样?
#define MAX_BUFFER 128
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
vsprintf_s(buffer, 20, format, args);
va_end(args);
puts(buffer);
}
同样的,也引发了异常中断。所以,通过指定目标缓冲区大小,vsprintf_s 确实可以保证不会溢出,但是会中断啊。再换个角度,如果设置的缓冲区大小比实际大,又会怎么样?
#define MAX_BUFFER 20
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
vsprintf_s(buffer, 40, format, args);
va_end(args);
puts(buffer);
}
此时出现了缓冲区溢出提示:Run-Time Check Failure #2 - Stack around the variable 'buffer' was corrupted. 看看 vsprintf_s 前后 buffer 的内存变化。
格式化前:  格式化后:  指定大小的缓冲区在接收完格式化的内容后,剩余的空间置为0xfe 。
所以,安全版本的 vsprintf_s 不一定就“安全”。需要满足的规则:目标缓冲区实际大小 >= 参数指定的缓冲区大小 >= 待接收数据大小+1
另外,根据微软的描述,vsprintf_s与不安全版本的差异仅在于安全版本支持位置参数。所谓位置参数,就是在格式说明中使用 %n$ 可将位置参数指定为格式化,其中 n 是参数列表中要格式化的参数位置,第一个参数的参数位置从 1 开始。举个例子:
_printf_p("%1\$d %2\$d %1\$d", 1, 2);
_printf_p("%3\$d %2\$d %1\$d \n", 1, 2, 3);
1.4 vsprintf_s的安全做法
既然使用 vsprintf_s 时需要足够的缓冲区,那么需要先获取格式化后数据的大小。这里可以使用_vscprintf 。
int _vscprintf(
const char *format,
va_list argptr
);
_vscprintf 返回使用可变参数列表指针进行格式化将生成的字符数,但不包括终止字符’\0’,所以实际需要的大小要在此基础上+1。
void format_string(const char* format, ...)
{
va_list args;
va_start(args, format);
int len = _vscprintf(format, args) + 1;
char* buffer = new char[len];
if (buffer)
{
vsprintf_s(buffer, len, format, args);
puts(buffer);
delete[] buffer;
buffer = NULL;
}
va_end(args);
}
这里使用了动态内存分配,优点是能保证接收格式化后的全部数据,但也有缺点,两次格式化(_vscprintf 获取大小、vsprintf_s 获取数据)、以及 new 和 delete 带来的系统开销,比一次格式化至少翻倍增加。
那么,只进行一次格式化,又要保证当缓冲区不够时能够进行自动截断而不会出现异常。又该如何处理?答案是可以使用_vsnprintf_s 。
1.5 _vsnprintf_s的使用
int _vsnprintf_s(
char *buffer,
size_t sizeOfBuffer,
size_t count,
const char *format,
va_list argptr
);
其中,sizeOfBuffer 是目标缓冲区的大小,count 则指定要写入的最大字符数 (不包括结束符’\0’) 或_TRUNCATE 。如果指定为_TRUNCATE ,并且源数据的字符数等于或超过 sizeOfBuffer ,则在写入的字符数量将超过 buffer 时,写入结束符’\0’。
这里将缓冲区大小设为20,看看结果会怎样。
#define MAX_BUFFER 20
void format_string(const char* format, ...)
{
char buffer[MAX_BUFFER] = { 0 };
va_list args;
va_start(args, format);
_vsnprintf_s(buffer, MAX_BUFFER, _TRUNCATE, format, args);
va_end(args);
puts(buffer);
}
输出为:hello, this is a st (一共输出19个字符,第20个为'\0')
虽然缓冲区大小只有20,但是指定了标识_TRUNCATE ,格式化时会根据指定的 sizeOfBuffer 进行自动-1截断。
2. 可变参数模板和sprintf (C++11)
在C++11中,提供了对可变参数模板的支持。可变参数模板,是支持任意数量的参数的类或函数模板。可变参数模板通过两种方式使用省略号。 ... 在参数名称的左侧,表示参数包 ,在参数名称的右侧,将参数包扩展为多个单独的名称。
下面是 可变参数模板类 定义语法的基本示例:
template<typename... Arguments> class classname;
对于参数包和扩展,可根据自己的偏好在省略号周围添加空白,可以这样:
template<typename ...Arguments> class classname;
或者这样:
template<typename ... Arguments> class classname;
使用 可变参数模板 时,可以直接使用sprintf 接收参数。
int sprintf(
char *buffer,
const char *format [,
argument] ...
);
与vsprintf 相同,基于安全考量,这里不再使用 sprintf 进行格式化,而使用更安全的_snprintf_s 替代。相关代码如下:
#define MAX_BUFFER 20
template<class ...T>
void format_string(const char* format, T&... args)
{
char buffer[MAX_BUFFER] = { 0 };
_snprintf_s(buffer, MAX_BUFFER, _TRUNCATE, format, args...);
puts(buffer);
}
输出为:hello, this is a st
若使用动态内存分配,相对应的可使用_scprintf 来获取格式化字符串中的字符数。
int _scprintf(
const char *format [,
argument] ...
);
代码如下:
template<class ...T>
void format_string(const char* format, const T&... args)
{
int len = _scprintf(format, args...) + 1;
char* buffer = new char[len];
if (buffer)
{
_snprintf_s(buffer, len, _TRUNCATE, format, args...);
puts(buffer);
delete[] buffer;
buffer = NULL;
}
}
输出为:hello, this is a string.
总结: 1、如果环境支持C++11,更推荐使用可变参数模板,使用起来更加灵活方便。 2、关于缓冲区大小,推荐固定大小,而不是使用动态内存分配。一般来说2048字节很充足了,不够可以按需调整,除非缓冲区大小不便估计或者有其它特别要求。从性能方面考虑,使用动态内存分配时,需要进行两次字符串格式化(一次获取大小、一次获取数据)、new和delete,耗时会增加1倍以上。
|