《C语言深度解剖》(16):C语言的文件读写操作

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨


目录

[1. 为什么使用文件](#1. 为什么使用文件)

[2. 文件概述](#2. 文件概述)

[2.1 什么是流](#2.1 什么是流)

[2.2 什么是文件](#2.2 什么是文件)

[2.3 文件名](#2.3 文件名)

[3. 文件的打开和关闭](#3. 文件的打开和关闭)

[3.1 文件指针](#3.1 文件指针)

[3.2 文件的打开和关闭](#3.2 文件的打开和关闭)

[4. 文件的顺序读写](#4. 文件的顺序读写)

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

[5.1 fssek](#5.1 fssek)

[5.2 ftell](#5.2 ftell)

[5.3 rewind](#5.3 rewind)

[6. 文本文件和二进制文件](#6. 文本文件和二进制文件)

[7. 文件读取结束的判定](#7. 文件读取结束的判定)

[7.1 文件的分类](#7.1 文件的分类)

[7.2 被错误使用的feof](#7.2 被错误使用的feof)

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


1. 为什么使用文件

程序执行起来后,称之为进程,进程运行过程中的数据,均在内存中,当我们需要把运算后的数据存储下来时,就需要文件。 做到数据的持久化。

2. 文件概述

2.1 什么是流

在C语言中,流是一个用于输入输出操作的抽象概念。它代表了一个数据的流动,从源头(比如键盘、文件、网络等)到目的地(比如屏幕、文件、网络等)。流可以看作是一个数据的序列,它按照一定的顺序被读取或写入。

C语言的标准库提供了一套文件输入输出函数,这些函数可以对流进行操作。在C中,所有的输入输出操作都是通过流来完成的。标准输入输出库 <stdio.h> 定义了流的概念,并提供了相关的函数。

以下是C语言中几个重要的流:

  • stdin - 标准输入流,通常指键盘输入。
  • stdout - 标准输出流,通常指屏幕输出。
  • stderr - 标准错误流,用于输出错误信息,通常也是输出到屏幕,但与stdout分离,以便于错误信息不会与普通输出混淆。

流在C语言中是通过文件指针来引用的,文件指针是一个指向FILE结构的指针,FILE结构包含了流的状态信息。例如,当你打开一个文件时,C语言的fopen函数会返回一个FILE指针,通过这个指针,你可以对文件进行读或写操作。

cpp 复制代码
FILE* fp; // 声明一个文件指针
fp = fopen("example.txt", "r"); // 打开一个文件,返回一个指向该文件的流
if (fp == NULL) {
    // 错误处理
}
// ... 使用fp进行文件操作
fclose(fp); // 关闭流

2.2 什么是文件

文件:文件指存储在外部介质(如磁盘磁带)上数据的集合。操作系统(windows,Linux, Mac 均是)是以文件为单位对数据进行管理的。

在C语言中,所有的设备都被视为文件,它们的输入输出操作都可以通过文件流来进行。这使得C语言的输入输出模型非常统一和灵活。

磁盘上的文件是文件。 但是在程序设计中 ,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。

  • 程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境 后缀为.exe)。

  • 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件, 或者输出内容的文件。

本章讨论的是数据文件。

在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理 的就是磁盘上文件。

2.3 文件名


3. 文件的打开和关闭

3.1 文件指针

缓冲文件系统中,关键的概念是**"文件类型指针"** ,简称**"文件指针"**。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名 字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统 声明的,取名FILE.

打开文件后我们得到 FILE*类型的文件指针,通过该文件指针对文件进行操作,FILE 是 一个结构体类型,那么首先让我们来看下它里边都有什么呢?

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息, 使用者不必关心细节。

一般都是通过一个 FILE 的指针来维护 FILE 结构的变量。

下面我们可以创建一个FILE*的指针变量:

cpp 复制代码
FILE* pf;//文件指针变量

定义 pf 是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。

比如:

3.2 文件的打开和关闭

文件在读写之前应该先打开文件 ,在使用结束之后应该关闭文件

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSIC 规定使用fopen数来打开文件fclose来关闭文件

  • 当无法打开文件时,fopen函数会返回空指针。这可能是因为文件不存在,也可能是因为文件的位置不对,还可能是因为我们没有打开文件的权限。
  • 函数 fclose()关闭给出的文件流, 释放已关联到流的所有缓冲区。fclose()执行成功 时返回 0,否则返回 EOF.

打开方式如下:

实例代码:

要区分标准的输入输出,和文件操作中的输入输出


4. 文件的顺序读写

上述简单了解需要用的时候查看一下即可,下面我会为上述每个函数举一个例子:

  • fgetc
cpp 复制代码
int fgetc(FILE* stream);

在这个例子中,我们使用 fgetc 读取名为 "example.txt" 的文件,并将文件内容打印到标准输出。我们检查每次 fgetc 的调用是否返回 EOF 来判断是否到达文件末尾。同时,我们使用 ferror 函数来检查文件流是否发生了错误。最后,我们关闭文件流以释放资源。

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

int main() {
    int ch;
    FILE* file = fopen("example.txt", "r"); // 打开文件进行读取

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 读取并打印文件内容,直到文件结束
    while ((ch = fgetc(file)) != EOF) {
        putchar(ch); // 将读取到的字符打印到标准输出
    }

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

    fclose(file); // 关闭文件
    return 0;
}
  • fputc
cpp 复制代码
int fputc(int ch, FILE* stream);

在这个例子中,我们使用 fputc 将字符 'A'、'B' 和 'C' 写入名为 "output.txt" 的文件中。然后我们关闭文件流。如果文件打开或写入过程中发生错误,我们会使用 perror 函数来打印错误信息。

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

int main() {
    FILE* file = fopen("output.txt", "w"); // 打开文件进行写入

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 写入字符到文件
    fputc('A', file); // 写入字符 'A'
    fputc('B', file); // 写入字符 'B'
    fputc('C', file); // 写入字符 'C'

    if (fclose(file) != 0) { // 检查文件是否成功关闭
        perror("Error closing file");
    }

    return 0;
}
  • fputs
cpp 复制代码
int fputs(const char* str, FILE* stream);

fputs 不会自动在字符串的末尾添加换行符(\n),如果需要添加换行符,必须在字符串中包含换行符或者在调用 fputs 后显式地写入换行符。

在这个例子中,我们使用 fputs 将字符串 "Hello, World!" 写入名为 "output.txt" 的文件中。然后我们写入一个换行符,并关闭文件流。如果文件打开或写入过程中发生错误,我们会使用 perror 函数来打印错误信息。

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

int main() {
    FILE* file = fopen("output.txt", "w"); // 打开文件进行写入

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 写入字符串到文件
    fputs("Hello, World!", file); // 写入字符串 "Hello, World!"

    // 写入换行符
    fputc('\n', file);

    if (fclose(file) != 0) { // 检查文件是否成功关闭
        perror("Error closing file");
    }

    return 0;
}
  • fscanf
cpp 复制代码
int fscanf(FILE* stream, const char* format, ...);

在这个例子中,我们使用 fscanf 从名为 "data.txt" 的文件中读取一个整数和一个浮点数。我们检查 fscanf 的返回值来确定是否成功读取了两个数据项。如果文件打开或读取过程中发生错误,我们会使用 perror 函数来打印错误信息。最后,我们关闭文件流以释放资源。

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

int main() {
    FILE* file = fopen("data.txt", "r"); // 假设文件包含整数和浮点数
    int num;
    float value;

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 从文件中读取整数和浮点数
    if (fscanf(file, "%d %f", &num, &value) == 2) {
        printf("Read: num = %d, value = %f\n", num, value);
    }
    else {
        printf("Failed to read data\n");
    }

    fclose(file); // 关闭文件
    return 0;
}
  • fprintf
cpp 复制代码
int fprintf(FILE* stream, const char* format, ...);

在这个例子中,我们使用 fprintf 将整数 42 和浮点数 3.14 写入名为 "output.txt" 的文件中,并且使用格式字符串来指定数据的输出格式。然后我们关闭文件流。如果文件打开或写入过程中发生错误,我们会使用 perror 函数来打印错误信息。

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

int main() {
    FILE* file = fopen("output.txt", "w"); // 打开文件进行写入

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 写入格式化的数据到文件
    int num = 42;
    float value = 3.14f;
    fprintf(file, "The number is %d and the value is %f\n", num, value);

    if (fclose(file) != 0) { // 检查文件是否成功关闭
        perror("Error closing file");
    }

    return 0;
}
  • fread
cpp 复制代码
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
  • ptr:指向要读取数据的内存块的指针。
  • size:每个数据项的大小,以字节为单位。
  • nmemb:要读取的数据项的数量。
  • stream:指向 FILE 对象的指针,该对象标识了输入流。这个流可以是标准输入 stdin,也可以是已经打开的文件流。
  • 返回值:fread 返回实际读取的数据项的数量,这个数量可能小于 nmemb 指定的数量,如果到达文件末尾或发生读取错误,则返回值小于 nmemb。

在这个例子中,我们使用 fread 从名为 "data.bin" 的文件中读取5个整数。我们检查 fread 的返回值来确定是否成功读取了5个数据项。如果文件打开或读取过程中发生错误,我们会使用 perror 函数来打印错误信息。最后,我们关闭文件流以释放资源。

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

int main() {
    FILE* file = fopen("data.bin", "rb"); // 打开文件进行二进制读取
    int data[5]; // 假设文件包含5个整数

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 从文件中读取整数
    size_t num_read = fread(data, sizeof(int), 5, file);
    if (num_read != 5) {
        perror("Error reading data");
    }

    fclose(file); // 关闭文件

    // 打印读取到的整数
    for (size_t i = 0; i < num_read; i++) {
        printf("%d ", data[i]);
    }
    printf("\n");

    return 0;
}
  • fwrite
cpp 复制代码
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);

在这个例子中,我们使用 fwrite 将5个整数写入名为 "data.bin" 的文件中。我们检查 fwrite 的返回值来确定是否成功写入了5个数据项。如果文件打开或写入过程中发生错误,我们会使用 perror 函数来打印错误信息。最后,我们关闭文件流以释放资源。

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

int main() {
    FILE *file = fopen("data.bin", "wb"); // 打开文件进行二进制写入
    int data[5] = {1, 2, 3, 4, 5}; // 假设要写入5个整数

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 将整数写入文件
    size_t num_written = fwrite(data, sizeof(int), 5, file);
    if (num_written != 5) {
        perror("Error writing data");
    }

    fclose(file); // 关闭文件

    return 0;
}

对比一组函数:

这两主要讲解一些 sscanfsprintf.

  • sprintf
cpp 复制代码
int sprintf(char *str, const char *format, ...);

在这个例子中,我们使用 sprintf 将整数 42 和浮点数 3.14 格式化为字符串,并写入 buffer 数组中。然后我们打印 buffer 数组中的内容。sprintf 函数返回写入的字符数,这个数可以帮助我们确定字符串的实际长度,并确保 buffer 数组有足够的空间来存储结果。

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

int main() {
    char buffer[100]; // 假设buffer足够大以存储结果
    int num = 42;
    float value = 3.14f;

    // 使用sprintf格式化字符串并写入buffer
    sprintf(buffer, "The number is %d and the value is %f\n", num, value);

    // 打印buffer中的内容
    printf("%s", buffer);

    return 0;
}
  • sscanf
cpp 复制代码
int sscanf(const char *str, const char *format, ...);

在这个例子中,我们使用 sscanf 从字符串 "42 3.14" 中解析一个整数和一个浮点数。我们检查 sscanf 的返回值来确定是否成功解析了两个数据项。如果字符串中没有足够的输入数据,或者格式字符串中的格式规范符与输入数据不匹配,sscanf 可能不会成功匹配所有输入项,或者根本不匹配任何输入项。

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

int main() {
    char input[20];
    int num;
    float value;

    // 假设输入字符串为 "42 3.14"
    strcpy(input, "42 3.14");

    // 从字符串中解析整数和浮点数
    if (sscanf(input, "%d %f", &num, &value) == 2) {
        printf("Parsed: num = %d, value = %f\n", num, value);
    } else {
        printf("Failed to parse data\n");
    }

    return 0;
}

5. 文件的随机读写

5.1 fssek

根据文件指针的位置和偏移量来定位文件指针。

cpp 复制代码
int fseek(FILE *stream, long int offset, int origin);

参数说明:

  • stream:指向 FILE 对象的指针,该对象标识了要设置位置的文件流。
  • offset:要移动的字节数,可以是正数、负数或零。
  • origin:指定移动的起点,可以是以下常量之一:
  • SEEK_SET:从文件流的开始位置开始移动。
  • SEEK_CUR:从文件流的当前位置开始移动。
  • SEEK_END:从文件流的结束位置开始移动。
cpp 复制代码
#include <stdio.h>

int main() {
    FILE *file = fopen("example.txt", "r+"); // 打开文件进行读写操作

    if (file == NULL) {
        perror("Error opening file");
        return 1;
    }

    // 将文件指针移动到文件末尾
    if (fseek(file, 0, SEEK_END) == 0) {
        printf("File pointer moved to end of file\n");
    } else {
        perror("Error moving file pointer");
    }

    fclose(file); // 关闭文件
    return 0;
}

在这个例子中,我们使用 fseek 将文件指针移动到文件末尾。我们检查 fseek 的返回值来确定是否成功移动了文件指针。如果文件打开或移动过程中发生错误,我们会使用 perror 函数来打印错误信息。最后,我们关闭文件流以释放资源。

5.2 ftell

返回文件指针相对于起始位置的偏移量

cpp 复制代码
long int ftell(FILE* stream);
cpp 复制代码
/* ftell example : getting size of a file */
#include <stdio.h>
int main()
{
    FILE* pFile;
    long size;
    pFile = fopen("myfile.txt", "rb");
    if (pFile == NULL) perror("Error opening file");
    else
    {
        fseek(pFile, 0, SEEK_END);   // non-portable
        size = ftell(pFile);
        fclose(pFile);
        printf("Size of myfile.txt: %ld bytes.\n", size);
    }
    return 0;
}

5.3 rewind

让文件指针的位置回到文件的起始位置

cpp 复制代码
void rewind ( FILE * stream );
cpp 复制代码
/* rewind example */
#include <stdio.h>
int main()
{
    int n;
    FILE* pFile;
    char buffer[27];
    pFile = fopen("myfile.txt", "w+");
    for (n = 'A'; n <= 'Z'; n++)
        fputc(n, pFile);
    rewind(pFile);
    fread(buffer, 1, 26, pFile);
    fclose(pFile);
    buffer[26] = '\0';
    puts(buffer);

    return 0;
}

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

根据数据的组织形式,数据文件被称为文本文件 或者二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件 。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件件

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而

二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

测试代码:

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

int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
	fclose(pf);
	pf = NULL;
	return 0;
}

7. 文件读取结束的判定

7.1 文件的分类

  • 从用户观点:

特殊文件(标准输入输出文件或标准设备文件)。

普通文件(磁盘文件)。

  • 从操作系统的角度看,每一个与主机相连的输入、输出设备看作是一个文件。

例:

输入文件:终端键盘

输出文件:显示屏和打印机

  • 按数据的组织形式:

ASCII 文件(文本文件):每一个字节放一个 ASCII 代码。在文本文件(textfile)中,字节表示字符,这使人们可以检查或编辑文件。例如,C程序源代码是存储在文本文件中的。

**二进制文件:**把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放。 在二进制文件(binaryfile)中,字节不一定表示字符;字节组还可以表示其他类型的数据,比如整数和浮点数。如果试图查看可执行C程序的内容,你会立刻意识到它是存储在二进制文件中的。

7.2 被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。

而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

正确的使用:

文本文件的例子:

二进制文件的例子:

8. 文件缓冲区

C语言对文件的处理方法:

缓冲文件系统:

非缓冲文件系统:

系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。用非缓冲文件系统进行的输入输出又称为低级输入输出系统。

cpp 复制代码
int fflush(FILE *stream);

void setbuf(FILE *stream, char *buffer);

int setvbuf(FILE *stream, char *buffer, int mode, size_t size);

向磁盘驱动器传入数据或者从磁盘驱动器传出数据都是相对较慢的操作。因此,在每次程序想读或写字符时都直接访问磁盘文件是不可行的。获得较好性能的诀窍就是缓冲(buffering):把写入流的数据存储在内存的缓冲区域内;当缓冲区满了(或者关闭流)时,对缓冲区进行"清洗"(写入实际的输出设备)。输入流可以用类似的方法进行缓冲:缓冲区包含来自输入设备的数据,从缓冲区读数据而不是从设备本身读数据。缓冲在效率上可以取得巨大的收益,因为从缓冲区读字符或者在缓冲区内存储字符几乎不花什么时间。当然,把缓冲区的内容传递给磁盘,或者从磁盘传递给缓冲区是需要花时间的,但是一次大的"块移动"比多次小字节移动要快很多。
当程序向文件中写输出时,数据通常先放入缓冲区中。当缓冲区满了或者关闭文件时,缓冲区会自动清洗。然而,通过调用fflush函数,程序可以按我们所希望的频率来清洗文件的缓冲区。

  • 调用 **fflush(fp);**为和fp相关联的文件清晰了缓冲区。
  • 调用**fflush(NULL);**清洗了全部输出流。如果调用成功,fflush函数会返回零;如果发生错误,则返回EOF。
cpp 复制代码
#include <stdio.h>
#include <windows.h>
//VS2013 WIN10环境测试
int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区
	printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(10000);
	printf("刷新缓冲区\n");
	fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
	//注:fflush 在高版本的VS上不能使用了
	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);
	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;

	return 0;
}

这里可以得出一个结论

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题。

本篇文章到此结束,这篇文章学透,对于C语言中的文件操作就会行云流水~麻烦点个赞评论支持一下吧!

相关推荐
云空11 分钟前
《解锁 Python 数据挖掘的奥秘》
开发语言·python·数据挖掘
青莳吖21 分钟前
Java通过Map实现与SQL中的group by相同的逻辑
java·开发语言·sql
Buleall28 分钟前
期末考学C
java·开发语言
重生之绝世牛码31 分钟前
Java设计模式 —— 【结构型模式】外观模式详解
java·大数据·开发语言·设计模式·设计原则·外观模式
小蜗牛慢慢爬行37 分钟前
有关异步场景的 10 大 Spring Boot 面试问题
java·开发语言·网络·spring boot·后端·spring·面试
荒古前43 分钟前
龟兔赛跑 PTA
c语言·算法
Algorithm15761 小时前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明1 小时前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
Monly211 小时前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu1 小时前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa