C语言文件操作
先唠点嗑。
学习函数,要学习函数的名字、功能、各个参数、不同情况下的返回值,最好敲一段代码,由体会,到记忆,再到理解。
如同许多要学习的事物一样,入门编程,并不是像局外人印象中,随随便便敲一个烟花代码,的帅气与洒脱。而是,蹒跚学步的艰难,与无所适从。往往,自主解决一个,很简单的问题,都需要试很多次,甚至出不来。
不说什么鼓励的话,只要你能明确自己的目标,并且每天一定,一定,一定要做点事,做一点能靠近目标的事。
今天做实验割到手了,出了很多血。所以做事要小心。
1、文件的由来
我们打印在屏幕(也叫标准信息流)上的信息,在退出程序之后,就会被清除。也就是说,打印在屏幕上的信息,是临时的。
如何持久保存呢?这就涉及到了文件。
2、文件的分类
一般文件,分两类:程序文件、数据文件。
程序文件,有源程序文件.c
、目标文件Windows: .obj
、可执行程序文件Windows: .exe
。
数据文件,顾名思义,就是存放数据的文件。
3、数据文件
数据文件,可被分为:文本文件、二进制文件。
文本文件,可以被理解为存放字符ASCII值的文件,在文件中,每个字符被翻译为ASCII数值,并存下来。
二进制文件,可以被理解为,将数据转化为二进制序列,并存在文件中。
比如,存数据10000
存文本文件,分别将1 (ASCII码值为48) 0 (ASCII码值为49) 0 (ASCII码值为49) 0 (ASCII码值为49) 0 (ASCII码值为49)
存入文件。
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("data003.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
fputs("10000", fp);
fclose(fp);
fp = NULL;
return 0;
}
存二进制文件,将10000
翻译为:
00000000000000000010011100010000 00000000000000000010011100010000 00000000000000000010011100010000
再存入文件。
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("data004.txt", "wb");
if (fp == NULL)
{
perror("fopen");
}
int a = 10000;
fwrite(&a, sizeof(int), 1, fp);
fclose(fp);
fp = NULL;
return 0;
}

10 27 00 00,翻译一下,注意大小端,就是10000。
4、文件的打开与关闭
文件的类型,与其包含的内容,多种多样。为了方便对文件的操作,我们抽象出流的概念。
可以想象下面这样一种场景:
许多条不同的河流,汇入同一条河道,又分离到不同的河道之中。
上面说的同一条河道,类似于流。我们可以在文件中,输入信息,然后打印在屏幕上;可以在屏幕上,输入信息,然后输出到文件中。
4.1、标准流
C语言程序启动时,自动打开了3种流:
stdin
: 标准输入流,可以在屏幕上输入信息。stdout
: 标准输出流,可以在屏幕上输出信息。stderr
: 标准错误流。
这三个标准流的类型,都是FILE*
。
那什么又是FILE*
?
4.2、FILE*
FILE*
,称为文件类型指针,简称文件指针。
在vs 2022中,FILE*
本质上,是一种结构体类型。
我们不讨论那么多,只需要知道,文件类型变量创建方式:
c
#include<stdio.h>
int main()
{
FILE* fp;
return 0;
}
通过指针fp
,我们就可以找到,其指向的文件。
4.3、打开和关闭的基本操作
关闭函数fclose()
需要头文件<stdio.h>
声明:int fclose(FILE* stream);
- 功能是关闭文件(的流)。
stream
指向要关闭的流。- 成功关闭,返回0;失败,返回
EOF
。
打开函数fopen()
需要头文件<stdio.h>
声明:FILE* fclose(FILE* stream, const char* mode);
- 功能是打开文件(的流)。
stream
指向要打开的流,mode
指向打开的方式。- 成功关闭,返回该文件的指针;失败,返回
NULL
。 - 注意,创建了文件,最好判断一下,其可用性。
例如,我们以只写的形式,打开文件"myfile.txt"
:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("myfile.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int ret = fputs("Hello world", fp);
if (ret == EOF)
{
printf(""\n);
if (feof(fp))
printf("文件遇到末尾\n");
else if (ferror(fp))
printf("文件读写发生错误\n");
}
fclose(fp);
fp = NULL;
return 0;
}

下面是一些常见的文件打开方式:
打开方式 | 含义 | 如果指定文件不存在 |
---|---|---|
r | 为了输入数据,打开一个已经存在的文件 | 出错 |
w | 为了输出数据,打开一个文件 | 创建新的文件 |
a | 为了追加数据,打开一个文件 | 创建新的文件 |
5、文件的顺序读写
5.1、fputc()
c
int fputc(int character, FILE* stream);
- 功能:向所有输出流中输出信息。
character
为要输出字符的ASCII码值,stream
指向输出流- 函数返回成功,返回要输出字符的ASCII码值;返回失败,返回
EOF
,可由函数ferror()
检查。
将信息输出到屏幕:
c
#include<stdio.h>
#include<errno.h>
int main()
{
fputc('a', stdout);
printf("\n");
return 0;
}

输出到指定文件:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("file001.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
//这里我们可以借助循环,输出信息到文件
for (int i = 'a'; i <= 'z'; i++)
{
fputc(i, fp);
}
fclose(fp);
fp = NULL;
return 0;
}

5.2、fgetc()
c
int fgetc(FILE* stream);
- 功能:在流中读取字符。
stream
指向流。- 返回成功,返回读取字符的ASCII码值;返回失败,有两种情况:
- 若已处于文件末尾,可用
feof()
检查。 - 若发生读写错误,可用
ferror()
检查。
- 若已处于文件末尾,可用
在标准流中读取字符并打印:
c
#include<stdio.h>
int main()
{
int ret = fgetc(stdin);
fputc(ret, stdout);
printf("\n");
return 0;
}

在上面创建的file001.txt
文件中,读取前十个字符:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("file001.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
for (int i = 0; i < 10; i++)
{
int ret = fgetc(fp);
putchar(ret);
}
fclose(fp);
fp = NULL;
return 0;
}

5.3、feof()
c
int feof(FILE* stream);
文件读取遇到末尾时,函数会在对应流上,放置文件结束的指示符,feof()
的作用就是检测这个指示符,也就是检测读取文件是否遇到结尾。
如果检测到指示符,feof()
返回非0值,否则返回0。
测试feof()
:
c
#include<stdio.h>
#include<errno.h>
//假设一个文件中有6个字符,但我们硬是要读取10个字符
int main()
{
FILE* fp = fopen("file002.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
for (int i = 0; i < 10; i++)
{
int ret = fgetc(fp);
putchar(ret);
if (ret == EOF)
{
printf("\n");
if (feof(fp))
printf("文件读取遇到末尾\n");
else if (ferror(fp))
printf("文件读取出错\n");
}
}
printf("\n");
fclose(fp);
fp = NULL;
return 0;
}

5.4、ferror()
c
int ferror(FILE* stream);
如果文件读写发生错误,函数会在对应流上,放置文件结束的指示符,ferror()
的作用是检测这个指示符,也就是检测文件读写是否遇到错误。
如果检测到发生错误,ferror()
返回非0值,否则返回0。
测试ferror()
c
#include<stdio.h>
#include<errno.h>
//如果以只写的方式打开文件,再去读文件,会出错
int main()
{
FILE* fp = fopen("file003.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int ret = fgetc(fp);
if (ret == EOF)
{
printf("\n");
if (feof(fp))
printf("文件读取遇到末尾\n");
if (ferror(fp))
printf("文件读取出错\n");
}
fclose(fp);
fp = NULL;
return 0;
}

5.5、fputs()
c
int fputs(const char* str, FILE* stream);
- 功能:将指定字符串,输出到指定流中。字符串必须包含
\0
,同时读到\0
停止。 stream
指向输出到的流。- 成功,返回非0值;失败,返回
EOF
,可被检测到。
函数测试:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("file004.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
fputs("abc\0def\n", fp);
fputs("xxxxx", fp);
fclose(fp);
fp = NULL;
return 0;
}

5.6、fgets()
c
char* fgets(char* str, int num, FILE* stream);
- 功能:读取流中指定长度的字符串,并保存至
str
中。 str
指向要保存到的字符串的空间。num
表示要读取字符的个数。由于fputs()
会将字符串末尾的\0
一起读取,实际读取的最大字符数,就为num - 1
。- 成功,返回
str
;失败,返回NULL
,存在两种情况:- 若已处于文件末尾,可用
feof()
检查。 - 若发生读写错误,可用
ferror()
检查。
- 若已处于文件末尾,可用
函数测试:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("file002.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
char str1[] = "********";
fgets(str1, sizeof(str1), fp);
return 0;
}
调试下:
注意两点:
1、对num - 1
的理解。
比如我们想在文件内容为abcdef
中,读取前3个字符。但是监视窗口中,str
只有ab
2个,和\0
:
2、被读取文件内容中,出现换行的情况。
这时,在换行处,写入\n
,然后以\0
结尾:
5.7、fprintf()
fprintf()
,与printf()
的功能类似,但是前者已经不限于标准流,而是任何文件流。
c
int fprintf(FILE* stream, const char* format, ...);
- 功能就是打印信息。
stream
指向打印到的文件流,剩下的参数与printf()
是类似的。- 成功时,
fprintf()
返回写入字符的总数;失败时,返回EOF
,可用ferror()
检测。
函数测试:
c
#include<stdio.h>
#include<errno.h>
typedef struct stu
{
char name[30];
int age;
char sex[10];
}stu;
//创建一个结构体,然后打印到文件中
int main()
{
stu t0 = { "zhangsan", 18, "male" };
FILE* fp = fopen("file005.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int ret = fprintf(fp, "%s %d %s", t0.name, t0.age, t0.sex);
printf("%d\n", ret);
fclose(fp);
fp = NULL;
return 0;
}

5.8、fscanf()
与scanf()
类似。
c
int fscanf(FILE* fp, const char* format, ...)
- 功能是输入信息到流,与
fprintf()
对应。 - 参数依次是指向的流...
- 函数返回成功输入数据的个数。若到达结尾,返回
EOF
;若输入失败,返回EOF
。可检查错误。
函数测试:
c
#include<stdio.h>
#include<errno.h>
typedef struct stu
{
char name[30];
int age;
char sex[10];
}stu;
//创建一个结构体,然后打印到文件中
int main()
{
stu t0 = { 0 };
FILE* fp = fopen("file005.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int ret1 = fscanf(fp, "%s %d %s", t0.name, &t0.age, t0.sex);
int ret2 = fprintf(stdout, "%s %d %s", t0.name, t0.age, t0.sex);
printf("\n");
printf("%d %d\n", ret1, ret2);
fclose(fp);
fp = NULL;
return 0;
}

5.9、fwrite()
c
size_t fwrite(char* str, size_t size, size_t count, FILE* stream);
功能:将str
指定的数据块内容,以二进制的形式,写入stream
指定的流中。
参数:
str
:指向被写入的空间。size
:表示每个写入数据的大小(字节)。count
:表示写入数据的个数。stream
:指向要写入的文件流。
返回值:如果成功,返回count
;如果发生错误,返回值可能小于count
。
函数测试:
c
#include<stdio.h>
#include<errno.h>
typedef struct std
{
char name[30];
int age;
float score;
}std;
int main()
{
std s1 = { "lisi", 18, 95.5f };
FILE* fp = fopen("file006.txt", "wb");
if (fp == NULL)
{
perror("fopen");
return -1;
}
if (fwrite(&s1, sizeof(std), 1, fp) != 1)
{
perror("fwrite");
return -1;
}
fclose(fp);
fp = NULL;
return 0;
}
用自带笔记本打开,自然是一堆乱码。
用vs 2022自带的二进制编辑器打开,是这个样子:
依旧看不懂。
既然用二进制形式写入,那么可以读取出来吗?用什么函数?
答案是fread()
5.10、fread()
c
size_t fread(char* str, size_t size, size_t count, FILE* stream);
功能:将stream
指向文件流中二进制形式存储的内容,读取并存放到str
指向的空间。
参数:
str
:指向读取到的空间。size
:表示每个读取数据的大小(字节)。count
:表示读取数据的个数。stream
:指向被读取的文件流。
返回值:如果成功,返回count
;如果发生错误,返回值可能小于count
。
函数测试:
c
#include<stdio.h>
#include<errno.h>
typedef struct std
{
char name[30];
int age;
float score;
}std;
//输入到文件流,再输出出来
int main()
{
//std s1 = { "lisi", 18, 95.5f };
FILE* fp = fopen("file006.txt", "rb");
if (fp == NULL)
{
perror("fopen");
return -1;
}
//if (fwrite(&s1, sizeof(std), 1, fp) != 1)
//{
// perror("fwrite");
// return -1;
//}
std s2 = { 0 };
if (fread(&s2, sizeof(std), 1, fp) != 1)
{
perror("fread");
return -1;
}
printf("%s %d %f", s2.name, s2.age, s2.score);
fclose(fp);
fp = NULL;
return 0;
}

6、对比一组函数
我们在前面就知道:
scanf()/printf()
:对于标准输入/输出流的格式化数据的输入函数/输出函数。fscanf()/fprintf()
:对于所有输入/输出流的格式化数据的输入函数/输出函数。
我们再来补充一组:sscanf()/sprintf()
。
6.1、sprintf()
c
int sprintf(void* ptr, const char* format, ...);
功能:将格式化数据,写入到ptr
指向的空间中。
参数:
ptr
:指向要输出到的空间。format
:格式化字符串,如%d %s %f
。...
:可变参数列表。
返回值:返回成功输出的字符数。
函数测试:
c
#include<stdio.h>
#include<errno.h>
typedef struct std
{
char name[100];
int age;
float score;
}std;
int main()
{
//将结构体内容,改写为字符串
std s1 = { "lisi", 18, 95.5f };
char str[100] = { 0 };
sprintf(str, "%s %d %f", s1.name, s1.age, s1.score);
printf("%s\n", str);
return 0;
}

输是输进字符数组了,怎么从字符数组输出呢?
用sscanf()
。
6.2、sscanf()
c
int sscanf(void* ptr, const char* format, ...);
功能:读取ptr
指向的空间中内容,以格式化数据的形式。
参数:
ptr
:指向被读取到的空间。format
:格式化字符串,如%d %s %f
。...
:可变参数列表。
返回值:返回成功输出的数据个数,若失败,返回EOF
。
函数测试:
c
#include<stdio.h>
#include<errno.h>
typedef struct std
{
char name[100];
int age;
float score;
}std;
int main()
{
//将字符串内容,改写为结构体
std s2 = { 0 };
char str[100] = "zhangsan 20 85.5";
sscanf(str, "%s %d %f", s2.name, &s2.age, &s2.score);
printf("%s %d %f\n", s2.name, s2.age, s2.score);
return 0;
}

总结:
函数 | 区别 |
---|---|
printf() |
针对标准输出流的格式化的输出函数 |
scanf() |
针对标准输入流的格式化的输入函数 |
fprintf() |
针对所有输出流的格式化的输出函数 |
fscanf() |
针对所有输入流的格式化的输入函数 |
sprintf() |
将格式化数据转化为字符串 |
sscanf() |
在字符串中提取格式化数据 |
7、随机读写
上面提到的,都是按顺序来的,那有没有想读哪就读哪的方法?
7.1、fseek()
c
int fseek(FILE* stream, long int offset, int origin);
功能:移动光标到指定位置。
参数:
stream
:文件流。offset
:偏移量。origin
:指定光标的位置,有起始位置SEEK_SET
、当前位置SEEK_CUR
、结束位置SEEK_END
。
例如,向已输入一串字母文件中,读取特定字母:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("text001.txt", "r");
if (fp == NULL)
{
perror("fopen");
return -1;
}
int ret1 = fgetc(fp);
putchar(ret1);
printf("\n");
fseek(fp, 3, SEEK_CUR);
int ret2 = fgetc(fp);
putchar(ret2);
printf("\n");
fclose(fp);
fp = NULL;
return 0;
}
7.2、ftell()
c
long int ftell(FILE* stream);
功能:返回文件光标相对于起始位置的偏移量。
例如,可以用此计算一串字母中字符的个数:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("text002.txt", "r");
if (fp == NULL)
{
perror("fopen");
return -1;
}
fseek(fp, 0, SEEK_END);
long int ret = ftell(fp);
printf("%ld\n", ret);
fclose(fp);
fp = NULL;
return 0;
}
7.3、rewind()
c
void rewind(FILE* stream);
功能:将文件内容中的光标移到起始位置。
例如,向文件输入26个英文字母,再输入到字符串中:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("text002.txt", "w+");
if (fp == NULL)
{
perror("fopen");
return -1;
}
for (int i = 'a'; i <= 'z'; i++)
{
fputc(i, fp);
}
rewind(fp);
fflush(fp);
char buffer[27];
fread(buffer, sizeof(char), 26, fp);
buffer[26] = '\0';
printf(buffer);
fclose(fp);
fp = NULL;
return 0;
}
8、文件缓冲
数据从程序输入到硬盘,或从硬盘输入到程序,其实不是一下子就搞定的,而是中间经过了文件缓冲区。
c
int fflush(FILE* stream);
功能:强制刷新文件缓冲区。
- 对输出流,将缓冲区的数据全部写入文件。
- 对输入流,该行为不是C语言标准行为。
- 参数为
NULL
时,刷新所有打开的输出流。
例如,编写一段代码,佐证文件缓冲区的存在:
c
#include<stdio.h>
#include<errno.h>
#include<windows.h>
int main()
{
FILE* fp = fopen("text003.txt", "w");
if (fp == NULL)
{
perror("fopen");
return -1;
}
fputs("abcdef", fp);
printf("缓冲10秒,此时文件无内容。\n");
Sleep(10000);
printf("刷新文件\n");
fflush(fp);
printf("缓冲10秒,此时文件有内容。\n");
fclose(fp);
fp = NULL;
return 0;
}
8、更新文件
打开文件的方式,其实还有"r+ w+ a+"
。
其实它们都有同时读写的含义,但是使用时,必须注意两点:
- 写入数据后,一定用
fflush()
进行刷新。 - 重新写数据时,一定用
fseek() rewind()
重新定光标的位置。
例如,我们写上字符串abcdefghi
,并找到字符c
,然后写入hello
:
c
#include<stdio.h>
#include<errno.h>
int main()
{
FILE* fp = fopen("text004.txt", "w+");
if (fp == NULL)
{
perror("fopen\n");
return -1;
}
fputs("abcdefghi", fp);
fflush(fp);
rewind(fp);
fseek(fp, 2, SEEK_CUR);
int ret = fgetc(fp);
printf("%c\n", ret);
fseek(fp, -1, SEEK_CUR);
fputs("hello", fp);
fclose(fp);
fp = NULL;
return 0;
}