《C 语言文件操作补充:字符串格式化与随机读写全解析》

目录

[一. sprintf函数和sscanf函数](#一. sprintf函数和sscanf函数)

[1.1 sprintf 函数:将格式化数据写入字符串](#1.1 sprintf 函数:将格式化数据写入字符串)

[1.2 sscanf 函数:从字符串中格式化读取数据](#1.2 sscanf 函数:从字符串中格式化读取数据)

[二. 文件的随机读写](#二. 文件的随机读写)

[2.1 fseek 函数:移动文件读写指针](#2.1 fseek 函数:移动文件读写指针)

[2.2 ftell 函数:获取当前指针位置](#2.2 ftell 函数:获取当前指针位置)

[2.3 rewind 函数:将指针重置到文件开头](#2.3 rewind 函数:将指针重置到文件开头)

[三. 文件缓冲区](#三. 文件缓冲区)

[3.1 缓冲区的作用](#3.1 缓冲区的作用)

[3.2 缓冲区的分类(按刷新策略)](#3.2 缓冲区的分类(按刷新策略))

[3.3 缓冲区的操作函数](#3.3 缓冲区的操作函数)

fflush:手动刷新缓冲区

setvbuf:自定义缓冲区


一. sprintf函数和sscanf函数

在 C 语言中,sprintfsscanf 是用于字符串格式化处理 的函数,与 printf/scanf 类似,但操作对象不是标准流或文件流,而是字符数组(字符串),常用于字符串的拼接、解析和格式转换。

1.1 sprintf 函数:将格式化数据写入字符串

函数原型

cpp 复制代码
#include <stdio.h>
int sprintf(char *str, const char *format, ...);

参数说明

  • str:目标字符数组(缓冲区),用于存储格式化后的字符串。

  • format:格式化字符串(同 printf),包含普通字符和格式说明符(如 %d%s 等)。

  • ...:可变参数列表,需与 format 中的格式说明符匹配的数据。

返回值

  • 成功:返回写入到字符串中的字符总数 (不包含结尾的 \0)。

  • 失败:返回负数(通常为 -1,表示格式化错误或缓冲区溢出)。

特点与用法

  1. 字符串拼接与格式化 :将多个数据按指定格式组合成一个字符串,比 strcat 更灵活。

  2. 数值转字符串:可将整数、浮点数等转换为字符串形式。

示例 1:拼接字符串与数值

cpp 复制代码
#include <stdio.h>

int main() {
    char buffer[100];  // 目标缓冲区
    int id = 1001;
    char name[] = "LiHua";
    float score = 89.5f;

    // 将数据格式化写入 buffer
    int len = sprintf(buffer, "ID: %d, Name: %s, Score: %.1f", id, name, score);
    
    printf("格式化结果:%s\n", buffer);  // 输出:ID: 1001, Name: LiHua, Score: 89.5
    printf("字符总数:%d\n", len);       // 输出:32(不包含结尾的 \0)
    return 0;
}

示例 2:数值转字符串

cpp 复制代码
char num_str[20];
int num = 12345;
sprintf(num_str, "%d", num);  // 将整数转为字符串 "12345"

1.2 sscanf 函数:从字符串中格式化读取数据

函数原型

cpp 复制代码
#include <stdio.h>
int sscanf(const char *str, const char *format, ...);

参数说明

  • str:源字符串,从中读取并解析数据。

  • format:格式化字符串(同 scanf),指定读取数据的格式。

  • ...:可变参数列表,指向存储读取结果的变量指针(需与格式说明符匹配)。

返回值

  • 成功:返回成功匹配并赋值的数据项数量

  • 失败:返回 EOF-1),表示未匹配到任何数据。

特点与用法

  1. 字符串解析:从复杂字符串中提取指定格式的数据(如从日志字符串中提取时间、数值等)。

  2. 字符串转数值:将字符串形式的数字转换为整数、浮点数等类型。

示例 1:解析字符串中的数据

cpp 复制代码
#include <stdio.h>

int main() {
    char log[] = "2023-10-01 15:30, Temperature: 25.5, Humidity: 60";
    int year, month, day, hour, minute, humidity;
    float temp;

    // 从 log 中按格式提取数据
    int count = sscanf(log, "%d-%d-%d %d:%d, Temperature: %f, Humidity: %d",
                      &year, &month, &day, &hour, &minute, &temp, &humidity);
    
    if (count == 7) {  // 成功提取7项数据
        printf("日期:%d-%d-%d %d:%d\n", year, month, day, hour, minute);
        printf("温度:%.1f℃,湿度:%d%%\n", temp, humidity);
    }
    return 0;
}

输出:

cpp 复制代码
日期:2023-10-01 15:30
温度:25.5℃,湿度:60%

示例 2:字符串转数值

cpp 复制代码
char str[] = "123.45";
int int_val;
float float_val;

sscanf(str, "%d", &int_val);    // 提取整数部分:int_val = 123
sscanf(str, "%f", &float_val);  // 提取浮点数:float_val = 123.45

三、注意事项

  1. 缓冲区溢出风险
    sprintf 不会检查目标缓冲区 str 的大小,若写入内容超过缓冲区长度,会导致内存越界(覆盖其他数据)。建议使用更安全的 snprintf(限制最大写入长度):

    cpp 复制代码
    // 最多写入 99 个字符(留 1 个给 \0)
    snprintf(buffer, 100, "ID: %d, Name: %s", id, name);
  2. 格式匹配要求
    sscanf 依赖格式字符串与源字符串的严格匹配,若格式不符(如多余字符、缺少分隔符),会导致提取失败。例如:

    cpp 复制代码
    char str[] = "age=20";
    int age;
    sscanf(str, "age=%d", &age);  // 正确匹配,age=20
    sscanf(str, "%d", &age);      // 匹配失败,返回 0

二. 文件的随机读写

2.1 fseek 函数:移动文件读写指针

fseek 用于将文件流的读写指针移动到指定位置,是文件随机访问的核心函数。

函数原型

cpp 复制代码
#include <stdio.h>
int fseek(FILE *stream, long int offset, int origin);

参数说明

  • stream:目标文件流(文件指针)。

  • offset:偏移量(字节数),可正可负(正数表示向后移动,负数表示向前移动)。

  • origin:起始位置(基准点),必须是以下宏之一:

    • SEEK_SET(0):从文件开头开始计算偏移。

    • SEEK_CUR(1):从当前指针位置开始计算偏移。

    • SEEK_END(2):从文件末尾开始计算偏移。

返回值

  • 成功:返回 0

  • 失败:返回非零值(如偏移量超出文件范围)。

用法示例

cpp 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.bin", "rb+");  // 二进制读写模式
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    // 1. 移动到文件开头后第10字节处(从开头偏移10)
    fseek(fp, 10, SEEK_SET);

    // 2. 从当前位置向后移动5字节
    fseek(fp, 5, SEEK_CUR);

    // 3. 移动到文件末尾前3字节处(从末尾偏移-3)
    fseek(fp, -3, SEEK_END);

    fclose(fp);
    return 0;
}

注意事项

  • 二进制文件 vs 文本文件

    • 二进制文件中,fseek 可准确定位到任意字节位置。

    • 文本文件中,由于换行符转换(如 Windows 下 \n 存储为 \r\n),SEEK_END 和负偏移可能导致定位不准,建议仅在二进制文件中使用随机访问。

  • 读写模式要求 :文件需以允许读写的模式打开(如 r+/rb+ 等),否则定位可能失败。

2.2 ftell 函数:获取当前指针位置

ftell 用于获取文件流当前读写指针相对于文件开头的偏移量(字节数)。

函数原型

cpp 复制代码
#include <stdio.h>
long int ftell(FILE *stream);

参数说明

  • stream:目标文件流(文件指针)。

返回值

  • 成功:返回当前指针位置(从文件开头算起的字节数)。

  • 失败:返回 -1Llong 类型的 -1)。

用法示例

cpp 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.bin", "rb");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    // 读取2个字符后,获取当前位置
    fgetc(fp);
    fgetc(fp);
    long pos = ftell(fp);
    printf("当前指针位置:%ld 字节(从文件开头算起)\n", pos);  // 输出:2

    fclose(fp);
    return 0;
}

典型用途

  • 计算文件大小(结合 fseek 定位到文件末尾):

    cpp 复制代码
    fseek(fp, 0, SEEK_END);       // 移动到文件末尾
    long file_size = ftell(fp);   // 获取文件总字节数
  • 记录当前位置,后续可通过 fseek 回到该位置。

2.3 rewind 函数:将指针重置到文件开头

rewind 是简化版的 fseek,功能是将文件读写指针移动到文件开头,同时清除文件流的错误标志。

函数原型

cpp 复制代码
#include <stdio.h>
void rewind(FILE *stream);

参数说明

  • stream:目标文件流(文件指针)。

功能等价于

cpp 复制代码
fseek(stream, 0L, SEEK_SET);  // 移动到文件开头
clearerr(stream);             // 清除错误标志

用法示例

cpp 复制代码
#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    // 读取文件内容
    char buf[100];
    fgets(buf, 100, fp);
    printf("第一次读取:%s", buf);

    // 重置指针到开头,重新读取
    rewind(fp);
    fgets(buf, 100, fp);
    printf("第二次读取:%s", buf);  // 再次读取第一行

    fclose(fp);
    return 0;
}

总结

  • fseek:灵活移动指针,支持从开头、当前位置、末尾计算偏移,是随机访问的核心。

  • ftell:获取当前指针位置,常用于计算文件大小或记录位置。

  • rewind:快速将指针重置到文件开头,简化常见定位操作。

这三个函数配合使用,可实现文件的随机读写(如修改中间数据、跳读特定块),但需注意文本文件的定位限制,优先在二进制文件中使用。

三. 文件缓冲区

在 C 语言文件操作中,文件缓冲区(File Buffer) 是内存中的一块块临时存储区域,用于缓和程序与外部存储设备(如硬盘)之间的读写速度差异,是提高文件操作效率的关键机制。

3.1 缓冲区的作用

  • 平衡速度差异:CPU 和内存的读写速度远快于磁盘,缓冲区可批量处理数据(先攒一批数据再一次性写入磁盘,或一次性从磁盘读取一批数据到内存),减少磁盘 I/O 次数。
  • 减少系统调用:每次直接读写磁盘需要操作系统介入(系统调用),开销较大;通过缓冲区批量操作,可大幅减少系统调用次数。

3.2 缓冲区的分类(按刷新策略)

C 语言标准库根据文件流类型,默认使用三种缓冲区策略:

缓冲区类型 适用场景 刷新时机(数据写入磁盘)
全缓冲 普通文件(如 test.txt 1. 缓冲区满时 2. 调用 fclose 关闭文件时 3. 手动调用 fflush
行缓冲 标准输出流 stdout 1. 遇到换行符 \n 时 2. 缓冲区满时 3. 关闭文件或手动刷新时
无缓冲 标准错误流 stderr 数据直接写入设备(无缓冲),确保错误信息及时显示

3.3 缓冲区的操作函数

fflush:手动刷新缓冲区

强制将缓冲区中的数据写入磁盘(或输出设备),避免数据滞留内存。

cpp 复制代码
#include <stdio.h>
int fflush(FILE *stream);
  • 参数:stream 为目标文件流(NULL 表示刷新所有输出流)。
  • 返回值:成功返回 0;失败返回 EOF

示例

cpp 复制代码
FILE *fp = fopen("data.txt", "w");
fputs("Hello", fp);  // 数据暂存在缓冲区,未写入磁盘
fflush(fp);          // 手动刷新,数据写入磁盘
setvbuf:自定义缓冲区

在打开文件后、进行读写操作前,可通过 setvbuf 手动设置缓冲区的类型和大小。

cpp 复制代码
#include <stdio.h>
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
  • 参数:
    • stream:目标文件流。
    • buf:自定义缓冲区地址(NULL 表示使用系统自动分配的缓冲区)。
    • mode:缓冲模式(_IOFBF 全缓冲、_IOLBF 行缓冲、_IONBF 无缓冲)。
    • size:缓冲区大小(字节数)。
  • 返回值:成功返回 0;失败返回非零。

示例

cpp 复制代码
FILE *fp = fopen("test.txt", "w");
char mybuf[1024];
// 设置为全缓冲,使用自定义缓冲区(大小1024字节)
setvbuf(fp, mybuf, _IOFBF, 1024);