C语言——文件操作

C语言——文件操作_第1张图片

(本图系AI生成)

0.前言

在计算机编程中,文件操作是一项重要的任务。通过文件操作,我们可以读取和写入数据,实现数据的持久化存储。C语言提供了丰富的文件操作函数,使得处理文件变得相对简单。本篇博客将介绍C语言中的文件操作,包括文件的基本概念、打开和关闭文件、顺序读写和随机读写等内容。

1.为什么使用文件

文件在计算机编程中扮演着关键的角色,尤其是在C语言这样的系统级编程语言中。

  1. 文件提供了一种持久化存储数据的方式。不同于内存中的数据在程序关闭时会丢失,文件中的数据可以长期保存,即使在程序不运行的时候也能保持不变。这对于需要长期存储用户数据、应用配置或日志信息的应用程序来说至关重要。
  2. 文件允许数据共享和交换。通过文件,不同的程序可以交换数据,无论这些程序是同时运行还是在不同的时间运行。例如,一个程序可以生成一个数据文件,供另一个程序稍后读取和处理。这种灵活性使得文件成为应用之间通信的重要手段。
  3. 文件操作提供了数据管理的灵活性。程序可以按需读取和写入文件的不同部分,支持随机访问或顺序访问。这种灵活性对于处理大量数据或需要高效读写操作的应用尤其重要。
  4. 文件系统提供了组织和管理数据的结构化方式。通过目录和文件名,数据可以被有序地存储和检索。这种结构化方法使得管理复杂数据集变得更加容易,对于开发者和用户都是有益的。

2.什么是文件

在计算机编程和操作系统的背景下,文件是一个存储在持久化存储设备上的数据集合。文件以一种结构化的方式存储数据,这些数据可以是文本、图像、音频、视频或其他任何形式。在C语言编程中,理解不同类型的文件及其用途是非常重要的。

2.1程序文件

程序文件包含了计算机程序的源代码或可执行代码。源代码文件是程序员编写的、用于描述程序如何工作的文本文件,通常包含有C、C++、Python等编程语言编写的代码。源代码文件需要通过编译器转换为可执行文件,计算机才能执行这些指令。可执行文件是编译后的产物,包含了计算机可以直接执行的二进制代码。这些文件通常具有特定的文件扩展名,如在Windows系统中是.exe

2.2数据文件

数据文件用于存储非代码信息,如文本数据、图像、视频等。这类文件通常用于保存程序运行时产生的数据或程序需要处理的数据。例如,一个文本编辑器可能会保存用户输入的文本到文本文件中;图像处理软件可能会读取和写入图像文件。数据文件的格式多种多样,包括文本格式(如.txt, .csv),图像格式(如.jpg, .png),音视频格式(如.mp3, .mp4)等。

2.3文件名

文件名是用于标识文件的字符串。在大多数操作系统中,文件名由文件的基本名称和一个扩展名组成,两者通常由一个点.分隔。扩展名指示文件的类型,帮助操作系统和用户识别如何处理该文件。例如,.txt通常表示文本文件,.jpg表示JPEG图像文件。文件名不仅帮助用户识别文件内容,还允许操作系统使用关联的程序打开不同类型的文件。

另注意:在Windows系统中,文件名的格式一般遵循以下结构:

  1. 文件路径: 指明文件在文件系统中的位置。路径可以是绝对路径或相对路径。绝对路径从根目录开始,完整地描述了文件的存储位置,例如 C:\Users\Example\Documents\file.txt。相对路径则是相对于当前工作目录的路径,例如 Documents\file.txt

  2. 文件名主干: 这是文件的主要识别名,用于区分文件系统中的不同文件。例如,在 file.txt 中,file 就是文件名主干。

  3. 文件后缀: 也称为文件扩展名,通常以点.开始,用于指明文件的类型,这对操作系统和应用程序识别如何打开该文件至关重要。例如,.txt 表示文本文件,.exe 表示可执行文件。

