C语言探索之旅 | 第二部分第七课:文件读写

做者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文:www.jianshu.com/p/4adb95073…程序员

《C语言探索之旅》全系列数组

内容简介


  1. 前言
  2. 文件的打开和关闭
  3. 读写文件的不一样方法
  4. 在文件中移动
  5. 文件的重命名和删除
  6. 第二部分第八课预告

1. 前言


上一课 C语言探索之旅 | 第二部分第六课:建立你本身的变量类型 以后,咱们来学习很经常使用的文件读写。bash

咱们学过了这么多变量的知识,已经知道变量实在是很强大的,能够帮助咱们实现不少事情。微信

变量当然强大,仍是有缺陷的,最大的缺陷就是:不能永久保存app

由于 C语言的变量储存在内存中,在你的程序退出时就被清除了,下次程序启动时就不能找回那个值了。编辑器

“蓦然回首,那人不在灯火阑珊处...”函数

“今天的你我, 怎样重复昨天的故事? 这一张旧船票, 还可否登上你的破船?”学习

不可以啊,“涛声不能依旧”啊...测试

若是这样的话,咱们如何在 C语言编写的游戏中保存游戏的最高分呢?怎么用 C语言写一个退出时依然保存文本的文本编辑器呢?ui

幸亏,在 C语言中咱们能够读写文件。这些文件会储存在咱们电脑的硬盘上,就不会在程序退出或电脑关闭时被清除了。

为了实现文件读写,咱们就要用到迄今为止咱们所学过的知识:

指针,结构体,字符串,等等。

也算是复习吧。

2. 文件的打开和关闭


为了读写文件,咱们须要用到定义在 stdio.h 这个标准库头文件中的一些函数,结构,等。

是的,就是咱们所熟知的 stdio.h,咱们的“老朋友” printf 和 scanf 函数也是定义在这个头文件里。

下面按顺序列出咱们打开一个文件,进行读或写操做所必须遵循的一个流程:

  1. 调用“文件打开”函数 fopen(f 是 file(表示“文件”)的首字母;open 表示“打开”),返回一个指向该文件的指针。

  2. 检测文件打开是否成功,经过第 1 步中 fopen 的返回值(文件指针)来判断。若是指针为 NULL,则表示打开失败,咱们须要中止操做,而且返回一个错误。

  3. 若是文件打开成功(指针不为 NULL),那么咱们就能够接着用 stdio.h 中的函数来读写文件了。

  4. 一旦咱们完成了读写操做,咱们就要关闭文件,用 fclose(close 表示“关闭”)函数。

首先咱们来学习如何使用 fopen 和 fclose 函数,以后咱们再学习如何读写文件。

fopen:打开文件


函数 fopen 的原型是这样的:

FILE* fopen(const char* fileName, const char* openMode);
复制代码

不难看出,这个函数接收两个参数:

  • fileName:文件名(name 表示“名字”)。是一个字符串类型,并且是 const,意味着不能改变其值。

  • openMode:打开方式(open 表示“打开”,mode 表示“方式”)。代表咱们打开文件以后要干什么的一个指标。只读、只写、读写,等等。

这个函数的返回值,是 FILE *,也就是一个 FILE(file 表示“文件”)指针。

FILE 定义在 stdio.h 中。有兴趣的读者能够本身去找一下 FILE 的定义。

咱们给出 FILE 的通常定义:

typedef struct {
    char *fpos; /* Current position of file pointer (absolute address) */
    void *base; /* Pointer to the base of the file */
    unsigned short handle; /* File handle */
    short flags; /* Flags (see FileFlags) */
    short unget; /* 1-byte buffer for ungetc (b15=1 if non-empty) */
    unsigned long alloc; /* Number of currently allocated bytes for the file */
    unsigned short buffincrement; /* Number of bytes allocated at once */
} FILE;
复制代码

能够看到 FILE 是一个结构体(struct),里面有 7 个变量。固然咱们没必要深究 FILE 的定义,只要会使用 FILE 就行了,并且不一样操做系统对于 FILE 的定义不尽相同。

细心的读者也许会问:“以前不是说结构体的名称最好是首字母大写么,为何 FILE 这个结构体每个字母都是大写呢?怎么和常量的命名方式同样呢?”

