C语言中的printf、sprintf、snprintf、vsnprintf 函数

目录

[1.1 概述](#1.1 概述)

[1.2 函数原型](#1.2 函数原型)

[1.3 返回值](#1.3 返回值)

[1.4 示例](#1.4 示例)

[1.5 输出结果](#1.5 输出结果)

[1.6 常用格式说明符](#1.6 常用格式说明符)

[1.7 注意事项](#1.7 注意事项)

[2. snprintf 函数](#2. snprintf 函数)

[2.1 概述](#2.1 概述)

[2.2 函数原型](#2.2 函数原型)

[2.3 返回值](#2.3 返回值)

[2.4 示例](#2.4 示例)

[2.5 输出结果](#2.5 输出结果)

[2.6 使用场景](#2.6 使用场景)

[2.7 注意事项](#2.7 注意事项)

[3. vsnprintf 函数](#3. vsnprintf 函数)

[3.1 概述](#3.1 概述)

[3.2 函数原型](#3.2 函数原型)

[3.3 返回值](#3.3 返回值)

[3.4 使用场景](#3.4 使用场景)

[3.5 示例](#3.5 示例)

[3.6 输出结果](#3.6 输出结果)

[3.7 注意事项](#3.7 注意事项)

[4. 如何选择使用哪一个函数](#4. 如何选择使用哪一个函数)

[4.1 简单总结](#4.1 简单总结)

[5. 实际应用示例:构建并发送 AT 指令](#5. 实际应用示例:构建并发送 AT 指令)

[5.1 使用 snprintf 构建指令](#5.1 使用 snprintf 构建指令)

[5.2 使用 vsnprintf 在可变参数函数中构建指令](#5.2 使用 vsnprintf 在可变参数函数中构建指令)

[5.3 输出结果](#5.3 输出结果)

[5.4 解释](#5.4 解释)

[6. 总结与实践](#6. 总结与实践)


理解 printf、sprintf、snprintfvsnprintf 这几个函数对于有效地处理字符串输出和格式化非常重要,sprintf安全性低,容易发生溢出风险,具有安全隐患,所以这个函数学习意义不是很大,了解即可,接下来介绍其他三个函数的用途、区别以及如何在实际编程中使用,包括代码示例和解释。


  1. printf 函数

1.1 概述

printf 是 C 语言中最常用的输出函数,用于将格式化的数据输出到标准输出(通常是终端或控制台)。

1.2 函数原型

#include <stdio.h>

int printf(const char *format, ...);
  • format :一个格式字符串,指定了输出的格式和内容。
  • ... :可变参数,根据格式字符串中的占位符提供相应的值。

1.3 返回值

printf 返回成功输出的字符数。如果发生错误,则返回一个负值。

1.4 示例

#include <stdio.h>

int main(void) {
    int num = 42;
    double pi = 3.14159;
    char *str = "Hello, World!";
    
    // 简单输出
    printf("整数: %d\n", num);
    printf("浮点数: %.2f\n", pi); // 保留两位小数
    printf("字符串: %s\n", str);
    
    // 多个参数
    printf("组合: 整数=%d, 浮点数=%.3f, 字符串=%s\n", num, pi, str);
    
    return 0;
}

1.5 输出结果

整数: 42
浮点数: 3.14
字符串: Hello, World!
组合: 整数=42, 浮点数=3.142, 字符串=Hello, World!

1.6 常用格式说明符

格式说明符 描述
%d 有符号十进制整数
%u 无符号十进制整数
%f 浮点数
%.2f 浮点数,保留两位小数
%s 字符串
%c 单个字符
%x 无符号十六进制整数(小写)
%X 无符号十六进制整数(大写)
%% 输出一个 % 字符

1.7 注意事项

  • 缓冲区溢出 :使用 printf 时,需要确保提供的参数类型与格式说明符匹配,否则可能导致未定义行为。
  • 安全性:避免将用户输入直接作为格式字符串,防止格式化字符串漏洞。

2. snprintf 函数

2.1 概述

snprintf 类似于 printf,但它将格式化后的输出存储在一个字符数组(缓冲区)中,而不是输出到标准输出。它还允许指定要写入缓冲区的最大字符数,从而防止缓冲区溢出。

2.2 函数原型

#include <stdio.h>

int snprintf(char *str, size_t size, const char *format, ...);
  • str:指向字符数组的指针,用于存储格式化后的字符串。
  • sizestr 的大小(以字节为单位),包括终止符 '\0'
  • format:格式字符串。
  • ...:可变参数。

2.3 返回值

  • 成功时snprintf 返回 将要写入的字符总数 (不包括终止符 '\0')。
  • 如果返回值 >= size ,表示输出被 截断,即目标缓冲区不足以容纳完整的格式化字符串。
  • 出错时,返回一个负值。

2.4 示例

#include <stdio.h>

int main(void) {
    char buffer[50];
    int age = 30;
    double salary = 75000.50;
    
    // 使用 snprintf 格式化字符串
    int written = snprintf(buffer, sizeof(buffer), "年龄: %d, 工资: %.2f", age, salary);
    
    if (written < 0) {
        printf("格式化字符串时出错。\n");
    } else if ((size_t)written >= sizeof(buffer)) {
        printf("输出被截断。\n");
    } else {
        printf("缓冲区内容: %s\n", buffer);
    }
    
    return 0;
}

2.5 输出结果

缓冲区内容: 年龄: 30, 工资: 75000.50

2.6 使用场景

  • 构建字符串:在需要将多个变量组合成一个字符串时非常有用。
  • 避免缓冲区溢出:通过指定缓冲区大小,防止写入超过数组边界的数据。

2.7 注意事项

  • 终止符snprintf 会自动在字符串末尾添加 '\0',前提是 size 大于 0。
  • 返回值检查:始终检查返回值,以确定输出是否被截断。
  • 缓冲区大小:确保缓冲区足够大以存储预期的字符串,尤其是在拼接多个变量时。

3. vsnprintf 函数

3.1 概述

vsnprintfsnprintf 的变体,用于处理可变参数列表(va_list)。它通常与可变参数函数(如自定义的打印函数)一起使用。

3.2 函数原型

#include <stdio.h>
#include <stdarg.h>

int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • str:指向字符数组的指针,用于存储格式化后的字符串。
  • sizestr 的大小(以字节为单位),包括终止符 '\0'
  • format:格式字符串。
  • ap :类型为 va_list,表示可变参数列表。

3.3 返回值

snprintf 相同,vsnprintf 返回将要写入的字符总数(不包括终止符 '\0')。如果返回值大于或等于 size,则表示输出被截断。

3.4 使用场景

vsnprintf 主要用于:

  • 自定义可变参数函数:例如,编写自己的日志函数或格式化函数。
  • 处理 va_list :当可变参数已被封装在 va_list 中时。

3.5 示例

假设我们要编写一个自定义的打印函数 my_printf,它接受一个格式字符串和可变参数,并将格式化后的字符串存储在一个缓冲区中。

#include <stdio.h>
#include <stdarg.h>

void my_printf(char *buffer, size_t size, const char *format, ...) {
    va_list args;
    va_start(args, format);
    
    vsnprintf(buffer, size, format, args);
    
    va_end(args);
}

int main(void) {
    char buffer[100];
    int id = 101;
    char name[] = "Alice";
    double score = 95.75;
    
    my_printf(buffer, sizeof(buffer), "学生ID: %d, 姓名: %s, 成绩: %.1f", id, name, score);
    
    printf("%s\n", buffer); // 输出: 学生ID: 101, 姓名: Alice, 成绩: 95.8
    
    return 0;
}

3.6 输出结果

学生ID: 101, 姓名: Alice, 成绩: 95.8

3.7 注意事项

  • 初始化和清理 :始终在使用 va_list 前调用 va_start,使用后调用 va_end
  • 参数顺序va_start 的第二个参数应为最后一个固定参数。
  • 类型匹配:确保传入的参数类型与格式说明符匹配,否则会导致未定义行为。

4. 如何选择使用哪一个函数

函数 适用场景 备注
printf 直接将格式化字符串输出到标准输出(如终端、控制台)。 用于调试、日志输出等。
snprintf 将格式化字符串输出到字符数组,避免缓冲区溢出。 用于构建字符串供后续使用(如发送到设备)。
vsnprintf 在自定义可变参数函数中,将 va_list 格式化输出到字符数组。 用于高级用法,如编写自己的打印函数。

4.1 简单总结

  • 调试输出 :使用 printf,直接输出到控制台。
  • 构建字符串 :使用 snprintf,将格式化后的字符串存储在缓冲区中。
  • 可变参数处理 :在可变参数函数中使用 vsnprintf,处理 va_list

5. 实际应用示例:构建并发送 AT 指令

目前项目中使用到了AT指令,想通过将AT指令作为参数传入到API函数中,并且有的AT指令时带参数的,有的是不带参数的,这就导致API函数接收的参数个数是不确定的。

接下来构建一个 AT 指令并通过 API 函数发送,使用到了 snprintfvsnprintf 函数。

5.1 使用 snprintf 构建指令

函数 send_at_command,用于串口发送 AT 指令:

#include <stdio.h>
#include <string.h>

// 构建的发送函数
int send_at_command(const char *cmd);

// 使用 snprintf 构建并发送 AT 指令
int set_baud_rate(int baud_level) {
    char cmd[50];
    int written = snprintf(cmd, sizeof(cmd), "AT+IPR=%d\r\n", baud_level);
    
    if (written < 0 || written >= sizeof(cmd)) {
        // 处理错误:格式化失败或缓冲区不足
        return -1;
    }
    
    return send_at_command(cmd);
}

5.2 使用 vsnprintf 在可变参数函数中构建指令

构建更通用的函数 api_send_at_command_param,它可以接受格式化的指令:

#include <stdio.h>
#include <stdarg.h>

// 构建的发送函数
int send_at_command(const char *cmd);

// 定义 API 返回状态
typedef enum {
    API_OK = 0,
    API_ERROR,
    API_PARAM_ERROR
} API_Status;

// 使用 vsnprintf 在可变参数函数中构建并发送 AT 指令
API_Status api_send_at_command_param(const char *format, ...) {
    char cmd[128];
    va_list args;
    va_start(args, format);
    
    int written = vsnprintf(cmd, sizeof(cmd), format, args);
    va_end(args);
    
    if (written < 0 || written >= sizeof(cmd)) {
        // 处理错误:格式化失败或缓冲区不足
        return API_PARAM_ERROR;
    }
    
    if (send_at_command(cmd) != 0) {
        // 发送失败
        return API_ERROR;
    }
    
    return API_OK;
}

// 使用示例
int main(void) {
    // 设置波特率为 9
    if (api_send_at_command_param("AT+IPR=%d\r\n", 9) == API_OK) {
        printf("波特率设置成功。\n");
    } else {
        printf("波特率设置失败。\n");
    }
    
    // 发送数据
    if (api_send_at_command_param("AT+TXA=%u,%s\r\n", 12245, "Hello") == API_OK) {
        printf("数据发送成功。\n");
    } else {
        printf("数据发送失败。\n");
    }
    
    return 0;
}

5.3 输出结果

send_at_command 函数只是简单地通过串口打印发送的指令:

AT+IPR=9
波特率设置成功。
AT+TXA=12245,Hello
数据发送成功。

5.4 解释

  • snprintf :在 set_baud_rate 函数中,用于将整数 baud_level 插入到指令字符串中,构建完整的 AT 指令。
  • vsnprintf :在 api_send_at_command_param 函数中,用于处理可变参数,使函数能够接受任意数量和类型的参数,构建灵活的 AT 指令。

6. 总结与实践

  1. 选择合适的函数

    • 使用 printf 进行简单的调试输出。
    • 使用 snprintf 构建安全的、格式化的字符串,避免缓冲区溢出。
    • 在需要处理可变参数的自定义函数中使用 vsnprintf
  2. 始终检查返回值

    • snprintfvsnprintf 返回值可以帮助检测格式化过程中的错误或缓冲区溢出。
  3. 确保缓冲区足够大

    • 根据预期的最大输出长度选择合适的缓冲区大小,或动态分配缓冲区。
  4. 避免格式化字符串漏洞

    • 不要将未经验证的用户输入作为格式字符串传递给 printf 系列函数,防止潜在的安全漏洞。
  5. 使用类型匹配

    • 确保格式说明符与传入参数的类型匹配,避免未定义行为。
  6. 封装与复用

    • 封装常用的格式化操作到函数或宏中,提高代码复用性和可维护性。

通过理解和正确使用 printfsnprintfvsnprintf,可以更有效地处理 C 语言中的字符串输出和格式化任务,编写出更安全、可靠的代码。

相关推荐
w36250126623 分钟前
Java NIO
java·开发语言·nio
风间琉璃""23 分钟前
PWN的知识之栈溢出
数据结构·算法·网络安全·pwn·二进制安全·栈溢出
余识-25 分钟前
2.C语言基础:语句、表达式、注释与标准库简介
c语言
叫我阿呆就好了41 分钟前
C 实现植物大战僵尸(四)
c语言·开发语言
HUT_Tyne2651 小时前
力扣--494.目标和
数据结构·算法·leetcode
xianwu5432 小时前
cpp编译链接等
linux·开发语言·网络·c++·git
懒大王爱吃狼2 小时前
Python基于wordcloud库绘制词云图
开发语言·python·学习·python基础·python学习
i只喝怡宝2 小时前
基于辉芒51单片机的5档调光灯
c语言·单片机·嵌入式硬件·51单片机
wjs20242 小时前
Ruby 中文编码
开发语言
山语山2 小时前
C#多线程精解
开发语言·数据库·后端·c#