C语言中的printf、sprintf、snprintf、vsnprintf 函数
目录
1.1 概述
1.2 函数原型
1.3 返回值
1.4 示例
1.5 输出结果
1.6 常用格式说明符
1.7 注意事项
2. snprintf 函数
2.1 概述
2.2 函数原型
2.3 返回值
2.4 示例
2.5 输出结果
2.6 使用场景
2.7 注意事项
3. vsnprintf 函数
3.1 概述
3.2 函数原型
3.3 返回值
3.4 使用场景
3.5 示例
3.6 输出结果
3.7 注意事项
4. 如何选择使用哪一个函数
4.1 简单总结
5. 实际应用示例:构建并发送 AT 指令
5.1 使用 snprintf 构建指令
5.2 使用 vsnprintf 在可变参数函数中构建指令
5.3 输出结果
5.4 解释
6. 总结与实践
理解 printf
、sprintf、snprintf
和 vsnprintf
这几个函数对于有效地处理字符串输出和格式化非常重要,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
:指向字符数组的指针,用于存储格式化后的字符串。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 语言中的字符串输出和格式化任务,编写出更安全、可靠的代码。