【维生素C语言】第十六章 - 文件操作(上)

人 类 高 质 量 文 件 操 作 教 学(上)【C语言】

 🔥 CSDN 累计订阅量破千的火爆 C/C++ 教程的 2023 重制版,C 语言入门到实践的精品级趣味教程。
了解更多: 👉 "不太正经" 的专栏介绍 试读第一章
订阅链接: 🔗《C语言趣味教程》 ← 猛戳订阅!

前言:

本章为文件操作教学上篇,由浅入深的引入问题,然后逐一介绍知识。将详细讲解文件的打开和关闭、文件的顺序读写并精讲函数部分,初步学习“流”的概念!

 🚪 传送门:文件操作(下)


一、问题引入

0x00 什么是文件?

【百度百科】电脑文件,也可以称之为计算机文件,是存储在某种长期储存设备或临时存储设备中的一段数据流,并且归属于计算机文件系统管理之下。所谓“长期储存设备”一般指磁盘、光盘、磁带等。而“短期存储设备”一般指计算机内存。需要注意的是,存储于长期存储设备的文件不一定是长期存储的,有些也可能是程序或系统运行中产生的临时数据,并于程序或系统退出后删除。

📚 简单来讲,就是磁盘上的文件。

0x01 为什么使用文件?

📚 举个例子,我们想实现一个 “通讯录” 程序时,在通讯录中新建联系人、删除联系人等一系列操作,此时的数据存储于内存中,程序退出后所有数据都会随之消失,为了让通讯录中的信息得以保存,也就是想让数据持久化,我们就需要采用让数据持久化的方法。我们一般数据持久化的方法有:把数据存放在磁盘文件中,或存放到数据库等方式。

 0x02 什么是程序文件和数据文件?

📚 但在程序设计中,我们一般所说的文件为 程序文件 数据文件


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

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

💬 我们随便写一段代码,用于演示:

#include <stdio.h>

int main(void) {
    printf("Hello,World!\n");
    
    return 0;
}

🚩 随后运行代码(便于生成文件):

❓ 那么,什么是程序文件呢?

找到代码路径,打开文件夹查看 “可执行程序” :

退回到上层目录,找 “目标文件”:

❓ 那数据文件又是什么呢?

在代码路径下创建一个文件:

0x03 什么是文件名?

【百度百科】文件名是文件存在的标识,操作系统根据文件名来对其进行控制和管理。不同的操作系统对文件命名的规则略有不同,即文件名的格式和长度因系统而异。为了方便人们区分计算机中的不同文件,而给每个文件设定一个指定的名称。由文件主名和扩展名组成。