将这三部分结合起来,就构成了Windows系统中的完整文件名。例如,C:\Users\Example\Documents\file.txt 中,C:\Users\Example\Documents\ 是文件路径,file 是文件名主干,.txt 是文件后缀。

3.二进制文件和文本文件

在计算机系统中,所有文件最终都以二进制形式存储,但基于它们的使用和内容的不同,文件可以分为二进制文件和文本文件两大类。理解二进制文件和文本文件的区别,可以通过考虑如何在这两种类型的文件中存储同一个值,例如数字“114514”。我们将分别探讨这个数字在二进制文件和文本文件中的存储方式。

3.1. 二进制文件中的存储

在二进制文件中,数字“114514”被直接转换为其二进制形式。这个过程涉及将数字转换成计算机内部处理的二进制数。例如,十进制数“114514”在二进制中表示为 0001 1011 1100 0001 0010(以32位整数为例)。

这种存储方式是直接的,没有任何多余的转换或格式化。在二进制文件中,这个二进制数将直接存储,通常无法被人类直接阅读。这种格式适用于需要高效存储和处理数字数据的情景,如科学计算、图像处理等。

3.2. 文本文件中的存储

在文本文件中,数字“114514”则以字符序列的形式存储。这意味着每个数字字符('1'、'1'、'4'、'5'、'1'、'4')将转换为其对应的ASCII码。例如,字符'1'的ASCII码是49,'4'是52,'5'是53。因此,在文本文件中,"114514"将被存储为ASCII码序列 49 49 52 53 49 52

这种存储方式使得文件内容对人类是可读的,因为它使用标准的字符编码。文本文件通常用于存储需要人类阅读或编辑的数据,如文本文档、源代码等。

总的来说,二进制文件和文本文件在存储相同的数据时采用了不同的方法。二进制文件直接存储数据的二进制表示,适合机器处理和高效存储;而文本文件则存储数据的字符表示,适合人类阅读和编辑。理解这两种文件类型的区别对于选择正确的数据存储和处理方式至关重要。

4.文件的打开和关闭

4.1流和标准流

4.1.1流

由于“流”是一个抽象的概念,我们用较长的篇幅来介绍它。

在C语言中,理解流(Stream)的概念就像理解水流在河床中的运动。想象一下,河流是水的通道,它允许水流从一个地方顺畅地流向另一个地方。在C语言中,流的概念与此类似,但这里的“水”是数据,而“河流”则是数据流动的通道。

当我们谈论流时,我们实际上是在谈论一种连续的、顺序的数据传输方式。流提供了一个抽象层,使得我们可以轻松地读取来自文件、键盘、网络等来源的数据,或者写入数据到文件、显示器、网络等目的地。正如河流掩盖了水流的复杂性一样,流隐藏了数据处理的底层细节,让程序员可以不必担心数据是如何在底层存储或传输的。

在C语言中使用流,就像在河床中引导水流。你不需要知道水是如何从山上流下来,或者它将如何流入大海;你只需要知道如何控制水流的方向。同样,在使用流读写数据时,你不需要关心数据在计算机内部是如何被处理的;你只需要知道如何从流中读取数据或向流中写入数据。

总的来说,流在C语言中扮演的角色就像是数据流动的河道,提供了一种简单、直观且高效的方式来处理数据。它使得数据读写变得像是在河流中顺水推舟一样自然和简单。

4.1.2标准流

C语言标准定义了几个预设的标准流对象,它们自动被程序的执行环境创建,无需手动打开。这些标准流包括:

  • stdin:标准输入流,通常指键盘输入。
  • stdout:标准输出流,用于向屏幕输出数据。
  • stderr:标准错误流,用于输出错误消息,通常也是指向屏幕。

4.2文件指针

在C语言中,文件指针是用于访问文件的指针变量。当一个文件被打开时,会返回一个指向FILE类型的指针。这个指针用于后续的读写操作,以标识和管理打开的文件。

