C/C++中的格式化输出与输入snprintf&sscanf

格式化输出

格式化数据写入字符串缓冲区

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 是一个用于安全地、可重入地处理可变参数格式化的核心函数,它是编写高级、安全且功能强大的自定义打印函数的基础工具。

一个非常固定的流程来使用:

  1. 声明一个 va_list 类型的变量。

  2. 初始化这个变量,使其指向可变参数列表的第一个参数。这是 va_start 的工作。

  3. 使用这个变量,通过 va_arg 宏(你问题中没提到,但至关重要)一个一个地取出参数。

  4. 清理这个变量。这是 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 可以从输入流中读取直到遇到指定的分隔符(默认为换行符,但可以自定义),然后对每个字段单独处理。

相关推荐
Je1lyfish2 小时前
CMU15-445 (2026 Spring) Project#1 - Buffer Pool Manager
linux·数据库·c++·后端·链表·课程设计·数据库架构
m0_531237172 小时前
C语言-初始化赋值,函数,变量的作用域与生命周期
c语言·开发语言
好好学习天天向上~~2 小时前
12_Linux学习总结_进程地址空间(虚拟地址)
linux·学习
m0_531237172 小时前
C语言-变量,枚举常量,字符串,打印类型,转义字符
c语言·数据结构·算法
zyeyeye2 小时前
自定义类型:结构体
c语言·开发语言·数据结构·c++·算法
red_redemption2 小时前
自由学习记录(119)
学习
俩娃妈教编程2 小时前
2023 年 03 月 二级真题(1)--画三角形
c++·算法·双层循环
invicinble2 小时前
关于学习技术栈的思考
java·开发语言·学习
BugShare2 小时前
飞牛NAS笔记本盒盖不休眠
linux