标准IO

标准IO


一、文件的打开与关闭

不管用系统IO函数还是标准IO函数,操作文件的第一步,都是"打开(open/fopen)"文件,需要注意:

系统IO:打开文件得到的是一个整数,称为文件描述符

标准IO:打开文件得到的是一个指针,称为文件指针。

文件指针指向结构体 FILE,该结构体内部包含了文件描述符,如下图:

标准IO

二、基础API接口

① 打开文件 fopen

函数原型: 
    FILE *fopen(const char * pathname, const char * mode);
参数分析:
    pathname --> 需要打开的目标文件的完整路径+名字
    mode --> 打开的标记
        r  只读打开,并读写位置默认为文件头
        r+ 可读【可写】打开,并读写位置默认为文件头
        w  只写方式打开,如果不存在则创建,如果存在则清空文件。
        w+ 【必须得到一个全新“空白”的文件】可读可写方式打开,如果不存在则创建,如果存在则清空。
        a  【追加方式】只写方式打开,如果不存在则创建,如果存在则末尾追加
        a+ 【追加方式】可读可写方式打开,如果不存在则创建,如果存在则末尾追加
返回值:
    成功 返回一个文件流指针
    失败 返回NULL 并设置错误码

② 读写文件 fread fwrite

按数据块读写

函数原型:
size_t fread (     void  * ptr ,size_t size, size_t nmemb , FILE *restrict stream);
size_t fwrite(const void * ptr ,size_t size, size_t nmemb , FILE *restrict stream);
参数分析:
    ptr --> 
        把读取到的数据存入该内存空间【可读可写】
        需要写入的数据的内存空间【只读即可】
    size --> 需要读写的数据的尺寸
    nmemb --> 需要读写的数据块是多少
    stream --> 需要读写的目标文件流指针
返回值:
    成功 返回实际读写的数据块
    失败 0 

按字节读写文本文件

按字节读写
  • 关键点:
  • fgetc()getc()功能完全一样,区别是fgetc是函数,而getc是宏。
  • fputc()putc()功能完全一样,区别是fputc是函数,而putc是宏。
  • getchar()putchar()只能针对键盘输入和屏幕输出,不能指定别的文件。

按行读写文本文件

按行读写

关键点:

  • 对于读操作而言,返回 EOF(-1) 意味着读操作失败,这有两种情况:
    • i.如果 feof(fp) 为真,此时意味着读到了文件末尾,没有数据可读了。
    • ii.如果 ferror(fp) 为真,此时意味着遇到了错误。
  • 读操作函数接口的返回值是 int ,而不是 char ,原因是当读操作失败是返回的 EOF 的数值是 -1,而 char 型数据可能无法表达 -1。

fgets()gets() 都是按行读取文件数据,他们的区别是:

  • fgets() 可以读取指定的任意文件,而 gets() 只能从键盘读取。
  • fgets() 有内存边界判断,而 gets() 没有,因此后者是不安全的,不建议使用。
  • fgets() 在任何情形下都按原样读取数据,会读取\n,但 gets() 会自动去除数据末尾的 ‘\n’

fputs()puts() 都是按行将数据写入文件,他们的区别是:

  • fputs() 可以将数据写入指定的任意文件,而 puts() 只能将数据输出到屏幕。
  • fputs() 在任何情形下都按原样写入数据,但 puts() 会自动给写入数据的末尾加上 ‘\n’

按指定格式读写文本文件

按指定格式读写
  • 注意:
    1.fprintf( )不仅可以像printf( )一样向标准输出设备输出信息,也可以向由stream指定的任何有相应权限的文件写入数据。
    2.sprintf()snprintf()都是向一块自定义缓冲区写入数据,不同的是后者第二个参数提供了这块缓冲区的大小,避免缓冲区溢出,因此应尽量使用后者,放弃使用前者。
    3.fscanf( )不仅可以像scanf( )一样从标准输入设备读入信息,也可以从由stream指定的任何有相应权限的文件读入数据。
    4.sscanf( )从一块由s指定的自定义缓冲区中读入数据。
    5.这些函数的读写都是带格式的,格式由下表规定:
