本篇博客用于整理和介绍C语言中文件操作的相关知识,主要介绍基本的文件读写函数,并且举例演示如何使用这些函数。
目录
文件的基本定义
在程序设计中,我们一般谈到的文件分为程序文件和数据文件。
程序文件:包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
数据文件:文件内容不是程序,而是程序运行时读写的数据。比如说我们输入通讯录的联系人的信息。
这篇博客中所说的文件操作,指的是对数据文件的读写操作。
文件名
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt
一般在某个文件夹下,能够区分文件的文件主干+文件后缀就是文件名,比如说在C盘的code文件夹中,test.txt就是通常认为的文件名,但实际上应该要包括文件路径。
数据文件的分类
数据文件一般分为文本文件和二进制文件。
二进制文件: 数据在内存中是以二进制存储的,不加转换输出的外存的文件就是二进制文件。
文本文件: 如果在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
以ASCII字符型形式存储就指的是将数据看成字符,然后以字符的ASCII码值存储。
文件类型的指针
数据文件的处理是采用“缓冲文件系统”来处理数据文件的。系统会自动给程序中正在使用的数据文件在内存上开辟一个缓冲区,程序输出时将数据输入缓冲区,缓冲区满后输出到磁盘;程序输入时先从磁盘上读取一批数据放入内存,然后再根据需要一个个输入到程序数据中。这样做可以提高数据读写效率,因为内存上的数据读写速度更快。
缓冲文件系统中,每个正在使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息,这些信息存放在一个结构体(FILE
)中,在这里我们不用过分关注这些信息都是什么,我们的重点是如何进行文件读写。总结就是:我们进行操作的文件是FILE类型,我们在操作文件的时候创建一个FILE*指针来操作。
文件操作
在了解上述文件基本概念的基础之上,我们就可以进行文件操作了。
文件的打开、关闭和创建
文件读写的重要步骤就是打开和关闭,我们在使用文件时打开文件,在使用结束后一定要关闭文件。
文件的打开函数:
FILE *fopen( const char *filename, const char *mode )
参数:filename是文件名,mode是打开模式
返回值:FILE*类型的指针。文件打开或者创建之后,返回指向文件的指针,相当于建立了与该文件的联系;如果返回空指针说明出错。
作用:打开或者建立一个文件
下表是mode参数的选择:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 出错 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
在要打开的文件未创建的时候,选择合适的参数可以建立一个新的文件,所以说文件的创建也是用fopen函数来实现的。
文件关闭函数:
int fclose( FILE *stream );
参数: stream指的是指针变量
返回值: 关闭成功返回0,失败返回EOF
作用:关闭文件,将文件数据写入磁盘,可以防止文件一直被独占打开,防止数据丢失。
文件顺序读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
关于上述函数的命名需要解释一下:这块的输入指的是从磁盘到内存的输入(或者是从键盘到内存的输入),输出指的是从内存到磁盘的输出(或者是从内存到终端的输出)。举例来说,文件中的数据是放在磁盘上的,程序产生的数据在内存上,最终我们要将数据输出到磁盘中,要是想得到数据,那么就要从磁盘输入到内存中。
总结:输入输出的对象是内存。输入是往内存中输入,输出是从内存中输出。
fputc()
函数原型:
int fputc( int c, FILE *stream );
参数: c指的是字符,stream是指向要操作的文件的指针变量
返回值:int型的字符(字符的ASCII值),获取失败了返回EOF
作用:在文件中写入一个字符。(从内存往文件输出一个字符)
// 文件操作
int main()
{
FILE* pf = fopen("test.txt", "w"); // 以只写的方式打开文件test.txt,如果文件未定义,那么创建该文件再写入
if (pf == NULL) // 判断是否打开成功
{
perror("fopen");
return -1;
}
// 写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fputc('e', pf);
fputs("\nHello world!\n",pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgetc()
函数原型:
int fgetc( FILE *stream );
参数: stream是指向要操作的文件的指针变量
返回值:int型的字符(字符的ASCII值),获取失败了返回EOF
作用:读取文件中的一个字符。(从文件往内存中输入一个字符)
举例:对数据进行顺序读取
还是对上面创建的test.txt
文件进行操作`
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
// 读文件
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
上述代码是对test.txt文件中的内容进行顺序读取,是从文件中的第一个字符开始一个一个往后读取。
fputs()
函数原型:
int fputs( const char *string, FILE *stream );
参数:string是要写入到文件中的字符串的指针变量;stream是指向要操作的文件的指针变量
返回值:成功返回一个非负整数;出错返回EOF
作用:在文件中写入一行字符。(从内存往文件中输出一行字符)
举例:在文件中写入两行字符
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return -1;
}
//写入数据时,需要将fopen第二个参数为"w"
fputs("hello, world\n", pf); // 写一行
fputs("hello, world\n", pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fgets()
函数原型:
char *fgets( char *string, int n, FILE *stream );
参数:与fputs的区别是多了一个参数n,这参数是要读取的最大字符数
返回值:返回指向读取到的字符串的指针;返回NULL以指示错误或遇到文件结束条件
作用:从文件中读取n个字符。(文件中的数据输入到内存中)
举例:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
char str[20] = { 0 };
fgets(str, 13, pf);
printf("%s\n", str);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在上面的代码中,hello, world
总共12个字符,因为读取结果是以字符串形式返回,所以最后面需要加'\0'
,因此我们要读取所有字符的话fgets()函数的第二个参数应该写13。
fprintf()
从内存格式化输出(格式化写入文件)
函数原型:
int fprintf( FILE *stream, const char *format [, argument ]...);
参数:stream是指向要写入的文件的指针;format是格式控制字符串;argument选择性参数
返回值:成功返回写入的字节数;失败返回负值
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "zhangsan", 20, 66.5f };
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 格式化写入文件
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
fscanf()
往内存中格式化输入
函数原型:
int fscanf( FILE *stream, const char *format [, argument ]... );
参数:stream是指向要读取的文件的指针;format是格式控制字符串;argument选择性参数
返回值:返回成功转换和分配的字段数;返回值不包括已读取但未分配的字段。返回值0表示未分配任何字段。如果发生错误或者到达文件流的末尾,则返回EOF
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
// 格式化输入内存
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
// 打印
printf("%s %d %f\n", s.name, s.age, s.score);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
文件二进制读写
fwrite()
函数原型:
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
参数:buffer是指向要写数据的存储位置的指针;size是写的项目的单位大小(比如说要读取int型的数据,那么一个单位就是4个字节);count是写入项目的最大个数;stream是指向要写入的文件的指针
返回值:fwrite返回实际写入的完整项目数,如果发生错误,该数字可能小于count。此外,如果发生错误,则无法确定文件位置指针。
举例
struct Stu
{
char name[20];
int age;
float score;
};
// 写文件
int main()
{
FILE* pf = fopen("test.dat", "wb");
if (pf == NULL)
{
perror("open file for writing");
return 1;
}
struct Stu s = { "张三", 22, 99.5f };
// 写文件
fwrite(&s, sizeof(struct Stu), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
fread()
函数原型:
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
参数:buffer是指向要读取数据的存储位置的指针;size是读取的项目的单位大小(比如说要读取int型的数据,那么一个单位就是4个字节);count是读取项目的最大个数;stream是指向要读取的文件的指针
返回值:fread返回实际读取的项目数,如果发生错误或在达到count之前遇到文件结尾,则可能小于count,使用feof或ferror函数区分读取错误和文件结束情况。如果size或count为0,则fread返回0,并且缓冲区内容不变。
举例
struct Stu
{
char name[20];
int age;
float score;
};
// 读文件
int main()
{
FILE* pf = fopen("test.dat", "rb");
if (pf == NULL)
{
perror("open file for reading");
return 1;
}
struct Stu s = { 0 };
// 读文件
fread(&s, sizeof(struct Stu), 1, pf);
// 打印出来看一下读取结果是否正确
printf("%s %d %f", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
在文件的任意位置读写
上面讨论的都是对于文件的顺序读写,程序运行时我们每读取一个数据,文件中的指针就会按照顺序往后移动。因此,要想在文件中的任意位置进行读写,我们需要控制文件中的指针移动。
fseek()
函数原型:
int fseek( FILE *stream, long offset, int origin );
参数:offset是距离origin的字节数;origin是初始位置有三个可选参数:SEEK_CUR(当前位置)、SEEK_END(文件末尾)、SEEK_SET(文件开头)
返回值:如果成功fseek返回0。否则返回非零值。
作用:根据文件指针的位置和偏移量来定位文件指针
ftell()
函数原型:
long ftell( FILE *stream );
作用:返回文件指针相对于起始位置的偏移量
rewind()
函数原型:
void rewind( FILE *stream );
作用:让文件指针的位置回到文件的起始位置
举例:演示上面三个函数的功能
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
return 1;
}
fputs("ABEDEFGHIJKLMNOPQRSTUVWXYZ", pf);
// 首先测试fseek,目标是把指针定位到第10个字母(J)的位置
fseek(pf, 10, SEEK_SET); // 设置origin为文件开头,偏移量为10
// 然后测试ftell,目标是获取当前指针和初始位置的偏移量
int ret = ftell(pf); // 返回指针与初始位置的偏移量
printf("对于文件开头的偏移量为:%d\n", ret);
// 然后测试rewind,目标是在文件开头插入“hello ”
rewind(pf);
fputs("hello", pf);
// 关闭文件
fclose(pf);
pf = NULL;
return 0;
}
结果:
下图是文件中的最终结果