sprintf在C语言中如何实现?

99ANYc3cd6
预计阅读时长 26 分钟
位置: 首页 C语言 正文

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

sprintf c语言 实现
(图片来源网络,侵删)

sprintf 的原型和功能

函数原型:

#include <stdio.h>
int sprintf(char *str, const char *format, ...);

参数说明:

  • str: 指向字符数组的指针。sprintf 会将格式化后的字符串写入这个数组。调用者必须确保这个数组有足够的空间,否则会导致缓冲区溢出,这是一个常见的安全漏洞。
  • format: 格式化字符串,它包含了要被复制的字符以及由 开头的格式说明符。
  • 可变参数列表,这些参数的数量和类型必须与 format 字符串中的格式说明符一一对应。

返回值:

  • 成功时,返回写入到 str 中的字符总数(不包括结尾的空字符 \0)。
  • 如果发生编码错误,则返回一个负数。

一个简单的 sprintf 实现

下面是一个简化版的 sprintf 实现,这个版本不处理所有可能的格式说明符,但它涵盖了最核心的功能:将普通字符和不同类型的变量(如 int, char, char*)格式化到一个字符串中。

sprintf c语言 实现
(图片来源网络,侵删)

我们将这个函数命名为 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;      // 返回写入的总字符数
}

代码解析

  1. 头文件:

    • stdarg.h: 提供了处理可变参数的宏 va_list, va_start, va_arg, va_end
    • string.h: 提供了 strlenstrcat 等字符串操作函数,简化了实现。
  2. append_int 辅助函数:

    • 这个函数专门负责将一个整数转换成字符串。
    • 它处理了 0 和负数的情况。
    • 它通过取模和除法运算从低位到高位获取数字,然后将它们存入一个临时缓冲区,最后再反转这个缓冲区得到正确的字符串顺序,并追加到目标字符串 dest
  3. my_sprintf 主函数:

    sprintf c语言 实现
    (图片来源网络,侵删)
    • va_listva_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 标准库提供了更安全的版本:

  1. 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 (需要的字符数)
  2. asprintf (非标准,但常见)

    • 这个函数在 POSIX 系统和一些非标准库(如 glibc)中提供。
    • 功能: 它会自动为你分配足够的内存来存放格式化后的字符串,并将指针保存在一个 char** 参数中。
    • 优点: 完全免除了缓冲区管理的烦恼,非常安全方便。
    • 注意: 调用者需要在使用后 free() 这块分配的内存。
    char *str;
    // asprintf 会自动分配内存
    asprintf(&str, "Hello, %s!", "World");
    printf("%s\n", str); // 输出: Hello, World!
    free(str); // 记得释放内存

我们实现了一个简化版的 sprintf,它展示了该函数的核心工作原理:解析格式字符串,处理可变参数,并将结果拼接到一个目标缓冲区中,虽然这个实现不完整,但它清晰地揭示了 sprintf 的内部机制,在实际开发中,应始终优先使用更安全的 snprintfasprintf 来避免缓冲区溢出风险。

-- 展开阅读全文 --
头像
C语言中如何定义和使用refcursor变量?
« 上一篇 01-22
织梦模板如何添加搜索功能?
下一篇 » 01-22

相关文章

取消
微信二维码
支付宝二维码

目录[+]