目录
[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、snprintf
和 vsnprintf
这几个函数对于有效地处理字符串输出和格式化非常重要,sprintf安全性低,容易发生溢出风险,具有安全隐患,所以这个函数学习意义不是很大,了解即可,接下来介绍其他三个函数的用途、区别以及如何在实际编程中使用,包括代码示例和解释。
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
:指向字符数组的指针,用于存储格式化后的字符串。size
:str
的大小(以字节为单位),包括终止符'\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 概述
vsnprintf
是 snprintf
的变体,用于处理可变参数列表(va_list
)。它通常与可变参数函数(如自定义的打印函数)一起使用。
3.2 函数原型
#include <stdio.h>
#include <stdarg.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
str
:指向字符数组的指针,用于存储格式化后的字符串。size
:str
的大小(以字节为单位),包括终止符'\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 函数发送,使用到了 snprintf
和 vsnprintf
函数。
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. 总结与实践
-
选择合适的函数:
- 使用
printf
进行简单的调试输出。 - 使用
snprintf
构建安全的、格式化的字符串,避免缓冲区溢出。 - 在需要处理可变参数的自定义函数中使用
vsnprintf
。
- 使用
-
始终检查返回值:
snprintf
和vsnprintf
返回值可以帮助检测格式化过程中的错误或缓冲区溢出。
-
确保缓冲区足够大:
- 根据预期的最大输出长度选择合适的缓冲区大小,或动态分配缓冲区。
-
避免格式化字符串漏洞:
- 不要将未经验证的用户输入作为格式字符串传递给
printf
系列函数,防止潜在的安全漏洞。
- 不要将未经验证的用户输入作为格式字符串传递给
-
使用类型匹配:
- 确保格式说明符与传入参数的类型匹配,避免未定义行为。
-
封装与复用:
- 封装常用的格式化操作到函数或宏中,提高代码复用性和可维护性。
通过理解和正确使用 printf
、snprintf
和 vsnprintf
,可以更有效地处理 C 语言中的字符串输出和格式化任务,编写出更安全、可靠的代码。