指定格式

③ 设置偏移量

函数原型
   int fseek(FILE *stream, long offset, int whence);
参数分析:
   stream --> 指明具体的文件流指针
   offset --> 设置具体的偏移量
   whence --> 如何偏移
       SEEK_SET 设置读写位置到offset 的位置(从0开始)
       SEEK_CUR 以当前位置开始往前或往后偏移 (offset 可以是正数,也可以是负数)
       SEEK_END 把偏移量从文件末尾开始设置 一般把offset设置为负数
返回值:
   成功 返回0
   失败 返回-1 并设置错误码

④ 获取当前的文件读写位置

long ftell(FILE *stream);

⑤ 关闭文件

int fclose(FILE *stream);

⑥ 示例代码

// 标准 I/O 示例代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

// 示例1:fopen的各种打开模式
void example1() {
    FILE *fp;
    
    // r 模式:只读打开
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("r模式:文件不存在或打开失败\n");
    } else {
        printf("r模式:文件打开成功\n");
        fclose(fp);
    }
    
    // w 模式:只写打开,不存在则创建,存在则清空
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("w模式:打开失败\n");
    } else {
        printf("w模式:文件打开/创建成功\n");
        fclose(fp);
    }
    
    // a 模式:追加方式打开
    fp = fopen("test.txt", "a");
    if (fp == NULL) {
        printf("a模式:打开失败\n");
    } else {
        printf("a模式:文件打开成功\n");
        fclose(fp);
    }
    
    // r+ 模式:可读可写打开
    fp = fopen("test.txt", "r+");
    if (fp == NULL) {
        printf("r+模式:打开失败\n");
    } else {
        printf("r+模式:文件打开成功\n");
        fclose(fp);
    }
    
    // w+ 模式:可读可写,清空文件
    fp = fopen("test.txt", "w+");
    if (fp == NULL) {
        printf("w+模式:打开失败\n");
    } else {
        printf("w+模式:文件打开成功\n");
        fclose(fp);
    }
    
    // a+ 模式:可读可写,追加方式
    fp = fopen("test.txt", "a+");
    if (fp == NULL) {
        printf("a+模式:打开失败\n");
    } else {
        printf("a+模式:文件打开成功\n");
        fclose(fp);
    }
}

// 示例2:按数据块读写
void example2() {
    FILE *fp;
    struct Student {
        char name[20];
        int age;
        float score;
    };
    
    struct Student students[3] = {
        {"张三", 20, 85.5},
        {"李四", 21, 92.0},
        {"王五", 19, 78.5}
    };
    
    struct Student read_students[3];
    
    // 写入数据块
    fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    size_t written = fwrite(students, sizeof(struct Student), 3, fp);
    printf("写入了 %zu 个学生数据\n", written);
    fclose(fp);
    
    // 读取数据块
    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    size_t read = fread(read_students, sizeof(struct Student), 3, fp);
    printf("读取了 %zu 个学生数据\n", read);
    
    for (int i = 0; i < read; i++) {
        printf("学生%d: 姓名=%s, 年龄=%d, 成绩=%.1f\n", 
               i+1, read_students[i].name, read_students[i].age, read_students[i].score);
    }
    fclose(fp);
}

// 示例3:按字节读写
void example3() {
    FILE *fp;
    char ch;
    
    // 写入字节数据
    fp = fopen("byte_test.txt", "w");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    for (ch = 'A'; ch <= 'Z'; ch++) {
        fputc(ch, fp);
    }
    fputc('\n', fp);
    fclose(fp);
    
    // 读取字节数据
    fp = fopen("byte_test.txt", "r");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    printf("文件内容: ");
    while ((ch = fgetc(fp)) != EOF) {
        putchar(ch);
    }
    fclose(fp);
}

// 示例4:按行读写
void example4() {
    FILE *fp;
    char buffer[256];
    
    // 写入行数据
    fp = fopen("line_test.txt", "w");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    fputs("这是第一行\n", fp);
    fputs("这是第二行\n", fp);
    fputs("这是第三行\n", fp);
    fclose(fp);
    
    // 读取行数据
    fp = fopen("line_test.txt", "r");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    printf("文件内容:\n");
    int line_num = 1;
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("第%d行: %s", line_num++, buffer);
    }
    fclose(fp);
}

