MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言文件输入输出深入解析

2021-01-167.3k 阅读

C 语言文件输入输出基础概念

文件的定义与分类

在 C 语言中,文件是存储在外部介质(如硬盘、U盘 等)上的数据的集合。从操作系统的角度来看,文件可以分为两大类:文本文件和二进制文件。

  1. 文本文件:文本文件以字符编码(如 ASCII 码)的形式存储数据,每个字符占用一个字节(对于 ASCII 编码而言)。例如,数字 123 在文本文件中实际存储的是字符 '1'(ASCII 码值为 49)、字符 '2'(ASCII 码值为 50)和字符 '3'(ASCII 码值为 51)。文本文件的优点是可读性强,便于直接查看和编辑,但在存储数值等数据时可能会占用较多空间,并且在读取和写入时可能需要进行字符与数值的转换。

  2. 二进制文件:二进制文件直接以二进制形式存储数据,数据在内存中的存储形式与在文件中的存储形式相同。例如,整数 123 在内存中以二进制补码形式存储(假设为 32 位系统,存储为 0x0000007B),在二进制文件中也是以这 4 个字节(00 00 00 7B)的形式存储。二进制文件的优点是存储空间利用率高,读取和写入速度快,适合存储大量的数值数据、图像、音频等非文本数据,但可读性较差,不能直接用文本编辑器查看和编辑。

文件指针

在 C 语言中,对文件的操作是通过文件指针来实现的。文件指针是一个指向 FILE 类型结构体的指针变量。FILE 结构体定义在 <stdio.h> 头文件中,它包含了与文件相关的各种信息,如文件当前位置、文件状态标志等。

定义文件指针的一般形式为:

FILE *fp;

这里 fp 就是一个文件指针变量,可以用来指向一个具体的文件。在对文件进行任何操作之前,都需要先通过 fopen 函数打开文件,并将返回的文件指针赋值给 fp

文件的打开与关闭

文件打开函数 fopen

fopen 函数用于打开一个文件,其原型为:

FILE *fopen(const char *filename, const char *mode);
  • filename:要打开的文件名,可以包含路径。如果只写文件名,默认在当前工作目录下查找文件。
  • mode:文件打开模式,它决定了文件的打开方式(读、写、追加等)以及文件类型(文本文件或二进制文件)。常见的打开模式有:
    • "r":以只读方式打开文本文件。文件必须存在,否则 fopen 返回 NULL
    • "w":以写入方式打开文本文件。如果文件不存在则创建文件;如果文件已存在,则清空文件内容。
    • "a":以追加方式打开文本文件。如果文件不存在则创建文件;如果文件已存在,则在文件末尾追加数据。
    • "rb":以只读方式打开二进制文件。
    • "wb":以写入方式打开二进制文件。行为与 "w" 类似,但用于二进制文件。
    • "ab":以追加方式打开二进制文件。行为与 "a" 类似,但用于二进制文件。
    • "r+":以读写方式打开文本文件。文件必须存在。
    • "w+":以读写方式打开文本文件。如果文件不存在则创建文件;如果文件已存在,则清空文件内容。
    • "a+":以读写追加方式打开文本文件。如果文件不存在则创建文件;如果文件已存在,则在文件末尾追加数据,并且可以从文件开头读取数据。
    • "rb+""wb+""ab+":分别对应二进制文件的读写、写读、追加读写模式。

例如,打开一个名为 test.txt 的文本文件用于读取:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    // 在这里可以对文件进行读取操作
    fclose(fp);
    return 0;
}

文件关闭函数 fclose

fclose 函数用于关闭一个已打开的文件,其原型为:

int fclose(FILE *stream);

其中 stream 是要关闭的文件指针。当文件操作完成后,一定要调用 fclose 函数关闭文件,这样做有两个重要原因:

  1. 释放系统资源:操作系统为每个打开的文件分配了一定的系统资源(如文件描述符等),关闭文件可以释放这些资源,避免资源泄漏。
  2. 确保数据完整性:在对文件进行写入操作时,数据通常先被写入到缓冲区中,而不是直接写入到磁盘文件。当调用 fclose 时,缓冲区中的数据会被刷新到磁盘文件,从而确保数据的完整性。

fclose 函数返回一个整数值,成功关闭文件时返回 0,否则返回 EOF(在 <stdio.h> 中定义,通常为 -1)。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    // 写入一些数据到文件
    fprintf(fp, "这是写入的内容\n");
    int result = fclose(fp);
    if (result == 0) {
        printf("文件成功关闭\n");
    } else {
        printf("文件关闭失败\n");
    }
    return 0;
}