好问题。其实咱们以前建议的命名方式(对于结构体,首字母大写,例如:StructName)只是一个“规范”(虽然大多数程序员都喜欢遵循),并非一个强制要求。

这只能说明编写 stdio.h 的前辈并不必定遵循这个“规范”而已。固然,这对咱们并没什么影响。

如下列出几种可供使用的 openMode :

  • r :只读。r 是 read(表示“读”)的首字母。这个模式下,咱们只能读文件,而不能对文件写入。文件必须已经存在。

  • w :只写。w 是 write(表示“写”)的首字母。这个模式下,只能写入,不能读出文件的内容。若是文件不存在,将会被建立。

  • a :追加。a 是 append(表示“追加”)的首字母。这个模式下,从文件的末尾开始写入。若是文件不存在,将会被建立。

  • r+ :读和写。这个模式下,能够读和写文件,但文件也必须已经存在。

  • w+ :读和写。预先会删除文件内容。这个模式下,若是文件存在且内容不为空,则内容首先会被清空。若是文件不存在,将会被建立。

  • a+ :读写追加。这个模式下,读写文件都是从文件末尾开始。若是文件不存在,将会被建立。

上面所列的模式,其实还能够组合上 b 这个模式。b 是 binary 的缩写,表示“二进制”。 对于上面的每个模式,若是你添加 b 后,会变成 rbwbabrb+wb+ab+ ),该文件就会以二进制模式打开。不过二进制的模式通常不是那么经常使用。

通常来讲,rwr+ 用得比较多。w+ 模式要慎用,由于它会首先清空文件内容。当你须要往文件中添加内容时,a 模式会颇有用。

下面的例子程序就以 r+(读写)的模式打开文件:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "r+");

    return 0;
}
复制代码

因而,file 成为了指向 test.txt 文件的一个指针。

你会问:“咱们的 test.txt 文件位于哪里呢?”

text.txt 文件和可执行文件位于同一目录下。

“文件必定要是 .txt 结尾的吗?”

不是,彻底由你决定文件的后缀名。你大能够建立一个文件叫作 xxx.level,用于记录游戏的关卡信息。

“文件必定要和可执行文件在同一个文件夹下么?”

也不是。理论上能够位于当前系统的任意文件夹里,只要在 fopen 函数的文件名参数里指定文件的路径就行了,例如:

file = fopen("folder/test.txt", "w");
复制代码

这样,文件 test.txt 就是位于当前目录的文件夹 folder 里。这里的 folder/test.txt 称为“相对路径”。

咱们也能够这样:

file = fopen("/home/user/folder/test.txt", "w");
复制代码

这里的 /home/user/folder/test.txt 是“绝对路径”。

测试打开文件


在调用 fopen 函数尝试打开文件后,咱们须要检测 fopen 的返回值,以判断打开是否成功。

检测方法也很简单:若是 fopen 的返回值为 NULL,那么打开失败;若是不为 NULL,那么表示打开成功。示例以下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "r+");

    if (file != NULL)
    {
        // 读写文件
    }
    else
    {
        // 显示一个错误提示信息
        printf("没法打开 test.txt 文件\n");
    }

    return 0;
}
复制代码

记得每次使用 fopen 函数时都要对返回值做判断,由于若是文件不存在或者正被其余程序占用,那可能会使当前程序运行失败。

fclose:关闭文件


close 表示“关闭”。

若是咱们成功地打开了一个文件,那么咱们就能够对文件进行读写了(读写的操做咱们下一节再详述)。

若是咱们对文件的操做已经结束,那么咱们应该关闭这个文件,这样作是为了释放占用的文件指针。

咱们须要调用 fclose 函数来实现文件的关闭,这个函数能够释放内存,也就是从内存中删除你的文件(指针)。

函数原型:

int fclose(FILE* pointerOnFile);
复制代码

这个函数只有一个参数:指向文件的指针。

函数的返回值(int)有两种状况:

  • 0 :当关闭操做成功时。
  • EOF(是 End Of File 的缩写,表示“文件结束”。通常等于 -1):若是关闭失败。

示例以下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "r+");

    if (file != NULL)
    {
        // 读写文件

        // ...

        fclose(file);  // 关闭咱们以前打开的文件
    }

    return 0;
}
复制代码

3. 读写文件的不一样方法


如今,咱们既然已经知道怎么打开和关闭文件了,接下来咱们就学习如何对文件进行读出和写入吧。

