一、打开和关闭文件
文件是什么?Linux 和其它类 UNIX 系统的一个重要特征就是:万物皆文件(Everything is a file)。具体可以参考如何理解“万物皆文件”的说法?
打开和关闭文件函数:
- fopen 函数用于打开一个文件并返回文件指针。
打开方式要区分文本模式和二进制模式的原因,主要是因为换行符的问题。C 语言用 \n 表示换行符,Unix 系统用 \n,Windows 系统用 \r\n,Mac 系统则用 \r。如果在 Windows 系统上以文本模式打开一个文件,从文件读到的 \r\n 将会自动转换成 \n,而写入文件则将 \n 替换为 \r\n。但如果以二进制模式打开则不会做这样的转换。Unix 系统的换行符跟 C 语言是一致的,所以不管以文本模式打开还是二进制模式打开,结果都是一样的。 - fclose 函数用于关闭先前由 fopen 函数打开的文件。
举个栗子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("hello.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while ((ch = getc(fp)) != EOF)
{
putchar(ch);
}
fclose(fp);
return 0;
}
二、读写文件
读写文件又分为顺序读写和随机读写。
1.顺序读写
下面介绍顺序读写的函数:
读写一个字符串:
举个栗子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp1;
FILE *fp2;
int ch;
if ((fp1 = fopen("hello.txt","r")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
if ((fp2 = fopen("fishc.txt","w")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
while ((ch = fgetc(fp1)) != EOF)
{
fputc(ch,fp2);
}
fclose(fp1);
fclose(fp2);
return 0;
}
读写整个字符串:
举个栗子:
#include <stdio.h>
#include <stdlib.h>
#define MAX 1024
int main(void)
{
FILE *fp;
cahr buffer[MAX];
if ((fp = fopen("file.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fputs("Hello FishC!\n", fp);
fputs("I love FishC.com!\n", fp);
fclose(fp);
if ((fp = fopen("lines.txt","w") == NULL))
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while (!feof(fp))
{
fgets(buffer,MAX,fp);
printf("%s",buffer);
}
fclose(fp);
return 0;
}
格式化读写文件:
举个栗子:将当前日期获取之后写入到新创建的文件中去,再将其读取并打印到屏幕上。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
FILE *fp;
struct tm *p;
time_t t;
time(&t);
p = localtime(&t);
if ((fp = fopen("date.txt","w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fprintf(fp,"%d-%d-%d",1900+p->tm_year,1+p->tm_mon,p->tm_mday);
fclose(fp);
int year,month,day;
if ((fp = fopen("date.txt","r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fscanf(fp,"%d-%d-%d",&year,&month,&day);
printf("%d-%d-%d\n",year,month,day);
return 0;
}
[liujie@localhost sle46]$ gcc test1.c && ./a.out
2022-11-14
二进制读写文件:
举个栗子:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char name[40];
char author[40];
char publisher[40];
struct Date date;
};
int main(void)
{
FILE *fp;
struct Book *book_for_write, *book_for_read;
// 为结构体分配堆内存空间
book_for_write = (struct Book *)malloc(sizeof (struct Book));
book_for_read = (struct Book *)malloc(sizeof (struct Book));
if (book_for_write == NULL || book_for_read == NULL)
{
printf("内存分配失败!\n");
exit(EXIT_SUCCESS);
}
// 填充结构体数据
strcpy(book_for_write->name, "《带你学C带你飞》");
strcpy(book_for_write->author, "小甲鱼");
strcpy(book_for_write->publisher, "清华大学出版社");
book_for_write->date.year = 2017;
book_for_write->date.month = 11;
book_for_write->date.day = 11;
if ((fp = fopen("file.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_SUCCESS);
}
// 将整个结构体写入文件中
fwrite(book_for_write, sizeof(struct Book), 1, fp);
// 写入完成,关闭保存文件
fclose(fp);
// 重新打开文件,检测是否成功写入数据
if ((fp = fopen("file.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
// 在文件中读取结构体并打印到屏幕上
fread(book_for_read, sizeof(struct Book), 1, fp);
printf("书名:%s\n", book_for_read->name);
printf("作者:%s\n", book_for_read->author);
printf("出版社:%s\n", book_for_read->publisher);
printf("出版日期:%d-%d-%d\n", book_for_read->date.year, book_for_read->date.month, book_for_read->date.day);
fclose(fp);
return 0;
}
2.随机读写文件
.随机读写文件也就是允许从文件的任意位置开始读写。系统为每一个打开的文件设置了位置指示器。位置指示器记录当前的读写位置,要获取位置指示器的值需要使用ftell
函数。
举个栗子:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
if ((fp = fopen("hello.txt","w")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
printf("%ld\n",ftell(fp));
fputc('F',fp);
printf("%ld\n",ftell(fp));
fputs("ishC\n",fp);
printf("%ld\n",ftell(fp));
fclose(fp);
return 0;
}
[liujie@localhost sle46]$ gcc test2.c && ./a.out
0
1
6
.那么,应该如何修改位置指示器呢?
rewind
函数:将文件指示器移动到文件头(位置为0),此时插入数据,会覆盖原始数据。fseek
函数用于设置文件流的位置指示器。
可移植性问题:想要编写可移植的代码,就需要考虑以下问题:
- 对于以二进制模式打开的文件,
fseek
函数在某些操作系统可能不支持SEEK_END
位置 - 对于以文本模式打开的文件,
fseek
函数的whence
参数只能取SEEK_SET
才是有意义的,并且传递给offset
参数的值要么是0,要么是上一次对同个文件调用ftell
函数获得的返回值
三、标准流和错误处理
1.标准流
当一个程序被执行的时候,C语言自动为其打开三个面向终端的文件流,
由于标准输出和标准错误输出通常都是直接打印到屏幕上,为了区分它们,我们可以使用Linux shell的重定向功能:
- 重定向标准输入使用
<
- 重定向标准输出使用
>
- 重定向标准错误输出使用
2>
2.错误处理
每个流对象内部都有两个指示器:一个是文件结束指示器,当遇到文件尾时,该指示器被设置;另一个是错误指示器,当读写文件出错时,该指示器被设置。
- 检测文件结束指示器:
feof
- 错误指示器:
ferror
使用clearerr
函数可以人为地清除文件末尾指示器和错误指示器的状态。ferror
函数只能检测是否出错,但无法获取错误原因。不过,大多数系统函数在出现错误的时候会将错误原因记录在errno
中。perror
函数可以直观地打印出错误原因。
strerror
函数直接返回错误码对应的错误信息。
四、IO缓冲区
IO的速度与cpu的速度相差好几个量级,为了协调IO设备与Cpu设备速度不匹配问题,就设计出IO缓冲区。
标准IO提供的三种类型的缓冲模式:
- 按块缓存
按块缓存也称为全缓存,即在填满缓冲区后才进行实际的设备读写操作; - 按行缓存
按行缓存是指在接收到换行符(’ \n’)之前,数据都是先缓存在缓冲区的; - 不缓存
最后一个是不缓存,也就是允许你直接读写设备上的数据。
setvbuf 函数用于指定一个数据流的缓存模式。
举个栗子:
#include <stdio.h>
#include <string.h>
int main(void)
{
char buff[1024];
memset(buff, '\0', sizeof(buff));
// 指定 buff 为缓冲区,_IOFBF 表示当缓冲区已满时才写入 stdout
setvbuf(stdout, buff, _IOFBF, 1024);
fprintf(stdout, "This is bbs.fishc.com\n");
fprintf(stdout, "This output will go into buff\n");
// fflush强制将上面缓存中的内容写入stdout
fflush(stdout);
fprintf(stdout, "this will appear when progream\n");
fprintf(stdout, "will come after sleeping 5 seconds\n");
sleep(5);
return 0;
}