对于初学 C 语言的同学来说,文件操作可能是一个有点抽象但又非常重要的知识点。当我们写的程序需要保存数据,而不是程序一退出数据就消失时,文件操作就派上用场了。这篇博客将带你全面了解 C 语言中的文件和文件操作,让你从入门到熟练掌握。
一、为什么需要文件?
在我们之前写的程序中,数据都是存储在内存中的。但内存有个特点,一旦程序退出,内存就会被回收,里面的数据也就丢失了。如果我们想让数据长久保存下来,比如做一个简单的通讯录程序,希望关闭程序后再次打开还能看到之前保存的联系人,这时候就需要用到文件了。
文件能让数据实现持久化存储,把数据保存到硬盘上,即使电脑关机,数据也不会丢失。
二、什么是文件?
在电脑中,硬盘上的文件就是我们通常所说的文件。但在 C 语言程序设计中,从功能角度可以把文件分为两类:
2.1 程序文件
这类文件和程序本身相关,包括:
- 源程序文件(后缀为.c),就是我们写代码的文件
- 目标文件(Windows 环境下后缀为.obj),是编译后生成的文件
- 可执行程序(Windows 环境下后缀为.exe),是最终可以运行的程序
2.2 数据文件
这类文件存储的是程序运行时需要读写的数据,比如程序运行时从中读取数据的文件,或者程序输出内容的文件。我们这篇文章主要讨论的就是数据文件。
以前我们写的程序,输入输出都是针对终端的,从键盘输入,结果显示在显示器上。而有了数据文件,我们可以把数据输出到硬盘上,需要的时候再从硬盘读取到内存中使用。
2.3 文件名
一个文件要有唯一的标识,方便我们找到它。文件名由三部分组成:
- 文件路径:比如
c:\code\ - 文件名主干:比如
test - 文件后缀:比如
.txt
合起来就是c:\code\test.txt,这就是一个完整的文件名。
三、二进制文件和文本文件
根据数据的组织形式,数据文件可以分为二进制文件和文本文件。
数据在内存中都是以二进制形式存储的。如果直接把内存中的二进制数据输出到外存文件中,这样的文件就是二进制文件。
如果把内存中的二进制数据转换为 ASCII 码形式再输出到外存,这样的文件就是文本文件。
举个例子,整数 10000 在内存中是以二进制形式存储的。如果直接输出到文件,就是二进制文件,占用 4 个字节;如果转换为 ASCII 码输出,就是 '1'、'0'、'0'、'0'、'0' 这 5 个字符,占用 5 个字节。
四、文件的打开和关闭
在对文件进行操作之前,我们需要先打开文件;操作完成后,要关闭文件。
4.1 流和标准流
C 语言中,为了方便对各种外部设备(如键盘、显示器、文件等)进行输入输出操作,抽象出了 “流” 的概念。我们可以把流想象成流淌着字符的河。
C 语言程序启动时,会默认打开 3 个流:
- stdin:标准输入流,通常对应键盘
- stdout:标准输出流,通常对应显示器
- stderr:标准错误流,通常也对应显示器
这三个流的类型都是FILE*,也就是文件指针。
4.2 文件指针
在 C 语言的缓冲文件系统中,每个被使用的文件都会在内存中开辟一个文件信息区,用来存放文件的相关信息,比如文件名、文件状态、当前位置等。这些信息存放在一个结构体变量中,这个结构体类型由系统声明,名为FILE。
我们可以定义一个FILE*类型的指针变量来指向这个结构体变量,通过这个指针来操作文件,这个指针就称为文件指针。例如:
FILE* pf; // 定义一个文件指针变量
4.3 打开和关闭函数
C 语言中用fopen函数打开文件,用fclose函数关闭文件。
fopen函数的原型:
FILE * fopen ( const char * filename, const char * mode );
其中,filename是文件名,mode是打开方式。
fclose函数的原型:
int fclose ( FILE * stream );
文件的打开方式有很多种,常见的有:
| 打开方式 | 含义 | 如果文件不存在 |
|---|---|---|
| "r" | 只读,打开文本文件 | 出错 |
| "w" | 只写,打开文本文件 | 创建新文件 |
| "a" | 追加,向文本文件末尾添加数据 | 创建新文件 |
| "rb" | 只读,打开二进制文件 | 出错 |
| "wb" | 只写,打开二进制文件 | 创建新文件 |
举个例子:
#include <stdio.h>
int main ()
{
FILE * pFile;
//打开文件
pFile = fopen ("myfile.txt","w");
//操作文件
if (pFile!=NULL)
{
fputs ("Hello, file!",pFile);
//关闭文件
fclose (pFile);
}
return 0;
}
五、文件的顺序读写
顺序读写就是按照文件的顺序依次进行读写操作,C 语言提供了一系列函数来实现:
| 函数名 | 功能 | 适用于 |
|---|---|---|
| fgetc | 字符输入函数 | 所有输入流 |
| fputc | 字符输出函数 | 所有输出流 |
| fgets | 文本行输入函数 | 所有输入流 |
| fputs | 文本行输出函数 | 所有输出流 |
| fscanf | 格式化输入函数 | 所有输入流 |
| fprintf | 格式化输出函数 | 所有输出流 |
| fread | 二进制输入 | 文件 |
| fwrite | 二进制输出 | 文件 |
5.1 字符读写
fputc用于向文件写入一个字符,fgetc用于从文件读取一个字符。
例如,向文件写入字符:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf != NULL)
{
fputc('A', pf); //向文件写入字符'A'
fclose(pf);
pf = NULL;
}
return 0;
}
从文件读取字符:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf != NULL)
{
int ch = fgetc(pf); //从文件读取一个字符
printf("%c\n", ch);
fclose(pf);
pf = NULL;
}
return 0;
}
5.2 文本行读写
fputs用于向文件写入一行文本,fgets用于从文件读取一行文本。
例如:
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf != NULL)
{
fputs("Hello, world!\n", pf); //写入一行
fclose(pf);
pf = NULL;
}
pf = fopen("test.txt", "r");
if (pf != NULL)
{
char buf[100];
fgets(buf, 100, pf); //读取一行
printf("%s", buf);
fclose(pf);
pf = NULL;
}
return 0;
}
5.3 格式化读写
fprintf和fscanf类似于printf和scanf,但它们是针对文件进行操作的。
例如:
#include <stdio.h>
struct Student
{
char name[20];
int age;
};
int main()
{
struct Student s = {"Tom", 18};
FILE* pf = fopen("student.txt", "w");
if (pf != NULL)
{
fprintf(pf, "%s %d", s.name, s.age); //格式化写入
fclose(pf);
pf = NULL;
}
struct Student s2;
pf = fopen("student.txt", "r");
if (pf != NULL)
{
fscanf(pf, "%s %d", s2.name, &s2.age); // 式化读取
printf("%s %d\n", s2.name, s2.age);
fclose(pf);
pf = NULL;
}
return 0;
}
5.4 二进制读写
fwrite和fread用于二进制文件的读写。
例如:
#include <stdio.h>
int main()
{
int a = 100;
FILE* pf = fopen("data.bin", "wb");
if (pf != NULL)
{
fwrite(&a, sizeof(int), 1, pf); //二进制写入
fclose(pf);
pf = NULL;
}
int b;
pf = fopen("data.bin", "rb");
if (pf != NULL)
{
fread(&b, sizeof(int), 1, pf); //二进制读取
printf("%d\n", b);
fclose(pf);
pf = NULL;
}
return 0;
}
六、文件的随机读写
有时候我们需要不按照顺序,直接跳到文件的某个位置进行读写,这就是随机读写,主要通过以下几个函数实现:
6.1 fseek 函数
fseek函数可以根据文件指针的位置和偏移量来定位文件指针,原型:
int fseek ( FILE * stream, long int offset, int origin );
其中,origin有三个取值:
SEEK_SET:从文件开头开始SEEK_CUR:从当前位置开始SEEK_END:从文件末尾开始
例如:
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple.", pFile );
fseek ( pFile , 9 , SEEK_SET ); //从开头偏移9个位置
fputs ( " orange", pFile );
fclose ( pFile );
return 0;
}
这个程序最终会在文件中写入 “This is an orange”。
6.2 ftell 函数
ftell函数用于返回文件指针相对于起始位置的偏移量,原型:
long int ftell ( FILE * stream );
例如:
#include <stdio.h>
int main ()
{
FILE * pFile;
long size;
pFile = fopen ("myfile.txt","rb");
if (pFile==NULL)
perror ("Error opening file");
else
{
fseek (pFile, 0, SEEK_END); //定位到文件末尾
size=ftell (pFile); //获取文件大小
fclose (pFile);
printf ("Size of myfile.txt: %ld bytes.\n", size);
}
return 0;
}
6.3 rewind 函数
rewind函数可以让文件指针回到文件的起始位置,原型:
void rewind ( FILE * stream );
例如:
#include <stdio.h>
int main ()
{
FILE * pFile;
char buffer[20];
pFile = fopen ("myfile.txt","w+");
fputs("Hello World", pFile);
rewind(pFile); //回到文件开头
fread(buffer, 1, 11, pFile);
printf("%s\n", buffer); //输出Hello World
fclose(pFile);
return 0;
}
七、文件读取结束的判定
很多同学会错误地用feof函数来判断文件是否结束,其实feof函数的作用是:当文件读取结束时,判断结束的原因是否是遇到了文件尾。
正确的判断方法是:
- 对于文本文件,判断
fgetc的返回值是否为EOF,或者fgets的返回值是否为NULL。 - 对于二进制文件,判断
fread的返回值是否小于实际要读取的个数。
例如,文本文件的读取:
#include <stdio.h>
int main(void)
{
int c;
FILE* fp = fopen("test.txt", "r");
if(!fp) {
perror("File opening failed");
return 1;
}
while ((c = fgetc(fp)) != EOF) //判断是否为EOF
{
putchar(c);
}
//判断结束原因
if (ferror(fp))
puts("Error reading file");
else if (feof(fp))
puts("End of file reached");
fclose(fp);
return 0;
}
八、文件缓冲区
C 语言采用缓冲文件系统,系统会自动为每个正在使用的文件在内存中开辟一块缓冲区。当我们向文件写入数据时,会先送到缓冲区,缓冲区满了之后再一起送到磁盘;读取数据时,会先把数据读到缓冲区,再从缓冲区送到程序数据区。
这个缓冲区的存在,要求我们在操作文件后,要么刷新缓冲区,要么关闭文件,否则可能导致数据没有真正写入文件。
可以用fflush函数来刷新缓冲区(注意:高版本 VS 可能不支持),而fclose函数在关闭文件时也会自动刷新缓冲区。
例如:
#include <stdio.h>
#include <windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf); //先存到缓冲区
printf("等待10秒,此时打开文件看不到内容\n");
Sleep(10000);
fflush(pf); //刷新缓冲区,数据写入文件
printf("再等待10秒,此时打开文件可以看到内容\n");
Sleep(10000);
fclose(pf); //关闭文件也会刷新缓冲区
pf = NULL;
return 0;
}
总结
文件操作是 C 语言中非常重要的一部分,它能让我们的程序实现数据的持久化存储。本文介绍了文件的基本概念、打开关闭、顺序读写、随机读写、结束判定以及缓冲区等知识。对于初学的同学来说,最重要的是多动手实践,通过编写代码来熟悉这些函数的使用,这样才能真正掌握文件操作。
1954



被折叠的 条评论
为什么被折叠?