// 示例5:格式化读写
void example5() {
    FILE *fp;
    char name[20];
    int age;
    float salary;
    
    // 格式化写入
    fp = fopen("format_test.txt", "w");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    fprintf(fp, "姓名: %s, 年龄: %d, 工资: %.2f\n", "张三", 25, 8000.50);
    fprintf(fp, "姓名: %s, 年龄: %d, 工资: %.2f\n", "李四", 30, 12000.75);
    fclose(fp);
    
    // 格式化读取
    fp = fopen("format_test.txt", "r");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    printf("格式化读取:\n");
    while (fscanf(fp, "姓名: %s, 年龄: %d, 工资: %f", name, &age, &salary) == 3) {
        printf("姓名: %s, 年龄: %d, 工资: %.2f\n", name, age, salary);
        // 跳过换行符
        fgetc(fp);
    }
    fclose(fp);
}

// 示例6:文件偏移操作
void example6() {
    FILE *fp;
    char buffer[100];
    
    fp = fopen("seek_test.txt", "w+");
    if (fp == NULL) {
        printf("打开文件失败\n");
        return;
    }
    
    // 写入测试数据
    fprintf(fp, "0123456789ABCDEFGHIJ");
    
    // 获取当前位置
    long pos = ftell(fp);
    printf("写入后当前位置: %ld\n", pos);
    
    // 回到文件开头
    fseek(fp, 0, SEEK_SET);
    printf("回到开头后位置: %ld\n", ftell(fp));
    
    // 读取前10个字符
    fread(buffer, 1, 10, fp);
    buffer[10] = '\0';
    printf("前10个字符: %s\n", buffer);
    printf("读取后位置: %ld\n", ftell(fp));
    
    // 从当前位置向后移动5字节
    fseek(fp, 5, SEEK_CUR);
    printf("向后移动5字节后位置: %ld\n", ftell(fp));
    
    // 从文件末尾向前移动3字节
    fseek(fp, -3, SEEK_END);
    printf("从末尾向前3字节位置: %ld\n", ftell(fp));
    
    // 读取当前位置的字符
    char ch = fgetc(fp);
    printf("当前位置的字符: %c\n", ch);
    
    fclose(fp);
}

// 示例7:错误处理
void example7() {
    FILE *fp;
    
    fp = fopen("nonexistent.txt", "r");
    if (fp == NULL) {
        perror("打开文件失败");
        printf("错误号: %d\n", errno);
        return;
    }
    
    // 模拟读取到文件末尾
    fgetc(fp); // 读取一个字符
    while (!feof(fp)) {
        fgetc(fp);
    }
    
    if (feof(fp)) {
        printf("已到达文件末尾\n");
    }
    
    if (ferror(fp)) {
        printf("文件操作发生错误\n");
    }
    
    fclose(fp);
}

// 示例8:sprintf和snprintf
void example8() {
    char buffer1[50];
    char buffer2[50];
    int year = 2024;
    char month[] = "March";
    int day = 15;
    
    // sprintf - 不安全的版本
    sprintf(buffer1, "今天是 %s %d, %d", month, day, year);
    printf("sprintf: %s\n", buffer1);
    
    // snprintf - 安全的版本
    snprintf(buffer2, sizeof(buffer2), "今天是 %s %d, %d", month, day, year);
    printf("snprintf: %s\n", buffer2);
    
    // 测试缓冲区溢出保护
    char small_buf[10];
    int needed = snprintf(small_buf, sizeof(small_buf), "这是一个很长的字符串");
    printf("需要的缓冲区大小: %d, 实际写入: %s\n", needed, small_buf);
}