咱们首先学习如何写入文件(相比读出要简单一些),以后咱们再看如何从文件读出。

对文件写入

用于写入文件的函数有好几个,咱们能够根据状况选择最适合的函数来使用。

咱们来学习三个用于文件写入的函数:

  • fputc:在文件中写入一个字符(一次只写一个)。是 file put character 的缩写。put 表示“放入”,character 表示“字符”。

  • fputs:在文件中写入一个字符串。是 file put string 的缩写。string 表示“字符串”。

  • fprintf:在文件中写入一个格式化过的字符串,用法与 printf 是几乎相同的,只是多了一个文件指针。

fputc

此函数用于在文件中一次写入一个字符。

函数原型:

int fputc(int character, FILE* pointerOnFile);
复制代码

这个函数包含两个参数:

  • character:int 型变量,表示要写入的字符。咱们也能够直接写 'A' 这样的形式,以前 ASCII 那节的知识点没有忘吧。

  • pointerOnFile:指向文件的指针。

函数返回 int 值。若是写入失败,则为 EOF;不然,会是另外一个值。

示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "w");

    if (file != NULL)
    {
        fputc('A', file);  // 写入字符 A
        fclose(file);
    }

    return 0;
}
复制代码

上面的程序用于向 test.txt 文件写入字符 'A'。

fputs

这个函数和 fputc 相似,区别是 fputc 每次是写入一个字符,而 fputs 每次写入一个字符串。

函数原型:

int fputs(const char* string, FILE* pointerOnFile);
复制代码

相似地,这个函数也接受两个参数:

  • string:要写入的字符串。

  • pointerOnFile:指向文件的指针。

若是出错,函数返回 EOF;不然,返回不一样于 EOF 的值。

示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;

    file = fopen("test.txt", "w");

    if (file != NULL)
    {
        fputs("你好,朋友。\n最近怎么样?", file);
        fclose(file);
    }

    return 0;
}
复制代码
fprintf

这个函数颇有用,由于它不只能够向文件写入字符串,并且这个字符串是能够由咱们来格式化的。用法其实和 printf 函数相似,就是多了一个文件指针。

函数原型:

int fprintf(FILE *stream, const char *format, ...)
复制代码

示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    int age = 0;

    file = fopen("test.txt", "w");

    if (file != NULL)
    {
        // 询问用户的年龄
        printf("您几岁了 ? ");
        scanf("%d", &age);

        // 写入文件
        fprintf(file, "使用者年龄是 %d 岁\n", age);
        fclose(file);
    }

    return 0;
}
复制代码

从文件中读出


咱们能够用与写入文件时相似名字的函数,只是略微修改了一些,也有三个:

  • fgetc:读出一个字符。是file get character 的缩写。get 表示“获取,取得”。

  • fgets:读出一个字符串。是 file get string 的缩写。

  • fscanf:与 scanf 的用法相似,只是多了一个文件指针。scanf 是从用户输入读取,而 fscanf 是从文件读取。

此次介绍这三个函数咱们会简略一些,由于若是你们掌握好了前面那三个写入的函数,那这三个读出的函数是相似的。只是操做相反了。

fgetc

首先给出函数原型:

int fgetc(FILE* pointerOnFile);
复制代码

函数返回值是读到的字符。若是不能读到字符,那会返回 EOF。

可是如何知道咱们从文件的哪一个位置读取呢?是第三个字符处,仍是第十个字符处呢?

其实,在咱们读取文件时,有一个“游标”(cursor),会跟随移动。

这固然是虚拟的游标,你不会在屏幕上看到它。你能够想象这个游标和你用记事本编辑文件时的闪动的光标相似。这个游标指示你当前在文件中的位置。

以后的小节,咱们会学习如何移动这个游标,使其位于文件中特定的位置。能够是开头,也能够是第 7 个字符处。

fgetc 函数每读入一个字符,这个游标就移动一个字符长度。咱们就能够用一个循环来读出文件全部的字符。例如:

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    int currentCharacter = 0;

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        // 循环读取,每次一个字符
        do
        {
            currentCharacter = fgetc(file);  // 读取一个字符
            printf("%c", currentCharacter);  // 显示读取到的字符
        } while (currentCharacter != EOF);  // 咱们继续,直到 fgetc 返回 EOF(表示“文件结束”)为止

        fclose(file);
    }

    return 0;
}
复制代码
fgets