📚 存在的意义:一个文件要有一个惟一的文件标识,方便用户识别和引用。

 ❗  文件名包含三个部分:文件路径 + 文件名主干 + 文件后缀

                          ( 例如:C:\code2021\TestDemo.txt

🔑 为了方便起见,文件标识通常被称为 文件名

🔺 本章我们将对 数据文件 进行探讨!

二、文件的打开和关闭

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

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

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

0x00 文件指针

❓ 什么是文件指针:

【百度百科】在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。

🔑 在缓冲文件系统中,有一个关键的概念是 "文件类型指针" ,简称 "文件指针" 。

📚 每个被使用的文件,都会在内存中开辟出一个相应的文件信息区。该信息区用来存放文件相关信息(如文件名、文件状态以及文件当前位置等)。这些信息是保存在一个结构体变量中的,该结构体类型是由系统申明的,名为 FILE (注意是类型)。

 💬 例如由 VS2013 编译环境提供的 stdio.h 头文件中有以下的文件类型声明:

struct _iobuf {
    char *_ptr;
    int   _cnt;
    char *_base;
    int   _flag;
    int   _file;
    int   _charbuf;
    int   _bufsiz;
    char *_tmpfname;
};
typedef struct _iobuf FILE;

📌 注意事项:

FILE 的结构在不同的C编辑器中包含的内容并不是不完全相同的,但还是颇为相似的。

② 每当打开一个文件时,系统会根据文件的状况自动创建一个 FILE 结构的变量,并填充其中的信 息,只要文件被读写发生变化,文件信息区也会跟着发生变化。至于文件变化时文件信息区是怎么变化和修改的,我们其实并不需要关心这些细节,因为C语言已经帮你弄好了。

③ 我们一般会通过一个 FILE 的指针来维护这个 FILE 结构的变量。并不会直接使用,而是拿一个结构体指针指向这个结构,通过这个指针来访问和维护相关的数据,这样使用起来会更加方便。

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

定义 pf 是一个指向 FILE 类型的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区的信息就能够访问该文件。

FILE* pf; // 文件指针变量

💡 也就是说,通过文件指针变量能够找到与他关联的文件

0x01 fopen 函数与 fclose 函数

📜 头文件:stdlib.h

📚 ANSIC 规定使用 fopen 函数来打开文件, fclose 函数来关闭文件。

✅ filename 参数指的是文件名mode 参数为打开方式,打开方式如下:

文件使用方式

含义

如指定文件不存在
"  r "(只读)为了输入数据,打开一个已经存在的文本文件出错
" w "(只写)为了输出数据,打开一个文本文件建立一个新文件
" a "(追加)像文本文件尾添加数据建立一个新文件
" rb "(只读)为了输入数据,打开一个二进制文件          出错
" wb "(只写)为了输出数据,打开一个二进制文件建立一个新文件
" ab "(追加)象一个二进制文件尾添加数据出错
" r+ "(读写)为了读和写,打开一个文本文件出错
" w+ "(读写)为了读和写,建立一个新的文件建立一个新的文件
" a+ "(读写)打开一个文件,在文件尾进行读写                建立一个新的文件
" rb+ "(读写)为了读和写,打开一个二进制文件        出错
" wb+ "(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
" ab+ "(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

💬 代码演示:打开我们刚刚手动创建的 test.dat 文件

#include <stdio.h>

int main(void) {
    FILE* pf = fopen("test.dat", "w");
    // 检查是否为空指针
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    /* 写文件 */

    // 关闭文件
    fclose(pf);
    pf = NULL; // 记得将pf置为空指针

    return 0;
}

🚩 (代码正常运行)

❓ 之前我们创建的 test.dat 的路径是在 路径下的,如果放在其他路径下可以读吗?

🔑 可以,但文件必须在该工程的路径下才行。

💬 我们把 test.dat 文件删除,然后打开方式改成 r 试试看:

#include <stdio.h>

int main(void) {
    FILE* pf = fopen("test.dat", "r");
    // 检查是否为空指针
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    /* 写文件 */

    // 关闭文件
    fclose(pf);
    pf = NULL; // 记得将pf置为空指针

    return 0;
}

🚩 运行结果如下:  (通过刚才的表格可知,如果 r 找不到指定的文件,会导致报错)

💬 如果不适用相对路径,使用绝对路径读文件:

可以使用绝对路径,但是要注意转义绝对路径中的斜杠!

#include <stdio.h>

int main(void) {
    // FILE* pf = fopen("D:\code2021\0817\0817\test2.dat", "w"); // error
    FILE* pf = fopen("D:\\code2021\\0817\\0817\\test2.dat", "w"); // 转移字符\

    // 检查是否为空指针
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    /* 写文件 */

    // 关闭文件
    fclose(pf);
    pf = NULL; // 记得将pf置为空指针

    return 0;
}

📌 注意事项: 不关闭文件的后果:一个程序能够打开的文件是有限的,文件属于一种资源。如果只打开不释放,文件就会被占用。可能会导致一些操作被缓冲在内存中,如果不能正常关闭,缓冲在内存中的数据就不能正常写入到文件中从而导致数据的丢失。

三、文件的顺序读写

0x00 什么是顺序读写

首先要了解什么是读写:我们写的程序是在内存中,而数据是要放到文件中的,文件又是在硬盘上的。当我们把文件里的数据读到内存中去时,这个动作我们称之为输入/读取。反过来,如果把程序中的东西放到硬盘上,这个动作我们称之为输出/写入。

📚 顺序读写,顾名思义就是按照顺序在文件中读和写。

0x01 顺序读写函数一览表

0x02 字符输出函数 fputc

📚 介绍:将参数 char 指定的字符写入到指定的流 stream 中,并把位置标识符向前移动 (字符必须为一个无符号字符)。适用于所有输出流。

💬 代码演示:创建一个 test.txt,随后使用 fputc 函数分别写入 "abc" 到文件中

#include <stdio.h>

int main(void) {
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    // 写文件
    fputc('a', pf);
    fputc('b', pf);
    fputc('c', pf);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

🚩(代码正常运行)

📂 此时打开工程文件夹可以成功看到 test.txt 被创建了(大小为1kb可以看出写入成功了):

💬 我们正好测试下 w 的覆盖效果,我们把写的内容注释掉:

#include <stdio.h>

int main(void) {
    FILE* pf = fopen("test.txt", "w");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    // 写文件
    //fputc('a', pf);
    //fputc('b', pf);
    //fputc('c', pf);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

🚩 此时再次运行,我们发现那个文件里的内容不见了(大小也变为0kb):

 ❗  值得注意的是,文件的写入是有顺序的。abc,先是a,然后是b,最后是c:

fputc('a', pf);
fputc('b', pf);
fputc('c', pf);

0x03  字符输入函数 fgetc

📚 介绍:从指定的流 stream 获取下一个字符,并把位置标识符向前移动(字符必须为一个无符号字符)。如果读取成功会返回相应的ASCII码值,如果读取失败它会返回一个EOF。适用于所有输入流。

💬 代码演示:在工程文件夹里打开 test.txt ,随便写入一些数据,随后使用 fgetc 函数读取:

#include <stdio.h>
// 使用fgetc从文件里读
int main(void) {
    FILE* pf = fopen("test.txt", "r");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
    // 读文件
    int ret = fgetc(pf);
    printf("%c\n", ret);
    ret = fgetc(pf);
    printf("%c\n", ret);
    ret = fgetc(pf);
    printf("%c\n", ret);

    // 关闭文件
    fclose(pf);
    pf = NULL;
    
    return 0;
}

🚩 运行结果如下:

❓ 如果读完了会发生什么?

🐞 我们来调试一下看看:

0x04 文本行输出函数 fputs

📚 介绍:将字符串写入到指定的流 stream 中(不包括空字符)。适用于所有输出流。

💬 代码演示:利用 fputstest2.txt 中随便写入几行数据:

#include <stdio.h>

int main(void) {
    FILE* pf = fopen("test2.txt", "w");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
    // 写文件 - 按照行来写
    fputs("abcdef", pf);
    fputs("123456", pf);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

🚩  (代码成功运行)

❓ 如果想换行呢?

🔑 换行需要在代码里自行加 \n

fputs("abcdef\n", pf);
fputs("123456", pf);

(这时候打开文件,就是换行的了)

0x05 文本行输入函数 fgets

📚 介绍:从指定的流 stream 读取一行,并把它存储在 string 所指向的字符串中,当读取(n-1)个字符时,或者读取到换行符、到达文件末尾时,它会停止,具体视情况而定。适用于所有输入流。

📌 注意事项:假如 n 是100,读取到的就是99个字符(n-1),因为要留一个字符给斜杠0。

💬 代码演示:利用 fgets 读取 test2.txt 中的内容:

#include <stdio.h>

int main(void) {
    char arr[10] = "xxxxxx"; // 存放处

    FILE* pf = fopen("test2.txt", "r");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
    // 读文件 - 按照行来读
    fgets(arr, 4, pf);
    printf("%s\n", arr);

    fgets(arr, 4, pf);
    printf("%s\n", arr);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

🚩 代码运行结果如下:

 🐞 调试一下看看:

0x06 格式化输出函数 fprintf

📚 介绍:fprintf 用于对格式化的数据进行写文件,发送格式化输出到流 stream 中。适用于所有输出流。

💬 代码演示:将结构体的三个数据利用 fprintf 写到 test3.txt 中:

#include <stdio.h>

struct Player {
    char name[10];
    int dpi;
    float sens;
};

int main(void) {
    struct Player p1 = { "carpe", 900, 3.12f };

    // 对格式化的数据进行写文件
    FILE* pf = fopen("test3.txt", "w");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
    // 写文件
    fprintf(pf, "%s %d %f", p1.name, p1.dpi, p1.sens);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

 🚩  (代码成功运行)

 

 0x07 格式化输入函数 fscanf

📚 介绍:fscanf 用于对格式化的数据进行读取,从流 stream 读取格式化输入。适用于所有输入流。

💬 代码演示:利用 fscanf 读取 test4.txt 中的内容,并打印:

#include <stdio.h>

struct Player {
    char name[10];
    int dpi;
    float sens;
};

int main(void) {
    struct Player p1 = { 0 }; // 存放处

    // 对格式化的数据进行写文件
    FILE* pf = fopen("test3.txt", "r");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }

    // 读文件
    fscanf(
        pf, "%s %d %f",
        p1.name, &(p1.dpi), &(p1.sens) 
    ); // 👆 p1.name本身就是地址(不用&)

    // 将读到的数据打印
    printf("%s %d %f\n", p1.name, p1.dpi, p1.sens);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

 🚩 运行结果如下:

0x08 二进制输出函数 fwrite

📚 介绍:写一个数据到流中去,把 buffer 所指向的数组中的数据写入到给定流 stream 中。

💬 创建一个 test5.txt,用 fwrite 写入一个数据到 text5.txt 中去:

#include <stdio.h>
// 二进制的形式写

struct S {
    char arr[10];
    int num;
    float score;
};

int main(void) {
    struct S s = { "abcde", 10, 5.5f };

    FILE* pf = fopen("test5.txt", "w");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
    // 写文件
    fwrite(&s, sizeof(struct S), 1, pf);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

🚩  (代码成功运行)

 

 我们发现他烫起来了2333(划掉)

💡 打开文件后我们发现只有abcde看得懂,后面是什么我们看不懂。

我们试着用 nodepad++ 打开:

❓ 为什么还是乱码?为什么 abcde 不是乱码?

🔑 解答:

① 我们刚才用的都是文本编译器,文本编译器打开二进制形式的文件完全是两种状态。

② 因为字符串以文本形式写进去和以二进制形式写进去是一样的,但是对于整数、浮点数等来说就不一样了,文本形式写入和二进制形式写入完全是两个概念。

(那么该怎么读呢,我们来看下面的 fread 函数)

0x08 二进制输入函数 fread

📚 介绍:从流中读取,从给定流 stream 读取数据到 buffer 所指向的数组中。

💬 用 fread 读取 text5.txt 中的二进制数据:

#include <stdio.h>
// 二进制的形式读

struct S {
    char arr[10];
    int num;
    float score;
};

int main(void) {
    struct S s = { 0 }; // 存放处

    FILE* pf = fopen("test5.txt", "r");
    if (pf == NULL) {
        perror("fopen");
        return 1;
    }
    // 读文件
    fread(&s, sizeof(struct S), 1, pf);

    // 将读到的数据打印
    printf("%s %d %f", s.arr, s.num, s.score);

    // 关闭文件
    fclose(pf);
    pf = NULL;

    return 0;
}

🚩  (代码正常运行)

 ​​​​​​​

🔺 总结: fwritefread 是一对,fwrire 写进去用 fread 读。 

0x09 流的概念(stream)

在这里,我们补充一下流的概念。

📚 观察刚才的表格我们可以发现有的函数是适用于所有xx流的(比如 fputc 函数)。fputc 就适用于所有输出流,也就是说它不仅仅可以给文件里写。我们来读一下MSDN的介绍:

我们发现它还可以写到 stdout 上。

❓ 那么 stdout 是什么呢?

💡 stdout 就是标准输出流,在这里,我们要来讲一下流的概念。

📚 C语言默认打开的3个流:

      ① stdin   - 标准输入流 - 键盘
      ② stdout - 标准输出流 - 屏幕
      ③ stderr  - 标准输出流 - 屏幕

💬 我们用流向屏幕上输出信息 - stdout:

#include <stdio.h>

int main(void) {
    fputc('a', stdout);
    fputc('b', stdout);
    fputc('c', stdout);

    return 0;
}

🚩  a b c

 💬 fgetc 从标准输入流读取 - stdin

#include <stdio.h>
// 使用fgetc从标准输入流中读
int main(void) {
    int ret = fgetc(stdin);
    printf("%c\n", ret);
    ret = fgetc(stdin);
    printf("%c\n", ret);
    ret = fgetc(stdin);
    printf("%c\n", ret);
    
    return 0;
}

🚩 运行:


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

比特科技. C语言进阶[EB/OL]. 2021[2021.8.31]. .

📌 本文作者: 王亦优

📃 更新记录: 2021.8.19

❌ 勘误记录: 孙老师:有两张图违规了【现已修正】

💬 参考资料: 百度百科、比特科技、www.cplusplus.com、MSDN、RUNOOB.com

📜 本文声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

未完待续...

🚪 传送门:文件操作(下)

  • 239
    点赞
  • 214
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 117
    评论
评论 117
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王平渊

喜欢的话可以支持下我的付费专栏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值