文本文件的输入输出

字符输入输出函数 fgetc 和 fputc

  1. fgetc 函数fgetc 函数用于从指定的文件中读取一个字符,其原型为:
int fgetc(FILE *stream);

它从 stream 所指向的文件中读取一个字符,并将文件位置指针向后移动一个字节。如果读取成功,返回读取到的字符(转换为 int 类型);如果到达文件末尾或发生错误,返回 EOF。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    int ch;
    while ((ch = fgetc(fp)) != EOF) {
        printf("%c", ch);
    }
    fclose(fp);
    return 0;
}
  1. fputc 函数fputc 函数用于向指定的文件中写入一个字符,其原型为:
int fputc(int c, FILE *stream);

它将字符 c(先转换为 unsigned char 类型)写入到 stream 所指向的文件中,并将文件位置指针向后移动一个字节。如果写入成功,返回写入的字符;如果发生错误,返回 EOF。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    char str[] = "Hello, World!";
    for (int i = 0; str[i] != '\0'; i++) {
        fputc(str[i], fp);
    }
    fclose(fp);
    return 0;
}

字符串输入输出函数 fgets 和 fputs

  1. fgets 函数fgets 函数用于从指定的文件中读取一行字符串,其原型为:
char *fgets(char *str, int num, FILE *stream);

它从 stream 所指向的文件中读取最多 num - 1 个字符(以避免缓冲区溢出),并将它们存储到 str 指向的字符数组中。如果在读取 num - 1 个字符之前遇到换行符 '\n' 或文件末尾,读取操作结束。读取到的字符串会以 '\0' 结尾。如果读取成功,返回 str;如果到达文件末尾且没有读取到任何字符,返回 NULL;如果发生错误,也返回 NULL。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    char buffer[100];
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    while (fgets(buffer, 100, fp) != NULL) {
        printf("%s", buffer);
    }
    fclose(fp);
    return 0;
}
  1. fputs 函数fputs 函数用于向指定的文件中写入一个字符串,其原型为:
int fputs(const char *str, FILE *stream);

它将 str 指向的字符串写入到 stream 所指向的文件中,但不写入字符串结束符 '\0'。如果写入成功,返回一个非负整数;如果发生错误,返回 EOF。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    char str[] = "这是要写入的字符串\n";
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    fputs(str, fp);
    fclose(fp);
    return 0;
}

格式化输入输出函数 fprintf 和 fscanf

  1. fprintf 函数fprintf 函数用于向指定的文件中按照指定的格式输出数据,其原型为:
int fprintf(FILE *stream, const char *format, ...);

它的用法与 printf 函数类似,只是输出目标是文件而不是标准输出设备(屏幕)。stream 是要写入的文件指针,format 是格式控制字符串,后面是可变参数列表,根据格式控制字符串进行格式化输出。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    int num = 123;
    float f = 3.14;
    char str[] = "Hello";
    fp = fopen("test.txt", "w");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    fprintf(fp, "整数: %d, 浮点数: %f, 字符串: %s\n", num, f, str);
    fclose(fp);
    return 0;
}
  1. fscanf 函数fscanf 函数用于从指定的文件中按照指定的格式读取数据,其原型为:
int fscanf(FILE *stream, const char *format, ...);

它的用法与 scanf 函数类似,只是输入源是文件而不是标准输入设备(键盘)。stream 是要读取的文件指针,format 是格式控制字符串,后面是可变参数列表,用于存储读取到的数据。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    int num;
    float f;
    char str[20];
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    fscanf(fp, "整数: %d, 浮点数: %f, 字符串: %s", &num, &f, str);
    printf("读取的整数: %d, 浮点数: %f, 字符串: %s\n", num, f, str);
    fclose(fp);
    return 0;
}

二进制文件的输入输出

数据块输入输出函数 fread 和 fwrite

  1. fread 函数fread 函数用于从指定的文件中读取数据块,其原型为:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

它从 stream 所指向的文件中读取 count 个大小为 size 字节的数据块,并将它们存储到 ptr 指向的内存区域。返回实际成功读取的数据块数量。如果发生错误或到达文件末尾,返回的数量可能小于 count。例如,读取一个包含整数数组的二进制文件:

#include <stdio.h>
int main() {
    FILE *fp;
    int arr[5];
    fp = fopen("data.bin", "rb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    size_t result = fread(arr, sizeof(int), 5, fp);
    if (result != 5) {
        printf("读取数据块数量不足\n");
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    fclose(fp);
    return 0;
}
  1. fwrite 函数fwrite 函数用于向指定的文件中写入数据块,其原型为:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

它将 ptr 指向的内存区域中 count 个大小为 size 字节的数据块写入到 stream 所指向的文件中。返回实际成功写入的数据块数量。例如,将一个整数数组写入二进制文件:

#include <stdio.h>
int main() {
    FILE *fp;
    int arr[5] = {1, 2, 3, 4, 5};
    fp = fopen("data.bin", "wb");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    size_t result = fwrite(arr, sizeof(int), 5, fp);
    if (result != 5) {
        printf("写入数据块数量不足\n");
    }
    fclose(fp);
    return 0;
}

随机访问函数 fseek、ftell 和 rewind

  1. fseek 函数fseek 函数用于移动文件位置指针到指定的位置,其原型为:
int fseek(FILE *stream, long offset, int whence);
  • stream:要操作的文件指针。
  • offset:偏移量,以字节为单位。可以是正数(向后移动)、负数(向前移动)或 0(不移动)。
  • whence:起始位置,取值可以是 SEEK_SET(文件开头)、SEEK_CUR(当前位置)或 SEEK_END(文件末尾)。

例如,将文件位置指针移动到文件开头往后 10 个字节的位置:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    fseek(fp, 10, SEEK_SET);
    // 在这里可以从新的位置进行读取操作
    fclose(fp);
    return 0;
}
  1. ftell 函数ftell 函数用于获取文件位置指针当前的位置,其原型为:
long ftell(FILE *stream);

它返回从文件开头到当前文件位置指针的字节数。如果发生错误,返回 -1L。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    long pos = ftell(fp);
    printf("当前文件位置指针位置: %ld\n", pos);
    fclose(fp);
    return 0;
}
  1. rewind 函数rewind 函数用于将文件位置指针移动到文件开头,其原型为:
void rewind(FILE *stream);

它等价于 fseek(stream, 0, SEEK_SET)。例如:

#include <stdio.h>
int main() {
    FILE *fp;
    fp = fopen("test.txt", "r");
    if (fp == NULL) {
        printf("无法打开文件\n");
        return 1;
    }
    // 进行一些读取操作后
    rewind(fp);
    // 可以从文件开头重新读取
    fclose(fp);
    return 0;
}

文件操作中的错误处理

在进行文件操作时,可能会发生各种错误,如文件不存在、磁盘空间不足、权限问题等。因此,进行适当的错误处理是非常重要的。

  1. 检查 fopen 的返回值:在调用 fopen 函数打开文件后,应立即检查其返回值是否为 NULL。如果为 NULL,说明文件打开失败,需要采取相应的处理措施,如提示用户错误信息并退出程序。
FILE *fp;
fp = fopen("nonexistent.txt", "r");
if (fp == NULL) {
    perror("无法打开文件");
    return 1;
}

这里使用 perror 函数输出系统错误信息,它会根据 errno(在 <errno.h> 中定义)输出相应的错误描述。

  1. 检查其他文件操作函数的返回值:对于 fgetcfputcfreadfwrite 等函数,也要检查它们的返回值。例如,fgetc 返回 EOF 可能表示到达文件末尾或发生错误,需要进一步通过 ferror 函数来判断是哪种情况。
int ch;
while ((ch = fgetc(fp)) != EOF) {
    // 处理读取到的字符
}
if (ferror(fp)) {
    perror("读取文件时发生错误");
}

ferror 函数用于检查文件流是否发生错误,如果发生错误返回非零值,否则返回 0。

  1. 使用 clearerr 函数清除错误标志:当文件操作发生错误后,文件流的错误标志会被设置。如果希望在错误处理后继续进行文件操作,可以使用 clearerr 函数清除错误标志。
clearerr(fp);

这样可以使后续的文件操作正常进行,前提是错误原因已经被解决。

通过以上对 C 语言文件输入输出的深入解析,希望读者能够全面掌握文件操作的各种技术和要点,在实际编程中能够灵活、高效且正确地处理文件相关的任务。无论是文本文件还是二进制文件,不同的输入输出函数和操作方式都有其适用场景,根据具体需求选择合适的方法是实现高质量文件处理程序的关键。同时,注意文件操作中的错误处理,确保程序的健壮性和稳定性。