此函数每次读出一个字符串,这样能够没必要每次读一个字符(有时候效率过低)。

这个函数每次最多读取一行,由于它遇到第一个 '\n'(换行符)会结束读取。因此若是咱们想要读取多行,须要用循环。

插入一点回车符和换行符的知识: 关于“回车”(carriage return)和“换行”(line feed)这两个概念的来历和区别。 在计算机尚未出现以前,有一种叫作电传打字机(Teletype Model 33)的玩意,每秒钟能够打 10 个字符。 可是它有一个问题,就是打完一行换行的时候,要用去 0.2 秒,正好能够打两个字符。要是在这 0.2 秒里面,又有新的字符传过来,那么这个字符将丢失。 因而,研制人员想了个办法解决这个问题,就是在每行后面加两个表示结束的字符。一个叫作“回车”,告诉打字机把打印头定位在左边界;另外一个叫作“换行”,告诉打字机把纸向下移一行。这就是“换行”和“回车”的来历,从它们的英语名字上也能够看出一二。 后来,计算机被发明了,这两个概念也就被搬到了计算机上。那时,存储器很贵,一些科学家认为在每行结尾加两个字符太浪费了,加一个就能够。因而,就出现了分歧。在 Unix/Linux 系统里,每行结尾只有“<换行>”,即 "\n";在 Windows 系统里面,每行结尾是“<换行><回车>”,即 "\n\r";在 macOS 系统里,每行结尾是“<回车>”,即 "\r"。 一个直接后果是,Unix/Linux/macOS 系统下的文件在Windows里打开的话,全部文字会变成一行;而 Windows 里的文件在 Unix/Linux/macOS 下打开的话,在每行的结尾可能会多出一个 ^M 符号。 Linux 中遇到换行符会进行“回车 + 换行”的操做,回车符反而只会做为控制字符显示,不发生回车的操做。 而 Windows 中要“回车符 + 换行符”才会实现“回车+换行",缺乏一个控制符或者顺序不对都不能正确的另起一行。

函数原型:

char* fgets(char* string, int characterNumberToRead, FILE* pointerOnFile);
复制代码

示例:

#include <stdio.h>

#define MAX_SIZE 1000 // 数组的最大尺寸 1000

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    char string[MAX_SIZE] = "";  // 尺寸为 MAX_SIZE 的数组,初始为空

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        fgets(string, MAX_SIZE, file);  // 咱们读取最多 MAX_SIZE 个字符的字符串,将其存储在 string 中
        printf("%s\n", string);  // 显示字符串

        fclose(file);
    }

    return 0;
}
复制代码

这里,咱们的 MAX_SIZE 足够大(1000),保证能够容纳下一行的字符数。因此遇到 '\n' 咱们就中止读取,所以以上代码的做用就是读取文件中的一行字符,并将其输出。

那咱们如何可以读取整个文件的内容呢?很简单,加一个循环。

以下:

#include <stdio.h>

#define MAX_SIZE 1000 // 数组的最大尺寸 1000

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    char string[MAX_SIZE] = "";  // 尺寸为 MAX_SIZE 的数组,初始为空

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        while (fgets(string, MAX_SIZE, file) != NULL)  // 咱们一行一行地读取文件内容,只要不遇到文件结尾
        printf("%s\n", string);  // 显示字符串

        fclose(file);
    }

    return 0;
}
复制代码
fscanf

此函数的原理和 scanf 是同样的。负责从文件中读取规定样式的内容。

函数原型:

int fscanf(FILE *stream, const char *format, ...)
复制代码

示例:

例如咱们建立一个 test.txt 文件,在里面输入三个数:23, 45, 67。

输入的形式能够是相似下面这样:

  • 每一个数之间有空格

  • 每一个数之间换一行

#include <stdio.h>

int main(int argc, char *argv[])
{
    FILE* file = NULL;
    int score[3] = {0};  // 包含 3 个最佳得分的数组

    file = fopen("test.txt", "r");

    if (file != NULL)
    {
        fscanf(file, "%d %d %d", &score[0], &score[1], &score[2]);
        printf("最佳得分是 : %d, %d 和 %d\n", score[0], score[1], score[2]);

        fclose(file);
    }

    return 0;
}
复制代码

运行输出:

最佳得分是:23, 45, 67
复制代码

4. 在文件中移动


前面咱们提到了虚拟的“游标”,如今咱们仔细地来学习一下。

每当咱们打开一个文件的时候,实际上都存在一个“游标”,标识你当前在文件中所处的位置。

你能够类比咱们的文本编辑器,每次你在文本编辑器(例如记事本)里面输入文字的时候,不是有一个游标(光标)能够处处移动么?它指示了你在文件中的位置,也就是你下一次输入会从哪里开始。

总结来讲,游标系统使得咱们能够在文件中指定位置进行读写操做。

咱们介绍三个与文件中游标移动有关的函数:

  • ftell:告知目前在文件中哪一个位置。tell 表示“告诉”。

  • fseek:移动文件中的游标到指定位置。seek 表示“探寻”。

  • rewind:将游标重置到文件的开始位置(这和用 fseek 函数来使游标回到文件开始位置是一个效果)。rewind 表示“转回”。

ftell:指示目前在文件中的游标位置


这个函数使用起来很是简单,它返回一个 long 型的整数值,标明目前游标所在位置。函数原型是:

long ftell(FILE* pointerOnFile);
复制代码

其中,pointerOnFile 这个指针就是文件指针,指向当前文件。

相信没必要用例子就知道如何使用了吧。

fseek:使游标移动到指定位置


函数原型为:

int fseek(FILE* pointerOnFile, long move, int origin);
复制代码

此函数能使游标在文件(pointerOnFile 指针所指)中从位置(origin 所指。origin 表示“初始”)开始移动必定距离(move 所指。move 表示“移动”)。

  • move 参数:能够是一个正整数,代表向前移动;0,代表不移动;或者负整数,代表回退。

  • origin 参数:它的取值能够是如下三个值(#define 所定义的常量)中的任意:

    • SEEK_SET :文件开始处。SET 表示“设置”。
    • SEEK_CUR :游标当前所在位置。CUR 是 current(表示“当前”)的缩写。
    • SEEK_END :文件末尾。END 表示“结尾”。

来看几个具体使用实例吧:

// 这行代码将游标放置到距离文件开始处 5 个位置的地方
fseek(file, 5, SEEK_SET);

// 这行代码将游标放置到距离当前位置日后 3 个位置的地方
fseek(file, -3, SEEK_CUR);

// 这行代码将游标放置到文件末尾
fseek(file, 0, SEEK_END);
复制代码

rewind:使游标回到文件开始位置


这个函数的做用就至关于使用 fseek 来使游标回到 0 的位置

void rewind(FILE* pointerOnFile);
复制代码

相信使用难不倒你们吧,看函数原型就一目了然了。和 fseek(file, 0, SEEK_SET); 是一个效果。

5. 文件的重命名和删除


咱们来学习两个简单的函数,以结束此次的课程:

  • rename 函数:重命名一个文件(rename 表示“重命名”)。

  • remove 函数:删除一个文件(remove 表示“移除”)。

这两个函数的特殊之处就在于,不一样于以前的一些文件操做函数,它们不须要文件指针做为参数,只须要把文件的名字传给这两个函数就够了。

rename:重命名文件


函数原型:

int rename(const char* oldName, const char* newName);
复制代码

oldName 就是文件的“旧名字”,而 newName 是文件的“新名字”。

若是函数执行成功,则返回 0;不然,返回非零的 int 型值。

如下是一个使用的例子:

int main(int argc, char *argv[])
{
      rename("test.txt", "renamed_test.txt");

      return 0;
}
复制代码

很简单吧。

remove:删除一个文件


函数原型:

int remove(const char* fileToRemove);
复制代码

fileToRemove 就是要删除的文件名。

注意:remove 函数要慎用,由于它不会提示你是否确认删除文件。 文件是直接从硬盘被永久删除了,也不会先移动至垃圾箱。 想要再找回被删除的文件就只能借助一些特殊的软件了,可是恢复过程可能没那么容易,也不必定可以成功。

实例:

int main(int argc, char *argv[])
{
    remove("test.txt");

    return 0;
}
复制代码

6. 第二部分第八课预告


今天的课就到这里,一块儿加油吧!

下一课:C语言探索之旅 | 第二部分第八课:动态分配


我是 谢恩铭,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」

相关文章
相关标签/搜索