sprintf (string print formatted) 是一个标准 C 库函数,它的工作方式与 printf 非常相似,但它不是将格式化后的字符串输出到标准输出(通常是屏幕),而是将其写入一个字符数组(即字符串)中。

(图片来源网络,侵删)
sprintf 的原型和功能
函数原型:
#include <stdio.h> int sprintf(char *str, const char *format, ...);
参数说明:
str: 指向字符数组的指针。sprintf会将格式化后的字符串写入这个数组。调用者必须确保这个数组有足够的空间,否则会导致缓冲区溢出,这是一个常见的安全漏洞。format: 格式化字符串,它包含了要被复制的字符以及由 开头的格式说明符。- 可变参数列表,这些参数的数量和类型必须与
format字符串中的格式说明符一一对应。
返回值:
- 成功时,返回写入到
str中的字符总数(不包括结尾的空字符\0)。 - 如果发生编码错误,则返回一个负数。
一个简单的 sprintf 实现
下面是一个简化版的 sprintf 实现,这个版本不处理所有可能的格式说明符,但它涵盖了最核心的功能:将普通字符和不同类型的变量(如 int, char, char*)格式化到一个字符串中。

(图片来源网络,侵删)
我们将这个函数命名为 my_sprintf 以避免与标准库函数冲突。
#include <stdarg.h> // 用于可变参数
#include <string.h> // 用于 strlen
// 将一个整数转换为字符串,并追加到 dest 的末尾
void append_int(char *dest, int num) {
// 处理 0 的情况
if (num == 0) {
strcat(dest, "0");
return;
}
// 处理负数
if (num < 0) {
strcat(dest, "-");
num = -num;
}
// 将数字转换为字符串(反向)
char buffer[20]; // 足够存放一个 int 的字符串
int i = 0;
while (num > 0) {
buffer[i++] = (num % 10) + '0';
num /= 10;
}
buffer[i] = '\0'; // 字符串结束符
// 反转字符串并追加到 dest
int len = strlen(buffer);
for (int j = 0; j < len; j++) {
dest[strlen(dest)] = buffer[len - 1 - j];
}
}
/**
* @brief 一个简化的 sprintf 实现
* @param str 目标缓冲区
* @param format 格式化字符串
* @param ... 可变参数
* @return 写入的字符总数
*/
int my_sprintf(char *str, const char *format, ...) {
va_list args; // 定义一个 va_list 类型的变量
va_start(args, format); // 初始化 args,使其指向 format 后面的第一个可变参数
int count = 0; // 记录已写入的字符数
const char *p = format;
// 遍历格式化字符串
while (*p != '\0') {
if (*p != '%') {
// 如果不是格式说明符,直接复制字符
str[count++] = *p;
} else {
// 遇到格式说明符,处理可变参数
p++; // 跳过 '%'
switch (*p) {
case 'd': { // 处理整数 %d
int num = va_arg(args, int);
// 创建一个临时字符串来存放转换后的数字
char temp_buffer[20] = {0};
append_int(temp_buffer, num);
// 将临时字符串拼接到主字符串中
strcat(str, temp_buffer);
count += strlen(temp_buffer);
break;
}
case 'c': { // 处理字符 %c
// char 在可变参数中会被提升为 int
int c = va_arg(args, int);
str[count++] = (char)c;
break;
}
case 's': { // 处理字符串 %s
char *s = va_arg(args, char*);
// 将字符串拼接到主字符串中
strcat(str, s);
count += strlen(s);
break;
}
case '%': { // 处理 %% 输出一个百分号
str[count++] = '%';
break;
}
default: {
// 未知格式说明符,直接复制并打印一个警告
str[count++] = '%';
str[count++] = *p;
// printf("Warning: Unknown format specifier '%%%c'\n", *p);
break;
}
}
}
p++; // 移动到下一个字符
}
str[count] = '\0'; // 字符串必须以空字符结尾
va_end(args); // 清理 va_list
return count; // 返回写入的总字符数
}
代码解析
-
头文件:
stdarg.h: 提供了处理可变参数的宏va_list,va_start,va_arg,va_end。string.h: 提供了strlen和strcat等字符串操作函数,简化了实现。
-
append_int辅助函数:- 这个函数专门负责将一个整数转换成字符串。
- 它处理了 0 和负数的情况。
- 它通过取模和除法运算从低位到高位获取数字,然后将它们存入一个临时缓冲区,最后再反转这个缓冲区得到正确的字符串顺序,并追加到目标字符串
dest。
-
my_sprintf主函数:
(图片来源网络,侵删)va_list和va_start:va_list是一个用于遍历可变参数列表的类型。va_start宏初始化这个列表,使其指向format参数之后第一个可变参数的位置。- 遍历
format字符串: 我们使用一个指针p来逐个检查format字符串中的字符。 - 普通字符: 如果当前字符不是 ,就直接把它复制到目标缓冲区
str中,并增加计数器count。 - 格式说明符: 如果遇到 ,我们就进入
switch语句来处理后续的字符(如d,c,s)。%d: 使用va_arg(args, int)从可变参数列表中获取一个int类型的参数,然后调用append_int将其转换为字符串并拼接到str中。%c: 注意,在可变参数中,char类型的参数会被“提升”为int类型,va_arg的类型是int,我们获取到int后,再强制转换为char存入缓冲区。%s: 使用va_arg(args, char*)获取一个char*类型的参数(即字符串指针),然后直接使用strcat将其拼接到str中。- : 这是一个特殊情况,用于在输出中显示一个 字符本身。
- 字符串结尾: 在所有处理完成后,必须在
str的末尾手动添加一个空字符\0,否则它就不是一个合法的 C 字符串。 va_end: 使用va_end来清理va_list资源,这是一个好习惯。- 返回值: 返回
count,即写入的总字符数(不包括\0)。
使用示例
#include <stdio.h> // 用于标准 sprintf 进行对比
int main() {
char buffer1[100];
char buffer2[100];
int age = 30;
float height = 180.5f; // 我们的 my_sprintf 暂时不支持 %f
const char *name = "Alice";
// 使用我们自己的 my_sprintf
int written1 = my_sprintf(buffer1, "Name: %s, Age: %d, Height: %.1f", name, age, height);
printf("my_sprintf result: %s\n", buffer1);
printf("my_sprintf wrote %d characters.\n", written1);
printf("\n----------------------------------\n\n");
// 使用标准库的 sprintf 进行对比
int written2 = sprintf(buffer2, "Name: %s, Age: %d, Height: %.1f", name, age, height);
printf("sprintf result: %s\n", buffer2);
printf("sprintf wrote %d characters.\n", written2);
return 0;
}
编译和运行结果:
my_sprintf result: Name: Alice, Age: 30, Height: .0
my_sprintf wrote 24 characters.
----------------------------------
sprintf result: Name: Alice, Age: 30, Height: 180.5
sprintf wrote 29 characters.
结果分析:
- 我们可以看到,
my_sprintf成功处理了%s和%d。 - 由于我们的实现没有处理
%f(浮点数),所以它把%.1f当作普通字符处理了,导致Height: .0的错误输出。 - 写入的字符数也因此不同,标准
sprintf正确计算了浮点数部分所占的空间。
真实世界的 sprintf 和安全问题
- 功能强大: 标准库的
sprintf功能非常强大,支持int,float/double,long,short,unsigned, 指针,以及各种宽度、对齐、精度修饰符(如%05d,%-10.2f)等。 - 缓冲区溢出:
sprintf的最大问题是它不检查目标缓冲区的大小,如果格式化后的字符串超过了str指向的数组大小,就会发生缓冲区溢出,这会覆盖掉内存中的其他数据,可能导致程序崩溃、数据损坏,甚至被恶意利用。
安全的替代方案:
为了解决这个问题,C 标准库提供了更安全的版本:
-
snprintf(或sprintf_s)- 原型:
int snprintf(char *str, size_t size, const char *format, ...); - 功能: 它多了一个
size参数,用于指定目标缓冲区str的大小。 - 安全性: 它最多只会写入
size - 1个字符,并自动在末尾添加\0,如果输出被截断,它会返回一个负值或实际需要的字符数(具体行为取决于编译器标准),但缓冲区永远不会溢出。
char buffer[10]; // 即使格式化字符串很长,snprintf 也能保证安全 int n = snprintf(buffer, sizeof(buffer), "This is a very long string"); // buffer 的内容是 "This is a" (被截断),并且以 '\0' printf("n = %d\n", n); // n 会返回 19 (需要的字符数) - 原型:
-
asprintf(非标准,但常见)- 这个函数在 POSIX 系统和一些非标准库(如 glibc)中提供。
- 功能: 它会自动为你分配足够的内存来存放格式化后的字符串,并将指针保存在一个
char**参数中。 - 优点: 完全免除了缓冲区管理的烦恼,非常安全方便。
- 注意: 调用者需要在使用后
free()这块分配的内存。
char *str; // asprintf 会自动分配内存 asprintf(&str, "Hello, %s!", "World"); printf("%s\n", str); // 输出: Hello, World! free(str); // 记得释放内存
我们实现了一个简化版的 sprintf,它展示了该函数的核心工作原理:解析格式字符串,处理可变参数,并将结果拼接到一个目标缓冲区中,虽然这个实现不完整,但它清晰地揭示了 sprintf 的内部机制,在实际开发中,应始终优先使用更安全的 snprintf 或 asprintf 来避免缓冲区溢出风险。
