IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++可变参数的格式化 -> 正文阅读

[C++知识库]C++可变参数的格式化

在 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, ...)
{
    // TODO:...
}

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); // warning C4996: 'vsprintf': This function or variable may be unsafe. Consider using vsprintf_s instead.
    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);		 // 输出:1 2 1
_printf_p("%3\$d %2\$d %1\$d \n", 1, 2, 3);	 // 输出:3 2 1

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; // _vscprintf doesn't count terminating '\0'
    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; // _scprintf doesn't count terminating '\0'
    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倍以上。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-07-25 11:28:13  更:2021-07-25 11:29:50 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/3 4:36:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码