计算机中的文件是以计算机硬盘为载体存储在计算机上的信息集合。文件可以是文本文档、图片、程序等等。但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件
程序文件
包括源程序文件(后缀为.c),目标文件(后缀为.obj),可执行程序(后缀为.exe)。
数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
在这里我们讨论的是数据文件。
在以前我们所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。**其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。**冯诺依曼硬件体系结构里有提到cpu处理速度非常快,如果直接和磁盘打交道,磁盘相对来说存储的太慢了,cpu又一直在处理,导致cpu把之前来不及写入磁盘的都扔掉,这样可不行,所以,冯诺依曼决定,加一块存储器来交互:
输入设备 —> 存储器先保存------->cpu核心------->处理后数据保存到存储器-----> 输出设备。
但是内存无法永久保存数据,一断电数据就没了,没办法完全替代磁盘,而磁盘却可以永久保留数据。
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
二进制文件:
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
文本文件:
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐 个地将数据送到程序数据区(程序变量等)
。缓冲区的大小根据C编译系统决定的。
为什么要装满才可以呢?只是因为内存数据输入输出到磁盘上都是要花费资源的,要是写一点就传一点,难免会造成资源的浪费。客车拉人一样,都多拉就多拉,但是要是公司有强制命令,也必须遵守,而缓冲文件系统也有这样的措施:刷新缓冲区
除了缓冲区满的情况,刷新缓冲区输出到文件的另外几种方法
1.从main函数的return 返回时
2.fflush(stdout) 刷新缓冲到标准输出
3.\n 回车换行
4.调用exit函数也会刷新缓冲区
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量
中的。该结构体类型是有系统声明的,取名FILE
.
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
有了前面的基础,接下来就可以进入正题:
打开文件 fopen
#include
FILE *fopen(const char *path, const char *mode);
FILE: 返回值时文件流指针类型
path: 需要打开的文件名,可以带路径,否则就在当前目录下找
mode: 打开文件的方式 由参数选项规定
读文件 fread
#include
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr: 开辟的空间,将读到的内容保存到里面
size:块的大小(包含几个字节)
nmemb: 块的个数
stream: 文件流指针参数 从哪个文件读内容
#include
#include
int main()
{
FILE *fp = fopen("myfile", "r");
if(!fp){
printf("fopen error!\n");
}
char buf[1024];
const char *msg = "hello world!\n";
ssize_t s = fread(buf, 1, strlen(msg), fp);
printf("%s", buf);
fclose(fp);
return 0;
}
文件写操作 fwrite
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
ptr: 开辟的空间,把要写的内容先保存到里面
size:块的大小(包含几个字节)
nmemb: 块的个数
stream: 文件流指针参数 要把内容写到那个文件
#include
#include
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
const char *msg = "hello world!\n";
int count = 5;
while(count--){
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
根据文件指针的位置和偏移量来定位文件指针 fseek
#include
int fseek(FILE *stream, long offset, int whence);
stream:文件流指针
offset: 偏移量
whence: 设置到哪个位置 包含三个宏参数
SEEK_SET 移动到文件首部
SEEK_CUR 移动到文件流指针当前位置 ,配和 offset使用
SEEK_END 移动到文件尾部
关闭文件 fclose
#include
int fclose(FILE *fp);
open
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量是必选项,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC: 以截断方式打开
返回值:
成功:新打开的文件描述符
失败:返回-1
说明:
这两个open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。
read
#include
ssize_t read(int fd, void *buf, size_t count);
write
#include
ssize_t write(int fd, const void *buf, size_t count);
lseek
#include
#include
off_t lseek(int fd, off_t offset, int whence);
close
关闭文件描述符
#include
int close(int fd);
文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。
而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程控制块都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。
最小位分配原则
先要了解这三个 文件描述符 0 1 2
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2. 0,1,2对应的物理设备一般是:键盘,显示器,显示器
再看一段代码来分析
#include
#include
#include
#include
int main()
{
close(0); //关闭标准输入
//close(2); //或者关闭标准错误
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);//打开的这个文件的文件描述符为刚才被关闭(不使用)的 0 或2
close(fd);
return 0;
}
发现是结果是: fd: 0 或者fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
之前学过一个指令 :echo
可以将 " hello" 直接打印到终端上.
也可以配和重定向符号把字符重定向到文件中去
echo "hello" > [filename]
重定向符号
> 清空重定向 清空之前的,在写入
>> 追加重定向 追加到末尾
#include
int main()
{
close(1); //先关闭标准输出
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);//打开文件
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);//打印语句及输出,根据最小位分配原则,此文件描述符就是1
fflush(stdout);//刷新缓冲
close(fd);
exit(0);
}
此时,我们发现,本来应该输出到显示器上的内容,输出到了文件myfile 当中,其中,fd=1。这种现象叫做输出重定向。
上面的代码分析图示:
重定向也有个系统调用:dup2
#include
int dup2(int oldfd, int newfd);
oldfd:重定向的文件描述符
newfd: 要关闭的文件描述符
作用:将newfd指向的文件信息拷贝到oldfd中;要是oldfd是一个无效的文件描述符,会提前检测到,故newfd就不会关闭,这个dup2函数则失败。
#include
#include
#include
int main() {
int fd = open("./log", O_CREAT | O_RDWR); //文件描述符分配的是3
if (fd < 0) {
perror("open");
return 1;
}
close(1); //关闭的文件描述符newfd
dup2(fd, 1); //再调用重定向 此时打开的文件的文件描述符就是被重定向为1
printf("hello world");
fflush(stdout);
while(1) //让他睡着,好调试看结论
{
;
}
return 0;
}
如何查看当前操作系统为进程所分配的文件描述符信息
ll /proc / [pid] / fd
检查文件描述符泄露
1.ulimit -a 查看可以文件描述的数量
2.ll /proc / [pid] / fd
c文件操作的fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数
(libc)。而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口
。而之前我们讲过,用户想与系统内核交互,只能从内核露出来的系统调用接口才可以完成,所以这些库函数底层内部是绝对封装了系统调用的。
而其中文件流指针和文件描述符的关系也就明显了: 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定也是封装了fd。
两者关于缓冲区的问题
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。printf fwrite 库函数会自带缓冲区,而我们放在缓冲区中的数据,一般不会立即刷新,但是进程退出之后,会统一刷新,写入文件当中。write系统调用就没有所谓的缓冲,是立即读取的。
这也就引出个问题:当我们使用文件流指针的时候,必须要考虑读写缓冲区的问题,如果我们向一个文件写数据的时候,程序崩溃了,有可能就导致文件流指针写入的数据丢失;
而使用文件描述符,就不会出现,因为没有缓冲这个概念,但是立即读取确实非常耗费性能的,所以,写数据时,要根据情况使用两者,一般不是很重要的数据就用文件流指针操纵。
这张参考图可以更加细微的看出两者的关系: