全文目录
引言
为什么需要文件操作
我们在编写代码时,会创建许多的变量。为了提高读取速度,这些变量都是在内存中开辟空间存放的。为了保证别的项目在运行时的效率,在该项目结束后这些空间都必须被释放(包括在栈区的与堆区的等)。所以我们在内存中存储的变量在再次运行时就不会被保留,需要重新输入。
如果我们要实现一个通讯录或一个图书管理系统,如果每次关闭这个系统时里面的数据都被清空当然是不行的。所以我们需要在关闭项目前将这些数据存放到外存(硬盘)中,关闭程序后内存释放,但数据已经录入到外存,下次使用数据的时候再从外存中取出数据,再为这些数据在内存中开辟空间使用即可。
使用文件,我们就可以将数据存放在电脑的硬盘上实现其持久化保存。
接下来我们就来了解一下如何将数据写入文件与如何从文件中读取数据:
文件的打开与关闭
文件指针
在打开某个文件之前,我们需要找到这个文件的位置。文件指针就有这样的作用:
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息。这个信息区实际就是一个结构体变量,系统声明时,将其重命名为FILE(不同编译器中FILE的成员有一些差别)。
每当打开一个文件时,系统会自动创建一个FILE型的结构体变量。
指向这个FILE结构体的指针就是文件指针,类型是FILE*。FILE* pf;
我们一般通过文件指针来访问FILE这个结构体。
在打开与关闭文件时,就需要通过文件指针:
文件的打开与关闭
fopen
函数声明
fopen是文件打开库函数,我们可以查询到它的声明:
fopen在头文件stdio.h中声明。
这个函数有两个参数:
第一个参数类型是const char*。表示要打开的文件标识;
第二个参数类型是const char*。表示打开文件的方式。
返回值类型是FILE*。打开成功时返回这个文件打开后在内存中的文件信息区结构体的地址;打开失败时返会NULL。
打开文件后,系统就会为这个文件创建一个文件信息区,并返回这个文件信息区的指针。
文件标识由文件路径、文件名主干与文件后缀组成。而文件路径又分为绝对路径与相对路径。
例如:c:\vs\qqq的代码.txt。这就是一个绝对路径,在c盘中的vs下的qqq的代码这个文件,这个文件的后缀是.txt表示这是一个文本文件。
需要注意的是:我们如果将这个绝对路径的文件标识作为参数,fopen就会在所示的路径下去寻找文件;但如果我们直接将文件名主干与文件名后缀作为参数,fopen就会在这个.c文件当前目录中寻找文件打开。
打开的方式由参数mode决定。
文件的打开方式
这些字符每一种都是一种打开方式:
参数(打开方式) 含义 找不到时
"r" 为输入数据,打开一个已经存在的文本文件 出错
"w" 为了输出数据,新建一个文本文件
"a" 向文本文件的末尾添加数据 建立一个新文件
"rb" 为了输入数据,打开一个二进制文件 出错
"wb" 为了输出数据,新建一个二进制文件
"ab" 向一个二进制文件的末尾添加数据 出错
"r+" 为了读写,打开一个文本文件 出错
"w+" 为了读写,新建一个文本文件
"a+" 打开一个文本文件,在文件的末尾进行读写 建立一个新文件
"rb+" 为了读写,打开一个二进制文件 出错
"wb+" 为了读写,新建一个二进制文件
"ab+" 打开一个二进制文件,在文件末尾读写 建立一个新文件
首先要说明的是,这里的输入数据是指从文件向内存中输入数据,就是读取数据;输出就是从内存向文件中输出数据,就是写入数据。
不难发现,这其中是有一些规律的。
需要注意的是:对于w类的。它的含义是在该路径下(绝对或相对)建立一个名为xxx的文件。当该路径下已有名为xxx的文件时,会删除原文件,并创建一个新的文件。
函数使用
例如:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
return 0;
}
在这段代码中,fopen的第一个参数是"test.txt";第二个参数是"r"。表示在当前目录中以只读的方式打开test.txt文件。if判断,当返回值为NULL时,即打开失败。此时,我们可以使用库函数perror来打印出错的原因。这个库函数的参数可以作为标识。
由于我并没有在这个目录中创建test的文本文件,所以打印出的错误是:没有这样的文件或目录。
这一步只是以某种方式打开文件,而具体读写文件的操作需要一些库函数来实现,马上就会介绍。
fclose
在打开文件后自然需要关闭文件。我们可以使用库函数fclose。
函数声明
我们可以查询到这个函数的声明:
fclose库函数在头文件stdio.h中声明。
它有一个参数,类型为FILE*。表示要关闭的文件的文件指针(打开文件成功后会创建一个文件信息区)。
返回值类型为int。如果关闭成功,返回0;关闭失败,返回EOF。
函数使用
例如:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
现在,我已经创建好了一个test.txt文件了,所以能够成功打开。
但是这段代码并没有对这个文件进行读写,所以这个文件中还是空的:
文件的顺序读写
刚才的库函数只是打开了某个文件。接下来介绍的这些函数,就可以实现在文件的顺序读写:
fgetc与fputc
fgetc是字符输入函数,适用于所有输入流;fputc是字符输出函数,适用于所有输出流。
函数声明
这两个函数在声明在头文件stdio.h中:
fgets有一个参数,类型为FILE*。表示从FILE*指定的流中读取指示符当前的字符到内存中(即将字符输入到内存中)。读取后,标识符向前移动一个字符(这里的标识符可以理解为光标)。
返回值类型是int。如果读取成功,返回这个字符的ASCII码值;读取失败,返回EOF(这里有两种情况:是读取结束导致的,还是读取出错导致的?这个问题我们在下一篇文章中会介绍)。
fputc有两个参数:
第一个参数的类型是int。表示要输出的字符的ASCII码值;
第二个参数的类型是FILE*。表示要将这个字符输出到FILE*指定的流中。
输出后,标识符向前移动一个字符。
返回值类型为int。如果输出成功,返回这个字符的ASCII码值;输出失败,返回EOF。
函数使用
例如:
//fputc的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int i = 0;
while (fputc('a', pf) != EOF&&i < 5)//写入
{
i++;
}
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
这段代码使用了"w"输出的方式在当前目录中打开了一个test.txt文件,然后判断是否打开成功。然后while循环,使用fputc函数在这个文本文件中写入了5个字符a。最后关闭文件。
//fgetc的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int c = 0;
while (c!=EOF)//读取
{
c = fgetc(pf);
printf("%c ", c);
}
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
在这段代码中:
使用只读"r"的方式打开当前目录中的test.txt文件,并判断是否打开成功。然后while循环将test.txt文件中的所有字符字符依次存储到int型的变量c中并打印。最后关闭文件。
fgets与fputs
fgets是文本行输入函数,适用于所有输入流;fputs是文本行输出函数,适用于所有输出流。
函数声明
这两个函数在声明在头文件stdio.h中:
fgets有三个参数:
第一个参数类型为char*。表示将指定流中的字符串输入到的空间的首地址;
第二个参数类型为int。表示读取的字符的个数;
第三个参数类型为FILE*。表示从FILE*指定的流中读取指示符当前的num个字符到str指向的空间中(即将字符串输入到内存中)。读取后,标识符向前移动num个字符。
返回值为char*。读取成功时返回第一个参数str;失败时返回NULL(这里依旧有两种情况:是读取到末尾了,还是读取错误了?)。
需要注意的是,由于存入内存的是字符串,所以’\0’也要占一个位置。
fputs有两个参数:
第一个参数类型为const char*。表示要输出到指定流的字符串;
第二个参数类型为FILE*。表示将str指向的字符串中的内容输出到FILE*指定的流中。输入后,标识符向后移动到字符串的末尾。
需要注意的是,fputs输出字符串的结束标志是’\0’,并且在输出字符串时不输出’\0’。
返回值类型为int。输出成功时返回一个非负值;输出失败时,返回EOF。
函数使用
例如:
//fputs的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s1[] = "hallo kitty";
char s2[] = "hallo world";
fputs(s1, pf);//写入s1
fputs(s2, pf);//写入s2
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
在这段代码中:
首先以"w"的方式在当前目录中打开test.txt文件,并判断是否打开成功("w"的方式打开文件实际上是创建一个新的文件)。然后创建两个字符串s1与s2,并使用fputs将这两个字符串输出到test.txt文件中(由于输入完s1之后,标识符移动到末尾,再输入s2时就不会出现覆盖的错误)。最后关闭文件。
//fgets的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s[5] = { 0 };
while (fgets(s, 5, pf) != NULL)
{
printf("%s\n", s);
}
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
这段代码中:
首先以"r"的方式在当前目录中打开test.txt文件,并判断是否打开成功。然后创建一个字符串s。while循环将文本文件test.txt中的字符4个字符4个字符地依次存放在字符串s中,并打印(由于’\0’占一个字符,所以输入参数为5,只读取4个字符)。当读取结束后跳出循环。最后,关闭文件。
需要注意的是,创建的用于存储字符串的数组大小不能小于一次读取的量,否则就会出现数组越界的问题。
fscanf与fprintf
fscanf是格式化输入函数,适用于所有输入流;fprintf是格式化输出函数,适用于所有输出流。
其实这两个函数可以与我们熟知的scanf与printf函数结合理解:
scanf是从标准输入流输入格式化的数据到内存;printf是将格式化的数据以标准输出流的方式输出。
而fscanf可以将所有输入流的数据格式化的输入到内存;fprintf可以将内存中的数据格式化的输出到任何输出流。
标准输入流就是键盘,标准输出流就是屏幕。而我们今天所介绍的文件也是一种流。只不过标准输入流与标准输出流在程序运行的时候系统会自动打开,而其他的流需要我们自己打开。例如我们通过fopen打开一个文件流。
格式化就是整型的格式、浮点型的格式、字符的格式、字符串的格式等。我们用"%d"输出就是按照有符号的十进制整型输出;用"%c"输出就是按照字符输出;用"%s"输出就是按照字符串输出。输入时同理。这些相信大家在使用scanf与printf的时候就已经掌握了。
函数声明
这两个函数在声明在头文件stdio.h中:
我们可以看到,这两个函数的参数类型都有FILE*与const char*型,并且后面都有"…“。这里的”…"表示可变参数列表。
返回值类型都为int。表示成功输入/输出的数据的个数。
其实,这两个函数的参数与scanf、printf只相差了前面表示指定流的FILE*:
函数使用
在使用时,也是与scanf、printf类似,只是前面需要指定流。
例如我们将数据格式化的输出到文件流:
//fprintf的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s1[] = "hello kitty";
char s2[] = "hello world";
fprintf(pf,"%s%s", s1, s2);
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
将文件流中的数据格式化的输入到内存中:
//fscanf的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
char s1[20] = { 0 };
char s2[20] = { 0 };
char s3[20] = { 0 };
fscanf(pf, "%s%s%s", s1, s2, s3);
printf("%s\n%s\n%s\n", s1, s2, s3);
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
fread与fwrite
fread是二进制输入函数,只适用于文件流;fwrite是二进制输出函数,只适用于文件流。
函数声明
这两个函数在声明在头文件stdio.h中:
fread有4个参数:
第一个参数类型为void*。表示存储读取到的数据的空间的指针(void*可以接收任意类型的指针变量);
第二个参数类型为size_t(无符号整型)。表示读取的每个元素的大小;
第三个参数类型为size_t。表示读取count个大小为size的元素;
第四个参数类型为FILE*。表示从指定的FILE*文件流中读取数据。
返回值为size_t。返回成功读取的元素的个数。
如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。下一篇文章会介绍如何检查。
当size或count某一个为0时,函数不进行操作,并返回0。
fwrite有4个参数:
第一个参数类型为void*。表示将这个ptr指针指向的空间中的元素写入文件(void*可以接收任意类型的指针变量);
第二个参数类型为size_t(无符号整型)。表示输出的每个元素的大小;
第三个参数类型为size_t。表示输出count个大小为size的元素;
第四个参数类型为FILE*。表示将数据输出到指定的FILE*文件流中。
返回值为size_t。返回成功写入的元素总数。
如果此数字与 count 参数不同,则写入错误阻止函数完成。
如果大小或计数为零,则该函数返回零。
函数使用
例如:
//fwrite的使用
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int nums[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int i = 0;
while (i < 10 && fwrite(nums + i, sizeof(int), 1, pf) == 1)
{
i++;
}
int ret = fclose(pf);
if (ret != EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
由于fwrite是用于在二进制文件中写入数据,所以在打开文件时需要使用"wb"的方式。
并且在使用记事本打开时是无法读取的,所以是乱码。
当我们再使用二进制的方式读取时,即可获得其数据:
#include<stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
}
else
{
printf("打开成功\n");
int i = 0;
int n = 0;
while (i < 10 && fread(&n, sizeof(int), 1, pf) == 1)
{
printf("%d ", n);
}
int ret = fclose(pf);
if (ret!=EOF)
{
printf("关闭成功\n");
}
}
return 0;
}
而正是因为这些函数在读写数据的时候,每读或写一个单位的数据,标识符就会向前移动一个单位的长度。所以这些函数只能实现数据的顺序读写。其实还有一些函数可以改变标识符的位置,从而实现乱序读写,在下一篇文章中就会介绍。
总结
在这篇文章中我们了解了文件的打开与关闭、文件的顺序读写的相关函数。
其实有一些内容是没有介绍到的。比如当读写结束后原因的判断,到底是因为读写到了末尾,还是读写错误;比如二进制文件与文本文件;比如文件的随机读写等一些相关内容。
这些内容都会在下一篇文中介绍。
最后,如果对本文有任何问题,欢迎在评论区进行讨论
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