int main() {
    printf("=== 示例1: fopen各种打开模式 ===\n");
    example1();
    
    printf("\n=== 示例2: 按数据块读写 ===\n");
    example2();
    
    printf("\n=== 示例3: 按字节读写 ===\n");
    example3();
    
    printf("\n=== 示例4: 按行读写 ===\n");
    example4();
    
    printf("\n=== 示例5: 格式化读写 ===\n");
    example5();
    
    printf("\n=== 示例6: 文件偏移操作 ===\n");
    example6();
    
    printf("\n=== 示例7: 错误处理 ===\n");
    example7();
    
    printf("\n=== 示例8: sprintf和snprintf ===\n");
    example8();
    
    return 0;
}

04 标准IO的缓冲区(课堂原件,未改动)

> 以下所有内容均与原 .docx 保持一致,仅追加「个人拓展」章节,可直接复制使用。


三、标准IO缓冲区

标准IO实际上是系统IO的封装,这种封装体现如下图所示,fopen()函数将调用open()得到的文件描述符填入结构体 FILE 中,并为文件分配缓冲区、设置缓冲区类型,最后给用户返回指向 FILE 的指针 fp,称为文件指针:

标准IO缓冲区

如上图所示,每当使用标准IO的写操作函数,试图将数据写入文件 a.txt 时,数据都会流过缓冲区,然后再在适当的时刻冲洗(或称刷新,flush)到内核,最后才真正写入设备文件。

按照缓冲区按照什么时候冲洗数据到内核,可以将缓冲区分成以下三类:


不缓冲类型(_IONBF)

一旦有数据,立刻将数据冲洗到文件(同步到外围设备 系统IO)


全缓冲类型(_IOFBF)

  • 一旦填满缓冲区,立刻将数据冲洗到文件(同步到外围设备)
  • 程序正常退出时,立刻将数据冲洗到文件
  • 如果程序段错误或 Ctrl + C 强制终止,则数据会留在缓冲区无法同步到文件中(数据丢失)
  • 遇到 fflush() 强制冲洗时(强制同步)立刻将数据冲洗到文件
  • 关闭文件时,立刻将数据冲洗到文件
  • 读取文件内容时,立刻将数据冲洗到文件
  • 改变缓冲区类型时,立刻将数据冲洗到文件(setbuf / setvbuf ...)

行缓冲类型(_IOLBF)

同全缓冲类型,一旦遇到\n时,立刻将数据冲洗到文件


关键点摘要

  • 缓冲(buffer)都是针对写操作而言的,缓冲的存在是为了提高写效率
  • 对于标准输出而言,默认是行缓冲的
  • 对于标准出错而言,默认是不缓冲的
  • 对于普通文件而言,默认都是全缓冲类型
  • 滞留在缓冲区中的数据有时被称为脏数据(dirty data)
  • 脏数据的存在代表程序操作的结果与文件真实状态不一致,若未正常冲洗这些脏数据就退出程序(异常退出:崩溃、Ctrl+C)则有可能会造成数据丢失
  • 这三种缓冲类型,可以通过函数 setbuf()/setbuf() 来修改

如何设置缓冲区类型

函数原型:
    基础的万能类型
int setvbuf    (FILE *restrict stream, char buf[restrict .size],int mode, size_t size);

    可以便捷设置为无缓冲buf  设置为NULL 
void setbuf    (FILE *restrict stream, char *restrict buf                             );
    
    保持缓冲区类型不变,但是需要调成缓冲区的地址
void setbuffer (FILE *restrict stream, char buf[restrict .size],          size_t size );

    保持缓冲区地址不变,但是改变缓冲区类型为行缓冲
void setlinebuf(FILE *stream                                                          );

参数分析:
    stream --> 需要设置缓冲区的目标文件
    buf --> 指定用户的自定义缓冲区地址
    mode --> 缓冲区类型
        _IONBF unbuffered 无缓冲
        _IOLBF line buffered  行缓冲
        _IOFBF fully buffered 全缓冲
    size --> 缓冲区的具体尺寸

示例:

// 把现有的缓冲区类型设置为行行冲
setvbuf(stream, NULL, _IOLBF, 0);  
setlinebuf( stream );

// 设置无缓冲
setvbuf(fp, NULL, _IONBF, 0); 
posted @ 2025-11-06 08:27  林明杰  阅读(4)  评论(0)    收藏  举报