一、为什么要使用文件
我们在正常编写程序时,程序里的数据是存放在内存里的。当程序结束后,这些数据自然就不存在了。当下次运行程序的时候,数据又重新录入。而使用文件可以把数据存放到电脑里的硬盘里,这样数据就会一直存在,我们能够自己控制数据的保存与删除,做到了数据的持久化。
二、什么是文件
磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
1、程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2、数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
接下来我们要讨论的都是数据文件。
3、文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含三部分:文件路径+文件名主干+文件后缀。
例如:C:\Users\kaka\Desktop\c-language\contact.dat
为了方便起见,文件名常被称为文件名。
三、文件的打开与关闭
文件的操作分为三个步骤:
①打开文件
②使用文件(读/写)
③关闭文件
1、文件指针
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件名字,文件状态,文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体是由系统声明的,取名FILE。
下面是VS2013编译环境下提供的stdio.h头文件中有以下的文件类型声明
struct _iobuf {
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更方便。
下面就是一个FILE* 的指针变量。
FILE* p;
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区。通过该文件信息区中的信息就可以访问该文件。也就是说,通过指针变量能够找到与它关联的文件。
2、文件的打开和关闭
ANSIC规定使用fopen函数来打开文件,fclose函数来关闭文件。
以下是fopen函数的声明
FILE* fopen(const char* filename, const char* mode)
fopen函数打开文件成功后,会返回一个FILE*类型的指针指向该文件的文件信息区,相当于建立了指针和文件之间的关系。打开失败则会返回NULL。
filename:字符串,要打开的文件名称。
mode:字符串,文件的访问模式,可以使下表中的值
文件使用方式 |
含义 |
如果指定文件不存在 |
“r”(只读) |
为了输入数据,打开一个已经存在的文本文件 |
出错 |
“w”(只写) |
为了输出数据,创建一个文本文件 |
建立一个新的文件(如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。) |
“a”(追加) |
向文本文件末尾添加数据 |
建立一个新的文件 |
“rb”(只读) |
为了输入数据,打开一个二进制文件 |
出错 |
“wb”(只写) |
为了输出数据,打开或者建立一个二进制文件 |
建立一个新的文件 |
“ab”(追加) |
向一个二进制文件尾添加数据 |
出错 |
“r+”(读写) |
为了读和写,打开一个文本文件 |
出错 |
“w+”(读写) |
为了读和写,建立一个新的文件 |
建立一个新的文件(如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。) |
“a+”(读写) |
打开一个文件,在文件尾进行读写 |
建立一个新的文件 |
“rb+”(读写) |
为了读和写,打开一个二进制文件 |
出错 |
“wb+”(读写) |
为了读和写,打开或者新建一个二进制文件 |
建立一个新的文件 |
“ab+”(读写) |
打开一个二进制文件,在文件尾进行读写 |
建立一个新的文件 |
以下是fclose函数的声明:
int fclose(FILE* stream)
stream:这是指向FILE对象的指针,该FILE对象指向了要被关闭的流。
实例演示如下:
int main()
{
//打开文件
FILE* p = fopen("date.txt", "w");
//date.txt是相对路线,只能访问该程序里的文件
//如果想要访问本程序外的文件,需要用绝对路径
//例如 FILE *p=fopen("C:\\Users\\kaka\\Desktop\\date.txt.txt","w");
//注意转义字符
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//使用文件
//关闭文件
fclose(p);
p = NULL;//将p置为空指针,防止野指针
return 0;
}
四、文件的顺序读写
功能 |
函数名 |
适用于 |
字符输入函数 |
fgetc |
所有输入流 |
字符输出函数 |
fputc |
所有输出流 |
文本行输入函数 |
fgets |
所有输入流 |
文本行输出函数 |
fputs |
所有输出流 |
格式化输入函数 |
fscanf |
所有输入流 |
格式化输出函数 |
fprintf |
所有输出流 |
二进制输入函数 |
fread |
文件 |
二进制输出函数 |
fwrite |
文件 |
1、fputc函数
声明如下:
int fputc(int chars, FILE* stream)
把参数chars指定的一个字符(无符号字符)写到指定的流stream中。
如果没有发生错误,返回被写入的的字符,如果发生错误,返回EOF并设置错误标识。
实例演示如下:
int main()
{
//打开文件
FILE* p = fopen("date.txt", "w");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//写文件
//把24个大写字母写进文件
for (int i = 0;i < 26;i++)
{
fputc('a' + i, p);
}
//关闭文件
fclose(p);
p = NULL;
return 0;
}
成功写入文件
2、fgetc函数
声明如下:
int fgetc(FILE* stream)
从指定的流stream中获取一个字符(无符号字符)。
strem:这是指向FILE对象的指针,该FILE指针标识了要在上面进行操作的流。
获取成功是会以无符号char强制转换为int的形式返回读取的字符,如果到达文件末尾或者发生错误,返回EOF。
实例演示如下:
int main()
{
//打开文件
FILE* p = fopen("date.txt", "r");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//读文件
//读取储存在文件里的26个字母
for (int i = 0;i < 26;i++)
{
int ch = fgetc(p);//用ch来接收读取的字符
printf("%c ", ch);
}
//关闭文件
fclose(p);
p = NULL;
return 0;
}
成功读取
注意,当我们打开文件的时候有一个文件指针指向文件的起始位置
fgetc每次读取1个字符后都会让文件指针向后移动一格。
所以使用fgetc函数的时候就要注意文件指针的移动。
还要注意一点,文件指针可不是p,p指向的文件信息区,下面这段代码是不合理的。
p++;
3、fputs函数
函数声明如下:
int puts(const char* str, FILE* stream)
能够把字符串str写到指定的流stream中。
该函数返回一个非负值,否则返回EOF。
实例演示如下:
int main()
{
//打开文件
FILE* p = fopen("date.txt", "w");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//写文件
//写两行句子到文件里
fputs("hello world\n", p);
fputs("good morning", p);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
成功写入
4、fgets函数
声明如下:
char* fgets(char* str, int n, FILE* stream)
从指定的流stream中读取一行,并把它储存在str指向的字符串中,当读取n-1字符(留1个位置放\0),或者读取到换行符是,或者到达文件末尾时,函数停止。
如果成功,该函数返回一个相同的str参数,如果发生错误,返回一个空指针。
实例演示如下:
int main()
{
//打开文件
FILE* p = fopen("date.txt", "r");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//读文件
//把之前写入文件的hello读取出来
char str[20] = "0";
fgets(str, 6, p);
printf("%s\n", str);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
成功读取:
5.fprintf函数
printf函数与fprintf函数声明的对比:
我们可以看出fprintf函数的参数只是比printf函数多了一个流指针stream。
所以我们只要在使用printf函数的基础上再加上一个流指针就可以使用fprintf函数了。
fprintf("%s %c %d", s.a, s.b, s.c);//先和printf函数一样写上要输入的参数
fprintf(p,"%s %c %d", s.a, s.b, s.c);//再加上流指针
实例演示如下:
struct S
{
char a[10];
char b;
int c;
};
int main()
{
struct S s = { "Modric",'n',37 };
//打开文件
FILE* p = fopen("date.txt", "w");
if (p == NULL)
{
perror("fopen:Error");
return 0;
}
//写文件
fprintf(p,"%s %c %d", s.a, s.b, s.c);
//关闭文件
fclose(p);
p = NULL;
}
成功写入:
6、fscanf函数
scanf函数与fscanf函数声明的对比:
fscanf函数和上面所讲的fprintf函数一样,只是比scanf函数多了一个流指针参数,所以我们也可以像使用fpritf函数一样使用fscanf函数。
实例演示如下:
struct S
{
char a[10];
char b;
int c;
};
int main()
{
struct S s;
//打开文件
FILE* p = fopen("date.txt", "r");
if (p == NULL)
{
perror("fopen:Error");
return 0;
}
//读文件
fscanf(p,"%s %c %d", &s.a, &s.b, &s.c);
printf("%s\n", s.a);
printf("%c\n", s.b);
printf("%d\n", s.c);
//关闭文件
fclose(p);
p = NULL;
}
成功读取:
7、fwrite函数
fwrite函数的声明如下:
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream)
ptr:指向要被写入的元素数组的指针。
size:被写入的每个元素的大小,单位是字节。
count:被写入元素的个数。
stream:指向一个输出流。
该函数能把str指向的数组中的数据以二进制的形式写入到指定流stream中。
该函数会返回读取到的元素的总数,如果总数与count不同,就是发生错误。
实例演示如下:
struct S
{
char a[20];
char b;
int c;
};
int main()
{
struct S s[2] = { {"hello wirld",'a',18},{"the world",'b',37} };
//打开文件
FILE* p = fopen("date.txt", "wb");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//写入文件
fwrite(s, sizeof(struct S), 2, p);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
因为是二进制文件,所以用记事本打开我们是看不懂的。
8、fread函数
fread函数的声明如下:
size_t fread(void* ptr, size_t size, size_t count, FILE* stresm)
ptr:指向一块内存的指针。
size:要读取的每个元素的大小,单位是字节。
count:要读取的元素个数。
stream:指向一个输入流。
该函数能够从指定流stream中读取二进制数据到ptr指向的内存中。
该函数会返回读取到的元素的总数,如果总数与count不同,可能发生错误或者到达文件末尾。
实例演示如下:
struct S
{
char a[20];
char b;
int c;
};
int main()
{
//打开文件
FILE* p = fopen("date.txt", "rb");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
struct S sa[2] = { 0 };
//读取文件
fread(sa, sizeof(struct S), 2, p);
printf("%s %c %d\n", sa[0].a, sa[0].b, sa[0].c);
printf("%s %c %d\n", sa[1].a, sa[1].b, sa[1].c);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
承接上文,我们的虽然看不懂二进制文件,但是可以用fread函数把数据读取到内存吗,再打印出来。
9、流
我们在上面多次提到了流stream。
流是我们抽象出来的概念,当我们需要把数据传输到外部设备时(文件,屏幕,网络),就需要使用流。
流知道怎么把外部设备的数据传给内存或者把内存的数据传给外部设备,当我们需要转数据给外部设备时,就把数据传给流。当我们需要从外部设备读文件的时候,就从流里读。不需要关心流是怎么实现这写操作的。
当我们读写文件,操作的是文件流。
当我们使用终端设备(屏幕)的时候,操作的是标准输出流,键盘是标准输入
流,屏幕操作的还有标准错误流。
读写文件 |
文件流 |
C语言程序默认打开下面三个流:
终端-屏幕 |
标准输出流 |
stdout |
键盘 |
标准输入流 |
stdin |
屏幕 |
标准错误流 |
stderr |
因为默认打开了这三个流,所以我们在使用scanf输入数据时不要先打开键盘,再输入数据。在使用printf输出数据的时候,不需要先打开屏幕,再打印数据。
stdout、stdin、stderr都是流的名称,是FILE指针。
我们在前面用fputc把数据写入了文件中,我们知道fputc函数适用于所有输出流,我们尝试用fputc函数把数据打印到屏幕上。
int main()
{
//屏幕是标准输出流,不需要打开
fputc('a', stdout);//这里的第二个参数是标准输出流
fputc('b', stdout);
fputc('c', stdout);
fputc('d', stdout);
return 0;
}
成功打印:
第二个例子
struct S
{
char a[10];
char b;
int c;
};
int main()
{
struct S s;
//从标准输入流输入数据到内存中
fscanf(stdin,"%s %c %d", &s.a, &s.b, &s.c);
printf("%s %c %d", s.a, s.b, s.c);
}
成功输入,第一行是我们输入的,第二行我们把结构体成员打印出来:
10、对比一组函数
scanf:针对标注输入流(stdin)的格式化输入。
printf:针对标注输出流(stdout)的格式化输出。
fscanf:针对所有输入流(文件流、stdin)的格式化输入。
fprintf:针对所有输出流(文件流、stdout)的格式化输出。
下面是sprintf函数的声明:
int sprintf(char *str, const char *format, ...)
它的功能是把一个格式化的数据写到字符串中。(把一个格式化的数据转化为字符串)
下面是sscanf函数的声明
int sscanf(const char *str, const char *format, ...)
它的功能是把一个字符串转化为格式化数据。
实例演示如下:
struct S
{
char a[10];
char b;
int c;
};
int main()
{
struct S s = { "theworld",'n',18 };
char str[30] = "0";
//把结构体转化为字符串
sprintf(str, "%s %c %d", s.a, s.b, s.c);
printf("%s\n",str);
//把字符串转化为格式化数据
struct S b = { 0 };
sscanf(str, "%s %c %d", &b.a, &b.b, &b.c);
printf("%s %c %d\n", b.a, b.b, b.c);
return 0;
}
五、文件的随机读写
1、fseek函数
声明如下:
int freek(FILE* stream, long int offset, int whence)
stream:指向一个流。
offset:现对于whence的偏移量。
whence:表示开始添加偏移offset的位置,它一般指定为下列常量之一:
常量 |
描述 |
SEEK_SET |
文件的开头 |
SEEK_CUR |
文件指针的当前位置 |
SEEK_END |
文件的末尾 |
该函数能够根据文件指针的位置和偏移量来定位文件指针(可以让文件指针移动到你想移动到的位置),如果成功返回0,否则返回非零值。
实例演示如下:
文件内容如下
int main()
{
//打开文件
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
//从文件开头计算偏移量
fseek(p, 6, SEEK_SET);//文件指针指向'w'
char c = fgetc(p);//文件指针会后移一次,指向'o'
printf("%c\n", c);
//从文件指针当前位置计算偏移量
fseek(p, -4, SEEK_CUR);//文件指针指向'l'
c = fgetc(p);//文件指针会后移一次,指向'o'
printf("%c\n", c);
//从文件末尾计算偏移量
fseek(p, -1, SEEK_END);//文件指针指向'd'
c = fgetc(p);//文件指针会后移一次,指向文件末尾
printf("%c\n", c);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
运行结果如下:
2、ftell函数
声明如下:
long int ftell(FILE* stream)
该函数能够返回给定流文件指针相对于文件开头的偏移量。
实例演示如下:
还是这个文件:
int main()
{
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
fgetc(p);
fgetc(p);
fgetc(p);
fgetc(p);
//此时文件指针指向'0'
int a = ftell(p);//此时文件指针相对于文件开头的偏移量应该是4
printf("%d\n", a);
fclose(p);
p = NULL;
return 0;
}
运行结果如下:
3、rewind函数
声明如下:
void rewind(FILE* stream)
该函数能够让文件指针指向文件开头。
实例演示如下:
依旧是这个文件
int main()
{
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
fgetc(p);
fgetc(p);
fgetc(p);
fgetc(p);
//此时文件指针指向'0'
rewind(p);//此时文件指针重新指向文件开头
char a = fgetc(p);
printf("%c\n", a);//此时打印的应该是'H'
fclose(p);
p = NULL;
return 0;
}
运行结果如下:
六、文件读取结束的判定
1、feof的错误使用
在文件读取过程中,不能用feof函数的返回值直接判断文件是否结束。而是应用于当文件读取结束的时候,判断是读取失败还是遇到文件尾结束。
①文本文件读取是否结束,判断返回值是否是EOF(getc),或者NULL(fgets)。
例如:
- fgetc判断是否为EOF。
- fgets判断是否为NULL。
②二进制文件判断返回值是否小于实际要要读的个数。
例如:
- fread判断返回值是否小于实际要读的个数。
2、探究fgetc函数的返回值
- 遇到文件末尾,返回EOF,并设置一个状态,遇到文件末尾了,可以用feof来检测这个状态。
- 遇到错误,返回EOF,并设置一个状态,遇到错误了,可以用ferror来检测这个状态
int main()
{
FILE* p = fopen("wat.txt", "r");
if (p == NULL)
{
perror("fopen:Error:");
return 0;
}
char ch = 0;
while ((ch = fgetc(p)) != EOF)
{
printf("%c ", ch);
}
if (ferror(p))
{
printf("读取失败\n");
}
else if (feof(p))
{
printf("读取到文件末尾\n");
}
fclose(p);
p = NULL;
return 0;
}
fread也是同理,
int main(void)
{
double a[5] = { 1.,2.,3.,4.,5. };
FILE* p = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof * a, 5, p); // 写 double 的数组
fclose(p);
double b[5];
p = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof(double), 5, p); // 读 double 的数组
if (ret_code == 5) {
puts("读取数组成功,内容是: ");
for (int n = 0; n < 5; ++n)
printf("%f ", b[n]);
}
else {
if (feof(p))
printf("读取到文件末尾\n");
else if (ferror(p)) {
perror("读取失败\n");
}
}
fclose(p);
p = NULL;
}
正确使用feof函数和ferror函数能够帮助我们判断读取结束后是读到文件末尾了还是读取失败。
本文结束。