当前位置: 首页 > article >正文

C语言中的文件

文章目录

  • 文件
    • 1. 流
      • 1.1 文件缓冲
      • 1.2 标准流
      • 1.3 文本文件和二进制文件
    • 2. 打开/关闭文件
      • 2.1 fopen
      • 2.2 fclose
    • 3. 读写文件
      • 3.1 fgetc & fputc
      • 3.2 fgets & futs
      • 3.3 fscanf & fprintf
      • 3.4 fread & fwrite
    • 4. 文件定位
    • 5. 错误处理
      • 5.1 errno

文件

1. 流

在 C 语言中, (stream) 表示任意输入的源或任意输出的目的地。流是一个抽象的概念,它既可以表示存储硬盘上的文件,也可以表示网络端口或者打印设备。流这个概念可以很好地屏蔽硬件设备之间的差异,使得 C 语言可以像读写文件一样读写任意的设备。

Linux哲学:一切皆文件。

1.1 文件缓冲

仅仅了解抽象的概念是不够的,有时候我们还需要了解事物运行的机理。由于内存和硬件设备之间存在读写性能上的"鸿沟",所以操作系统会在内存上为流设置缓冲区。

在这里插入图片描述

缓冲区是以先进先出的方式管理数据的。缓冲区分为三种类型:

  • 满缓冲。当缓冲区空时,从输入流中读取数据;当缓冲区满时,向输出流中写入数据。
  • 行缓冲。每次从输入流中读取一行数据;每次向输出流中写入一行数据(stdin、stdout)。
  • 无缓冲。顾名思义,就是没有缓冲区(stderr)。

1.2 标准流

C 语言对流的访问是通过文件指针实现的,它的类型为 FILE* 。并且在<stdio.h>头文件中提供了 3 个标准流。这 3 个标准流可以直接使用——我们不需要对其进行声明,也不用打开或者关闭它们。

文件指针默认含义
stdin标准输入键盘
stdout标准输出屏幕
stderr标准错误屏幕

1.3 文本文件和二进制文件

C 语言支持两种类型的文件:文本文件和二进制文件。文本文件中存储的是字符数据,人类是可以看懂的;二进制文件中的数据,人类是看不懂的。

[!TIP]

二进制文件的存储的基本单位是字节

文本文件的基本单位是字符(字符 = 字节 + 编码(比如GBK UTF-8))

文本文件具有两个独特的性质:

  • 文本文件有行的概念。文本文件被划分为若干行,并且每一行的结尾都以特殊字符进行标记。在 Windows 系统中,是以回车符和换行符 (\r\n) 进行标记的;在 Unix 和 Macintosh 系统中是以换行符 (\n) 标记的。

  • 文本文件可能包含一个特殊的“文本末尾”标记。一些操作系统允许在文本文件的末尾使用一个特殊的字节作为标记。在 Windows 系统中,这个标记为 ‘\x1a’ (Ctrl+Z)。Ctrl+Z不是必需的,但如果存在,它就标志着文件的结束,其后的所有字节都会被忽略。大多数其他操作系统 (包括 UNIX) 是没有文件末尾字符。

    使用 Ctrl+Z 的这一习惯继承自 DOS,而 DOS 中的这一习惯又是从 CP/M (早期用于个人电脑的一种操作系统) 来的。

在写入数据时,我们需要考虑是以文本形式存储还是以二进制的形式存储。比如,存储整数 32767,一种选择是写入字符 ‘3’, ‘2’, ‘7’, ‘6’, ‘7’,需要 5 个字节。

在这里插入图片描述

另一个选择是以二进制形式存储这个数,这种方法只需要两个字节。

在这里插入图片描述

文本形式可以方便人类阅读和编辑;二进制形式可以节省空间,并且转换效率高。

2. 打开/关闭文件

2.1 fopen

读写文件之前,我们需要使用 fopen 函数打开文件。

FILE* fopen(const char* filename, const char* mode);

第一个参数是文件的路径,用来定位文件的;第二个参数表示是以何种模式打开文件的。如果无法打开文件, fopen 返回空指针。

文件路径

