格式化输出

格式化数据写入字符串缓冲区
str :指向目标字符串缓冲区的指针 size :缓冲区大小
format :格式化字符串 ...:可变参数列表
cpp
//自动截断并添加'\0'
int snprintf(char *str, size_t size, const char *format, ...);
动态分配缓冲区
cpp
/*如果成功,vsnprintf 返回假设缓冲区无限大时,本应写入的字符数(不包括结尾的空字符 \0)。这个返回值非常有用!
如果编码错误(例如格式字符串无效),则返回一个负数。*/
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
/*返回值 n 代表了格式化后字符串的真正长度。
如果 n < size:说明字符串被完整地写入了缓冲区。
如果 n >= size:说明缓冲区空间不足,输出被截断了。缓冲区 str 中包含了 size - 1 个字符,并以 \0 结尾。*/
char *str :指向一个字符数组的指针,用于存储格式化后的字符串。也就是结果存放的缓冲区。
size_t size :缓冲区 str 的总大小(以字节为单位)。这是该函数安全性的关键,它确保写入的字符数不会超过这个大小(包括结尾的空字符 \0)。
const char *format :格式化字符串,与 printf、sprintf 的用法完全相同。它指定了如何格式化后续的参数(例如 %d, %s, %.2f)。
va_list ap:这是一个已经初始化过的可变参数列表变量。它"承载"了要传递给 format 的实际参数。
核心用途与用法
vsnprintf 的核心用途是:安全地格式化字符串,并且它通常用于编写你自己的可变参数函数(即包装类函数)。
最常见的场景是你想创建一个类似 printf 的函数,比如用于日志记录、错误输出等。
为什么不用 snprintf?
你可以直接在你的可变参数函数里调用 snprintf。但 vsnprintf 的存在允许你将可变参数逻辑(va_start, va_end)与格式化逻辑分离,使代码更清晰。更重要的是,如果你的自定义函数需要多次使用这些可变参数(例如,先计算长度,再分配内存,最后写入),va_list 可以传递,而 ... 形式的参数不能直接重复使用。
cpp
#include <stdio.h>
#include <stdlib.h>
int create_message(char **msg, const char *format, ...) {
va_list args;
// 第一次调用获取所需长度
va_start(args, format);
int len = vsnprintf(NULL, 0, format, args);
va_end(args);
if (len < 0) return -1;
// 分配足够空间
*msg = malloc(len + 1);
if (!*msg) return -1;
// 第二次调用实际写入
va_start(args, format);
vsnprintf(*msg, len + 1, format, args);
va_end(args);
return 0;
}
优先使用 snprintf,因为它更安全
• sprintf 只应在确定不会发生缓冲区溢出时使用
• 始终检查 snprintf 的返回值来处理可能的截断
• 在需要精确控制内存使用时,考虑使用两阶段方法先计算长度再分配
假设要编写一个自定义的日志函数 my_log,它会在消息前加上 "[INFO]" 前缀,然后将格式化后的内容输出到标准错误(stderr)
cpp
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h> // 为了 malloc 和 free
// 自定义日志函数
void my_log(const char *format, ...) {
va_list args;
int needed_size;
char* buffer;
// 1. 初始化 args,指向第一个可变参数
va_start(args, format);
// 2. 试探性地获取所需缓冲区大小(不写入任何内容)
needed_size = vsnprintf(NULL, 0, format, args) + 1; // +1 是为了容纳结尾的 \0
// 注意:C99标准规定,传入size=0时,返回值仍为所需长度(不包括\0),行为与传入NULL一致。
// 3. 根据大小分配内存
buffer = (char *)malloc(needed_size * sizeof(char));
if (buffer == NULL) {
va_end(args);
return; // 内存分配失败
}
// 4. 再次调用 vsnprintf,真正将内容写入缓冲区
// 因为 needed_size 已经包含了 \0 的位置,所以绝对不会溢出
vsnprintf(buffer, needed_size, format, args);
// 5. 清理可变参数列表
va_end(args);
// 6. 使用格式化后的字符串
fprintf(stderr, "[INFO] %s\n", buffer);
// 7. 释放动态分配的内存
free(buffer);
}
int main() {
int count = 5;
float value = 3.14f;
char name[] = "World";
// 调用自定义的可变参数日志函数
my_log("Hello, %s! You have %d items worth $%.2f.", name, count, value);
// 输出:[INFO] Hello, World! You have 5 items worth $3.14.
return 0;
}
安全性:vsnprintf 是 sprintf 的安全替代品,因为它通过 size 参数防止了缓冲区溢出攻击和错误。应始终优先使用 snprintf 和 vsnprintf,而不是 sprintf 和 vsprintf。
参数顺序:确保在调用 vsnprintf 之前正确初始化 va_list args(使用 va_start),并在使用后清理(使用 va_end)。
截断处理:如果你的缓冲区大小是固定的(比如栈上的数组),一定要检查返回值以判断是否发生了截断,并根据需要处理(例如,报告错误或警告)。
总结来说,vsnprintf 是一个用于安全地、可重入地处理可变参数格式化的核心函数,它是编写高级、安全且功能强大的自定义打印函数的基础工具。
一个非常固定的流程来使用:
-
声明一个 va_list 类型的变量。
-
初始化这个变量,使其指向可变参数列表的第一个参数。这是 va_start 的工作。
-
使用这个变量,通过 va_arg 宏(你问题中没提到,但至关重要)一个一个地取出参数。
-
清理这个变量。这是 va_end 的工作。
你可以把这个过程类比为操作一个迭代器或指针:
• va_list 是指针本身。
• va_start 将指针指向链表(参数列表)的开头。
• va_arg 获取当前指针指向的值,并将指针移动到下一个位置。
• va_end 将指针置空(或进行必要的清理),表示操作完成。
顺序性:参数必须按照你期望的顺序取出。你不能跳过前面的参数直接取后面的,也不能往回取。
类型必须匹配:使用 va_arg 时,你指定的类型必须与函数调用时实际传入的参数类型完全一致。这是程序员的责任,编译器无法帮你检查。如果传入了 double 但你用 va_arg(args, int) 去取,结果将是错误的。
必须成对调用:每个 va_start 都必须有一个对应的 va_end。这在有多个返回分支的函数中要特别注意,确保在所有退出路径上都调用了 va_end。
无法直接获取参数数量:从 ... 本身是无法知道传入了多少个参数的。你必须通过其他方式告知函数
格式化输入
sscanf 是 C/C++ 中强大的字符串解析工具,通过格式字符串灵活控制输入转换。合理使用宽度限制、返回值检查和扫描集,可以安全高效地从字符串中提取结构化数据
修饰符:
宽度:指定最大读取字符数,如 %10s 最多读取 10 个字符(含结束符 \0)。
赋值抑制:* 表示跳过匹配的数据,不存储,如 %*d 读取整数但不保存。
长度修饰:h(short)、l(long/long double)、ll(long long)、L(long double)
cpp
int sscanf(const char *str, const char *format, ...);
str:指向要解析的输入字符串。
format:格式控制字符串,指定了如何解析输入。
...:可变参数列表,为每个格式说明符提供对应的变量地址(指针),用于存储解析后的数据。
返回值:成功匹配并赋值的输入项个数;如果在成功读取任何数据之前输入失败,则返回 EOF。
cpp
#include <stdio.h>
int main() {
const char *data = "2024-05-20";
int year, month, day;
// 解析日期格式,忽略分隔符 '-'
sscanf(data, "%d-%d-%d", &year, &month, &day);
printf("Year: %d, Month: %d, Day: %d\n", year, month, day);
const char *log = "Error: code 1234 at line 56";
int code, line;
// 使用 %*s 跳过 "Error:" 标记
sscanf(log, "%*s %*s %d %*s %*s %d", &code, &line);
printf("Code: %d, Line: %d\n", code, line);
return 0;
}
C++ 提供了 <sstream> 库中的 std::istringstream,可以类似地实现字符串解析
cpp
#include <sstream>
#include <string>
std::string data = "John 25 180.5";
std::string name;
int age;
double height;
std::istringstream iss(data);
iss >> name >> age >> height;
流方式类型安全,但可能比 sscanf 略慢
std::istringstream 结合 >> 是处理简单空白分隔数据的利器,但对于其他分隔符,需要借助额外工具灵活处理。std::getline 可以从输入流中读取直到遇到指定的分隔符(默认为换行符,但可以自定义),然后对每个字段单独处理。