文件指针是指向FILE类型结构的指针。FILE是C标准库中定义的一个结构体,它包含了关于文件的所有信息,如文件的位置、状态和当前的读写位置等。文件指针是这个结构体的一个引用,通过它可以访问和管理文件。

4.3文件的打开和关闭

  • 文件的打开:使用fopen函数打开一个文件。这个函数需要文件的路径和打开模式(如只读、只写、读写等)作为参数,并返回一个文件指针。如果打开失败,比如因为文件不存在或没有权限,fopen将返回NULL。

    示例:FILE *fp = fopen("example.txt", "r");(以只读方式打开文件)

  • 文件的关闭:打开的文件应该在完成操作后关闭,以释放资源。使用fclose函数关闭文件。这个函数接受一个文件指针作为参数,并在成功关闭文件时返回零。

    示例:fclose(fp);(关闭文件)

5.文件的顺序读写

在C语言中,文件的顺序读写是按照数据在文件中的顺序进行读写操作。这涉及到一系列函数,每个函数都有其特定的用途。

5.1顺序读写函数介绍

1. fgetcfputc

  • fgetc 用于从文件中读取一个字符。
  • fputc 用于将一个字符写入文件。
FILE *fp;
int ch;

fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

ch = fgetc(fp);
if (ch == EOF) {
    perror("Error reading file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

fp = fopen("output.txt", "w");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fputc(ch, fp) == EOF) {
    perror("Error writing file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

2. fgetsfputs

  • fgets 用于从文件中读取一行字符串。
  • fputs 用于将一个字符串写入文件。
FILE *fp;
char buffer[100];

fp = fopen("input.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fgets(buffer, sizeof(buffer), fp) == NULL) {
    perror("Error reading file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

fp = fopen("output.txt", "w");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fputs(buffer, fp) == EOF) {
    perror("Error writing file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

3. fscanffprintf

  • fscanf 用于从文件中按照指定格式读取数据。
  • fprintf 用于按照指定格式将数据写入文件。
FILE *fp;
int num;

fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fscanf(fp, "%d", &num) != 1) {
    perror("Error reading file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

fp = fopen("output.txt", "w");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fprintf(fp, "The number is: %d", num) < 0) {
    perror("Error writing file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

4. freadfwrite

  • fread 用于从文件中读取二进制数据块。
  • fwrite 用于将二进制数据块写入文件。
FILE *fp;
struct Person {
    char name[50];
    int age;
};

struct Person person;

fp = fopen("data.bin", "rb");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fread(&person, sizeof(struct Person), 1, fp) != 1) {
    perror("Error reading file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

fp = fopen("output.bin", "wb");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fwrite(&person, sizeof(struct Person), 1, fp) != 1) {
    perror("Error writing file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

5.2一组函数的对比

在文件的顺序读写中,scanf/fscanf/sscanfprintf/fprintf/sprintf 这两组函数在功能上各有不同,主要体现在以下几个方面:

scanf/fscanf/sscanf
  1. 数据来源:

    • scanf 从标准输入中读取数据。
    • fscanf 从文件中按照指定格式读取数据。
    • sscanf 从字符串中按照指定格式读取数据。
  2. 格式化输入:

    • scanf 通过键盘输入,通常用于从用户获取数据。
    • fscanf 从文件中按照指定格式读取数据,适用于从文件获取数据。
    • sscanf 从字符串中按照指定格式读取数据,用于将字符串解析为变量值。
  3. 错误处理:

    • scanf 需要处理用户输入的错误,比如输入的数据类型与格式不匹配。
    • fscanf 需要考虑文件可能不存在或文件格式与读取格式不匹配的错误。
    • sscanf 需要处理字符串格式不符合预期的错误。
int num;
if (scanf("%d", &num) != 1) {
    perror("Error reading from standard input");
    exit(EXIT_FAILURE);
}

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fscanf(fp, "%d", &num) != 1) {
    perror("Error reading from file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

char str[] = "123";
if (sscanf(str, "%d", &num) != 1) {
    perror("Error reading from string");
    exit(EXIT_FAILURE);
}
printf/fprintf/sprintf
  1. 数据输出:

    • printf 将数据输出到标准输出,通常用于向屏幕打印信息。
    • fprintf 将数据按照指定格式输出到文件,适用于将数据写入文件。
    • sprintf 将数据按照指定格式输出到字符串,用于将数据格式化为字符串。
  2. 格式化输出:

    • printf 主要用于在屏幕上显示信息,可根据需要进行格式化。
    • fprintf 用于将格式化后的数据写入文件,格式控制更为灵活。
    • sprintf 将格式化后的数据写入字符串,适用于需要字符串作为输出的场景。
  3. 错误处理:

    • printf 主要涉及到输出到屏幕,一般情况下不容易出现错误。
    • fprintf 需要考虑文件写入可能失败的情况,需要适当的错误处理。
    • sprintf 需要处理字符串缓冲区溢出等可能导致错误的情况。
int num = 42;
if (printf("Number: %d", num) < 0) {
    perror("Error writing to standard output");
    exit(EXIT_FAILURE);
}

FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

if (fprintf(fp, "Number: %d", num) < 0) {
    perror("Error writing to file");
    fclose(fp);
    exit(EXIT_FAILURE);
}

fclose(fp);

char buffer[50];
if (sprintf(buffer, "Number: %d", num) < 0) {
    perror("Error writing to string");
    exit(EXIT_FAILURE);
}

6.文件的随机读写

在C语言中,文件的随机读写允许程序直接访问文件中的任意位置,而不仅仅是从文件的开头到结尾的顺序读写。这对于在文件中定位特定数据或进行精确的文件操作非常重要。

6.1 fseek

fseek 函数用于设置文件指针的位置,以便在文件中进行定位。它允许将文件指针设置到文件的任意位置,从而实现随机读写。

int fseek(FILE *stream, long offset, int whence);
  • stream:文件指针,指向要进行定位的文件。
  • offset:位移的字节数,可以是正数(正向移动)或负数(反向移动)。
  • whence:起始位置,可以取 SEEK_SET(文件开头)、SEEK_CUR(当前位置)、SEEK_END(文件末尾)。
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

// 将文件指针定位到文件的第50字节处
if (fseek(fp, 50, SEEK_SET) != 0) {
    perror("Error setting file pointer");
    fclose(fp);
    exit(EXIT_FAILURE);
}

// 现在可以在这个位置进行读取操作
// ...

fclose(fp);

6.2 ftell

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

long ftell(FILE *stream);
  • stream:文件指针,指向要获取位置的文件。
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

// 获取文件指针当前位置
long position = ftell(fp);
if (position == -1) {
    perror("Error getting file position");
    fclose(fp);
    exit(EXIT_FAILURE);
}

// 输出当前位置
printf("Current position: %ld\n", position);

fclose(fp);

6.3 rewind

rewind 函数用于将文件指针重新设置到文件的开头。

void rewind(FILE *stream);
  • stream:文件指针,指向要重新设置的文件。
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

// 在进行文件读取之前,将文件指针重新设置到开头
rewind(fp);

// 现在可以从文件开头进行读取操作
// ...

fclose(fp);

7.文件读取结束的判定

在C语言中,判断文件读取是否结束是关键的,但是需要注意避免使用 feof 函数。原因如下:

feof 函数在文件遇到文件结束符时返回非零值,否则返回零。然而,feof 只能在读取操作发生后才能确定文件是否已经结束,而无法预测下一次读取是否会触发文件结束。因此,使用 feof 可能导致在文件尾部发生有效读取的情况下错误地判定文件结束,从而引发逻辑错误。

下面是使用其他方法进行文本文件和二进制文件读取结束的判定:

7.1. 文本文件的读取结束判定

7.1.1 fgetc 判断是否为 EOF
FILE *fp = fopen("textfile.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

int ch;
while ((ch = fgetc(fp)) != EOF) {
    // 处理读取到的字符
}

if (ferror(fp)) {
    perror("Error reading file");
}

fclose(fp);

在这个例子中,fgetc 函数返回的是一个字符,而 EOF 是一个文件结束的标志。循环在检测到文件结束时退出。使用 ferror 函数来检查是否有读取错误。

7.1.2 fgets 判断返回值是否为 NULL
FILE *fp = fopen("textfile.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

char buffer[100];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
    // 处理读取到的一行字符串
}

if (ferror(fp)) {
    perror("Error reading file");
}

fclose(fp);

fgets 函数在读取一行字符串时,如果到达文件末尾,则返回 NULL。因此,可以通过检查返回值是否为 NULL 来判断文件读取是否结束。同样使用 ferror 函数来检查是否有读取错误。

7.2. 二进制文件的读取结束判定

7.2.1 fread 判断返回值是否小于实际要读的个数
FILE *fp = fopen("binaryfile.bin", "rb");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

struct Data {
    // 定义结构体或其他数据类型
};

struct Data data;
size_t elements_read;

while ((elements_read = fread(&data, sizeof(struct Data), 1, fp)) == 1) {
    // 处理读取到的数据
}

if (ferror(fp)) {
    perror("Error reading file");
}

fclose(fp);

fread 函数返回实际读取的元素个数。在这个例子中,如果 elements_read 小于预期的 1,就说明已经到达文件末尾,因为无法再读取足够的元素。同样使用 ferror 函数来检查是否有读取错误。 

8.文件缓冲区

在C语言中,文件缓冲区是用于暂存数据的内存区域,用于提高文件的读写效率。文件I/O操作通常涉及大量数据的传输,为了避免频繁的磁盘访问,系统使用文件缓冲区进行数据的暂存和批量传输。

8.1 输出缓冲区

对于输出缓冲区,当使用 printffprintf 等输出函数时,数据首先被写入到输出缓冲区,而不是直接写入文件。输出缓冲区会在满、程序结束、或者使用 fflush 函数时被刷新,将数据写入到文件。

FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

// 数据先写入输出缓冲区
fprintf(fp, "Hello, File I/O!");

// 手动刷新输出缓冲区,将数据写入文件
fflush(fp);

fclose(fp);

8.2 输入缓冲区

对于输入缓冲区,当使用 fgetcfgetsfscanf 等读取函数时,数据首先被读取到输入缓冲区,而不是直接从文件中读取。输入缓冲区会在满、遇到换行符、或者使用 fflush 函数时被刷新。

FILE *fp = fopen("input.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

// 从输入缓冲区读取一个字符
int ch = fgetc(fp);

fclose(fp);

8.3 自定义缓冲区

我们也可以使用 setbuf 函数或 setvbuf 函数来自定义缓冲区,以满足特定的需求。

#include 

int main() {
    FILE *fp = fopen("custom_buffer.txt", "w");
    if (fp == NULL) {
        perror("Error opening file");
        exit(EXIT_FAILURE);
    }

    char buffer[BUFSIZ];
    setvbuf(fp, buffer, _IOFBF, BUFSIZ);

    // 使用自定义缓冲区进行文件写入操作
    fprintf(fp, "Custom Buffer Example");

    fclose(fp);

    return 0;
}

在这个例子中,setvbuf 函数设置了自定义的缓冲区,将其与文件关联起来。这允许我们在内存中使用自定义大小的缓冲区来提高文件写入效率。

9.结语

文件操作是C语言中不可或缺的一部分,通过学习文件的打开、读写、定位等基本操作,我们能够更灵活地处理数据,实现数据的持久化存储。掌握文件操作不仅有助于提高程序的效率,还为处理各种实际问题提供了便利。在编写C程序时,合理使用文件操作函数,注意错误处理,可以使程序更加稳健、可靠。希望本文介绍的文件操作知识能够帮助你更好地应用C语言进行编程。

你可能感兴趣的:(C语言基础知识,c语言,开发语言)