文件路径分为两种,一种是绝对路径:从根目录 (或者盘符) 开始,一直到文件所在的位置,比如:“c:/project/test.dat”。另一种是相对路径:从当前工作目录开始,一直到文件所在的位置,比如:“in.dat”。

在实际工作中,我们一般使用相对路径 (Why? 简单高效)。

模式

模式的选择不仅依赖于后续对文件的操作,还依赖于文件是文本形式还是二进制形式。打开一个文本文件,可以使用下面一些模式:

模式字符串含义
“r”打开文件用于读
“w”打开文件用于写(文件不存在则创建)
“a”打开文件用于追加(文件不存在则创建)
“r+”打开文件用于读和写,从文件头开始
“w+”打开文件用于读和写(文件不存在则创建)
“a+”打开文件用于读和写(文件不存在则创建)

当使用 fopen 打开二进制文件时,需要在模式字符串中包含字母 b。

模式字符串含义
“rb”打开文件用于读
“wb”打开文件用于写(文件不存在则创建)
“ab”打开文件用于追加(文件不存在则创建)
“r+b"或"rb+”打开文件用于读和写,从文件头开始
“w+b"或"wb+”打开文件用于读和写(文件不存在则创建)
“a+b"或"ab+”打开文件用于读和写(文件不存在则创建)

写模式和追加模式是不一样的。如果文件存在,写模式会清空原有的数据,而追加模式会在原有数据的后面写入新的内容。

2.2 fclose

fclose 可以关闭程序不再使用的文件。

int fclose(FILE* stream);

如果成功关闭,fclose返回零;否则返回 EOF。

[!TIP]

注意:当不再使用某个文件时,一定要及时关闭该文件。

下面给出了一个程序框架,展示了在实际工作中是如何打开和关闭文件的:

FILE* fp = fopen(filename, mode);
if (fp == NULL) {
    // error handling
}
...
fclose(fp);

3. 读写文件

前面介绍了如何打开和关闭文件,接下来我们来学习下如何读写文件。

其中 fgetc/fputc , fgets/fputsfscanf/fprintf 是用来读写文本文件的;

fread/fwrite 是用来读写二进制文件的。

3.1 fgetc & fputc

fgetc

fgetc 可以从输入流中读取一个字符,如果读取成功,返回读取的字符;如果读到文件末尾,或者读取失败,返回 EOF。

int fgetc(FILE* stream);

fgetcgetchar 类似。不同的是 getchar 只能从标准输入流(stdin)中读取字 符,而 fgetc 可以从任意一个输入流中读取字符。

fputc

fputc 可以向输出流中写入一个字符,如果写入成功,返回写入的字符;如果写入失败,返回EOF。

int fputc(int c, FILE* stream);

fputcputchar 类似。不同的是 putchar 只能向标准输出流(stdout)中写入字符,而fputc 可以向任意一个流中写入字符。

示例

#include <stdio.h>
#include <cstdlib>

int main(int argc, char** argv) {

    if (argc != 3) {
        printf("Error: invalid arguments\n");
        exit(EXIT_FAILURE);
    }

    // 打开文件流
    FILE* source_fp = fopen(argv[1], "r");
    if (source_fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    FILE* dest_fp = fopen(argv[2], "w");
    if (dest_fp == NULL) {
        printf("Can not open %s\n", argv[2]);
        fclose(source_fp);
        exit(EXIT_FAILURE);
    }

    // 复制文件
    int c = 0;
    while ((c = fgetc(source_fp)) != EOF) {
        fputc(c, dest_fp);
    }

    // 关闭文件流
    fclose(source_fp);
    fclose(dest_fp);

    return 0;
}

3.2 fgets & futs

一个字符一个字符地读写文本文件,效率太慢了。C 语言提供了一次性可以读写一行的函数 fgetsfputs

fgets

从输入流 stream 中,最多读取 count - 1 个字符,并把读取的字符存入 str 指向的字符数组中。 fgets 遇到换行符’\n’,或者文件的末尾就会终止(也就是说,读取的字符数可能不足 count - 1 个),并且会存储换行符’\n’。 fgets 会在最后添加空字符’\0’。

char* fgets(char* str, int count, FILE* stream);
参数:
    str: 指向一个字符数组
    count: 能够写入的最大字符数量(通常是str指向字符数组的长度)
    stream: 输入流
返回值:
    成功:返回str
    失败:NULL

fgetsgets 的通用版本,它可以从任意输入流中读取数据,而 gets 只能从 stdin 中读取数据。

fgets 也比 gets 更为安全,因为它限制了读取字符的最大数目 (count - 1)。此外,如果 fgets 是因为读取了换行符而终止,那么它会存储换行符’\n’,而 gets 函数从来不会存储换行符。

fputs

将 str 指向的字符串,写入输出流 stream 中。

int fputs(const char* str, FILE* stream);
参数:
    str: 要写的字符串('\0'结尾的字符串)
    stream: 输出流
返回值:
    成功:返回一个非负值。
    失败:返回EOF,并设置errno。

fputsputs 的通用版本,它可以将字符串写入到任意的输出流中,而 puts 只能 写入到 stdout 中。此外, fputs 是原样输出字符串,而 puts 会在字符串后面而外 输出一个换行符’\n’。

示例

#include <stdio.h>
#include <cstdlib>

int main(int argc, char** argv) {
    if (argc != 3) {
        printf("Error: invalid arguments\n");
        exit(EXIT_FAILURE);
    }

    // 打开文件流
    FILE* source_fp = fopen(argv[1], "r");
    if (source_fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    FILE* dest_fp = fopen(argv[2], "w");
    if (dest_fp == NULL) {
        printf("Can not open %s\n", argv[2]);
        fclose(source_fp);
        exit(EXIT_FAILURE);
    }

    // 复制文件(fgets & fputs)
    char str[1024];
    while (fgets(str, sizeof(str), source_fp) != NULL) {
        fputs(str, dest_fp);
    }

    // 关闭文件流
    fclose(source_fp);
    fclose(dest_fp);

    return 0;
}

3.3 fscanf & fprintf

fscanf

fscanfscanf 类似,是用来进行格式化输入的。

int fscanf(FILE* stream, const char* format, ...);

不同的是, scanf 是从标准输入(stdin)中读取数据,而 fscanf可以从任何一个流中读取数据。也就是说,当 fscanf 的第一个参数为 stdin 时,它的效果等价于scanf

顺便提一下,sscanf 可以从字符串中读取数据。

fprintf

fprintfprintf 类似,是用来进行格式化输出的。

int fprintf(FILE* stream, const char* format, ...);

不同的是, printf 始终是向标准输出(stdout)写入内容的,而 fprintf 可以向任何一个输出流中写入内容。也就是说,当 fprintf 的第一个参数为 stdout 时,它的效果等价于 printf

顺便提一下, sprintf 可以将内容写入到一个字符数组中。

格式化输入输出,可以用于序列化和反序列化过程中。所谓序列化,就是将程序中的对象转换成一种可以保存的格式(二进制或文本),从而方便存储(存储到文件或数据库中)或传输(通过网络传输给另一台机器)。反序列化则是序列化的逆过程,它将按一定格式存储的数据转换成程序中的对象。

示例

#include <stdio.h>
#include <cstdlib>

typedef struct {
    char name[25];
    int age;
    char gender;
} Student_t;

int main(int argc, char** argv) {

    if (argc != 2) {
        printf("Error: invalid arguments\n");
        exit(EXIT_FAILURE);
    }

    Student_t stu1 = {"tom", 18, 'f'};

    FILE* fp = fopen(argv[1], "w");
    if (fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    // 序列化
    fprintf(fp, "%s %d %c", stu1.name, stu1.age, stu1.gender);
    fclose(fp);

    fp = fopen(argv[1], "r");
    if (fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    // 反序列化
    Student_t stu2 = {"jack", 19, 'm'};
    fscanf(fp, "%s %d %c", stu2.name, &stu2.age, &stu2.gender);
    fclose(fp);
    
    printf("Stu2.name: %s Stu2.age: %d Stu2.gender: %c\n", stu2.name, stu2.age, stu2.gender);

    return 0;
}

3.4 fread & fwrite

freadfwrite 主要是用来处理二进制文件的。 fread 可以每次读取一大块数据, fwrite 可以每次写入一大块数据。

fread 从输入流 stream 中,最多读取 count 个元素,并把它们依次存放到 buffer 指向的数组中。

size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
参数:
    buffer: 指向存放数据的数组
    size: 每个元素的大小(以字节为单位)
    count: 最多可以读取的元素个数
    stream: 输入流
返回值:
    成功读取元素的个数。当读到文件末尾,或者发生错误时,返回值可能小于count。
    我们可以通过feof和ferror函数来判断,到底是读到了文件末尾,还是发生了错误。

fwrite 将存放在 buffer 指向的数组中的 count 个元素写入到输出流 stream 中。

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);
参数:
    buffer: 指向存放数据的数组。
    size: 每个元素的大小(以字节为单位)
    count: 要写入元素的个数
    stream: 输出流
返回值:
    成功写入元素的个数。当发生错误时,这个值可能小于count。

fread/fwrite 不仅可以用于读写二进制文件,还可以用于序列化和反序列化过程中。

示例1(复制二进制文件)

int main(int argc, char** argv) {
    if (argc != 3) {
        printf("Error: invalid arguments\n");
        exit(EXIT_FAILURE);
    }

    // 打开文件流
    FILE* source_fp = fopen(argv[1], "r");
    if (source_fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    FILE* dest_fp = fopen(argv[2], "w");
    if (dest_fp == NULL) {
        printf("Can not open %s\n", argv[2]);
        fclose(source_fp);
        exit(EXIT_FAILURE);
    }

    // 复制文件(fread & fwrite)
    char str[1024];
    int n = 0;
    while ((n = fread(str, 1, sizeof(str), source_fp)) != 0) {
        fwrite(str, 1, n, dest_fp);
    }

    // 关闭文件流
    fclose(source_fp);
    fclose(dest_fp);

    return 0;
}

示例2(序列化与反序列化)

#include <stdio.h>
#include <cstdlib>

typedef struct {
    char name[25];
    int age;
    char gender;
} Student_t;

int main(int argc, char** argv) {

    if (argc != 2) {
        printf("Error: invalid arguments\n");
        exit(EXIT_FAILURE);
    }

    Student_t stu1 = {"jerry", 20, 'm'};

    FILE* fp = fopen(argv[1], "w");
    if (fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    /* ********** 使用fscanf & fprintf ********** */
    // 序列化
    fwrite(&stu1, sizeof(Student_t), 1, fp);
    fclose(fp);

    fp = fopen(argv[1], "r");
    if (fp == NULL) {
        printf("Can not open %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }
    
    Student_t stu2 = {"jack", 19, 'm'};
    // 反序列化
    fread(&stu2, sizeof(Student_t), 1, fp);
    fclose(fp);

    printf("Stu2.name: %s Stu2.age: %d Stu2.gender: %c\n", stu2.name, stu2.age, stu2.gender);

    return 0;
}

4. 文件定位

每个流都有相关联的文件位置。在执行读写操作时,文件位置会自动推进,并按照顺序访问文件。顺序访问是很好的,但是有时候,我们可能需要跳跃地访问文件。为此, 提供了几个函数来支持这种能力:

int fseek(FILE* stream, long int offset, int whence);
long int ftell(FILE* stream);
void rewind(FILE* stream);

fseek

fseek 可以改变与 stream 相关联的文件位置。其中 whence 表示参照点,参照点有 3 个选择:

  • SEEK_SET:文件的起始位置
  • SEEK_CUR:文件的当前位置
  • SEEK_END:文件的末尾位置

offset 表示偏移量 (可能为负),它是以字节进行计数的。比如:移动到文件的起始位置,可以这样写:

fseek(fp, 0L, SEEK_SET);

移动到文件的末尾,可以这样写:

fseek(fp, 0L, SEEK_END);

往回移动10个字节,可以这样写:

fseek(fp, -10L, SEEK_CUR);

通常情况下, fseek 会返回 0;如果发生错误 (比如,请求的位置不存在),那么 fseek 会返回非 0 值。

ftell

ftell 以长整数形式返回当前文件位置;如果发生错误,ftell返回-1L。ftell 一般的用法是:记录当前位置,方便以后返回。

long int filePos = ftell(fp);
...
fseek(fp, filePos, SEEK_SET);

rewind

rewind 会将文件位置设置为起始位置,类似于调用:

fseek(fp, 0L, SEEK_SET);

练习

用户输入文件名,将整个文件的内容读入到字符数组中,并在后面添加空字符’\0’。

char* readFile(const char* path);
char* readFile(const char* path) {
    // 打开文件
    FILE* fp = fopen(path, "rb");
    if (fp == NULL) {
        printf("Can not open %s\n", path);
        exit(EXIT_FAILURE);
    }

    // 文件结尾位置
    fseek(fp, 0L, SEEK_END);
    long int file_len = ftell(fp);

    // 将文件内容复制到buffer中
    rewind(fp); // 回到文件开头,因为要从开头开始复制
    char* buffer = (char*)malloc(file_len + 1);
    fread(buffer, 1, file_len, fp);

    // 在数组末尾添加空字符
    buffer[file_len] = '\0';

    fclose(fp);

    return buffer;
}

int main(int argc, char* argv[]) {
    if (argc != 2) {
        printf("Error: invalid arguments\n");
        exit(EXIT_FAILURE);
    }

    char* buffer = readFile(argv[1]);
    printf("buffer content: \n%s\n", buffer);

    free(buffer);

    return 0;
}

5. 错误处理

错误的检测和处理并不是 C 语言的强项,C 语言没有其它高级语言 (C++, Java, C#等) 所具有的异常处理机制。

C 语言往往是通过函数的返回值,或者是测试 errno 变量来检测错误的;并且需要程序员自己编写代码来处理错误。

5.1 errno

errno 是一个 int 类型的全局变量 (C11 修改为线程本地变量,即每个线程都有一个独有的 errno 变量),它定义在 <errno.h> 头文件中。

标准库中有些函数 (比如与文件相关的一些函数),如果在调用过程中发生了错误,它会设置 errno 的值,以表明发生 了何种类型的错误。

程序启动时,会将 errno 的值设为 0,表示没有错误发生。其它非 0 值都表示发生了某种类型的错误。

我们可以通过 perrorstrerror 来显示错误信息。其中, perror 定义在 <stdio.h> 头文件 中, strerror 定义在 <string.h> 头文件中。

示例

int main(void) {
    printf("%d\n", errno); // errno == 0
    FILE* fp = fopen("not_exist.txt", "r");
    printf("%d\n", errno); // errno == 2
    printf("%s\n", strerror(errno)); // No such file or directory
    perror("not_exist.txt"); // not_exist.txt: No such file or directory

    return 0;
}

http://www.kler.cn/a/548144.html

相关文章:

  • C#学习之S参数读取(s2p文件)
  • Selenium自动化测试入门:python unittest 单元测试框架
  • 数字内容体验优化策略:全渠道整合与高效转化实践
  • 草图绘制技巧
  • 【linux】Socket网络编程
  • vue使用v-chart的实践心得
  • 【Elasticsearch】keyword分析器
  • 【ISO 14229-1:2023 UDS诊断全量测试用例清单系列:第二十节】
  • C语言表驱动法
  • 为什么vue3需要对引入的组件使用markRaw?
  • Java NIO基础与实战:如何提升IO操作性能
  • 【openGauss】6.0.0企业版单节点安装,配置刷新
  • 【鸿蒙HarmonyOS Next实战开发】lottie动画库
  • 【Python爬虫(3)】解锁Python爬虫技能树:深入理解模块与包
  • 20250214在ubuntu20.04下使用obs studio录制外挂的1080p的USB摄像头【下载安装】
  • PDF工具,个人作品,免费分享
  • 教程 | 从零部署到业务融合:DeepSeek R1 私有化部署实战指南
  • (rancher) k8s ingress 屏蔽 nginx版本信息
  • 解读 Flink Source 接口重构后的 KafkaSource
  • SIP中常见的服务器类型