前言
Vue框架:
Vue驾校-从项目学Vue-1
算法系列博客友链:
神机百炼
C语言文件操作函数:
一表总结:
- C语言属于用户层,只能通过文件指针FILE*来访问和操作文件
函数 | 功能 | 适用流 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
scanf | ||
fscanf | 格式化输入函数 | 所有输入流 |
printf | ||
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件 |
fwrite | 二进制输出 | 文件 |
sscanf | 将其他类型数据,转化为格式化数据 | |
sprintf | 将格式化数据,转化为其他类型数据 |
文件打开关闭:
fopen():
- 作用:打开指定路径下的文件,返回对该文件的指针
- 参数:
- 目标文件路径
- 宏定义好的打开方式mode
- 返回值:
- 成功:新打开文件的指针
- 失败:NULL
- 打开方式:通过携带不同参数mode,实现创建编辑/追加编辑/只读
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
/*
r:只读已存在的文件
r+:读写已存在的文件
rb:只读已存在的二进制文件
rb+:读写已存在的二进制文件
w:只写,创建/覆盖文件
w+:可读可写,创建/覆盖文件
wb:只写,创建/覆盖二进制文件
wb+:可读可写,创建/覆盖二进制文件
a:只写,创建/追加文件
a+:可读可写,创建/追加文件
ab:只写,创建/追加二进制文件
ab+:可读可写,创建/追加二进制文件
*/
- 实例:
#include <stdio.h>
int main(){
FILE *fp = fopen("文件主干名.文件后缀","打开方式");
if(fp == NULL){
perror("fopen失败");
return -1;
}
fclose(fp);
fp = NULL; //关闭文件后记得将指针指向NULL
return 0;
}
fclose():
- 作用:关闭文件指针所指向的文件
- 参数:FILE*文件指针
- 实例:
#include <stdio.h>
int main(){
FILE* fp = fopen("文件路径",mode);
if(fp == NULL){
perror("fopen失败");
return -1;
}
fclose(fp);
fp = NULL;
return 0;
}
三大标准文件指针:
标准输入流:
- stdin:对应设备为键盘
标准输出流:
- stdout:对应设备为显示器
标准错误流:
- stderr:对应设备为显示器
文件写入:
fputs():
- 作用:向指定文件输入内容
- 适用情况:向文件写入字符串
- 参数:输入内容的字符串指针 & 指定文件的文件指针
- 实例:
#include <stdio.h>
int main(){
FILE* fp = fopen("文件路径",mode);
if(fp == NULL){
perror("fopen失败");
return -1;
}
fputs("hello fputs\n", stdout);
fputs("hello fputs\n", fp);
fclose(fp);
fp = NULL;
return 0;
}
fprintf():
- 作用:按照指定格式将内容输入到目标文件中
- 适用情况:多用于将结构体内字段输入到文件中
- 参数:文件指针,格式,具体内容
- 实例:
#include <stdio.h>
struct test{
char a;
int b;
double c;
};
int main(){
struct test t = {'1', 2, 3};
FILE* fp = fopen("文件路径",mode);
if(!fp){
perror("fopen失败");
return -1;
}
fprintf(fp,"%c %d %lf",t.a, t.b, t.c);
fclose(fp);
fp = NULL;
return 0;
}
fwrite():
- 作用:从某地址开始,将指定大小字节数据写入目标文件
- 适用情况:文件传输
- 参数:内容地址,每个元素大小,元素个数,文件指针
- 返回值:比较写入元素个数和目标元素个数
1. 写入成功完成:元素个数
2. 写入失败:报错 - 实例:
#include <stdio.h>
#include <string.h>
int main(){
FILE *fp = fopen("路径","打开方式");
if(!fp){
perror("文件打开失败\n");
return -1;
}
const char *msg = "fwrite()用法示范";
for(int i=0; i<5; i++){
fwrite(msg, 1, strlen(msg), fp);
}
fclose(fp);
fp = NULL;
return 0; //exit()自带流关闭
}
文件读取:
fgets():
- 作用:读取文件中指定字节数内容
- 参数:结果保存的位置,读取的字节数,文件指针
- 实例:
#include <stdio.h>
int main(){
FILE* fp = fopen("文件路径",mode);
if(fp == NULL){
perror("fopen失败");
return -1;
}
char arr[20];
fgets(arr, 20, fp); //从文件中读取20个字符
printf("%s\n", arr);
fgets(arr, 20, stdin); //从键盘读取20个字符
printf("%s\n", arr);
fclose(fp);
pf = NULL;
return 0;
}
fscanf():
- 作用:按指定格式从文件中读取数据
- 适用情况:从文件中读取内容写入结构体变量中
- 参数:文件指针,输入格式,存储地址
- 实例:
#include <stdio.h>
struct test{
char a;
int b;
double c;
};
int main(){
struct test t;
FILE* fp = fopen("文件路径",mode);
if(fp == NULL){
perror("fopen失败");
return -1;
}
fscanf(fp, "%c %d %lf", &t.a, &t.b, &t.c);
fclose(fp);
fp = NULL;
return 0;
}
fread():
- 作用:读取文件中指定字节数的数据
- 适用情况:文件传输
- 参数:存储地址&每次读取字节数&读取次数&文件指针
- 返回值:当次读取的字节数
- 搭配函数:feof(FILE* fp)查看当前文件是否读取完毕
- 实例:
#include <stdio.h>
#include <string.h>
int main(){
FILE* fp = fopen("文件路径",mode);
if(!fp){
perror("fopen失败");
return -1;
}
char buffer[1024];
while(1){
ssize_t s = fread(buffer, 1, 24, fp);
if(s > 0){
buffer[s] = 0;
printf("%s\n", buffer);
}
if(feof(fp)) break;
}
fclose(fp);
return 0;
}
进程files_struct:
作用:
- 问题:由于进程独立性,每个进程打开的文件不同,所以每个进程需要专门创建一个结构来维护其所调用的文件
- 存储形式:files_struct单独开辟内存空间,由PCB/task_struct中的file*指针来连接
- fd:文件描述符,本质是file*[]数组的下标
- fd_array:文件描述符表,本质是文件指针数组
- 图示:
- 每个进程的task_struct中*file所指向的files_struct中fd_array数组中0 1 2三个元素都是默认的:
- fd_array[0]:标准输入,对应硬件是键盘
- fd_array[1]:标准输出,对应硬件是显示器
- fd_array[2]:标准错误,对应硬件是显示器
== printf()不可以视作是fprintf()固定stdout参数,实时上printf()函数输出对象是fd_array[1] ==
字符文件:
- 含义:linux下七大文件类型中的字符设备文件,其余六大为普通文件/目录文件/块文件/套接字文件/管道文件/链接文件
- 原因:这些文件中保存的都是char型数据
1. scanf()将char通过匹配转化为int double…
2. printf()将int double…转化为char - 举例:
1. 键盘驱动文件,标准输入流stdin
2. 显示器驱动文件,标准输出流stdout,标准错误流stderr
文件操作系统调用接口:
含义:
- 系统调用接口比C语言库函数更加底层:
接口应用:
- 系统调用接口比C语言文件操作函数更底层,直接体现在系统调用接口的操作对象是进程中文件描述符表的fd,而C语言文件操作函数的操作对象是文件指针
open():
用法:
- 作用:打开指定路径下文件,将文件指针装填进入进程fd_array[]中
- 参数:文件路径,打开方式,创建权限
- 返回值:
1. 打开成功:当前文件指针在文件描述符表中的下标
2. 打开失败:-1 - 函数说明:
//创建文件时需要赋予权限
int open(const char *pathname, int flags, mode_t mode);
//pathname:路径名
/*flag:打开模式
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
这三个变量,可以不指定,但是必须搭配前三个使用
*/
//mode:要赋予该文件的权限,还要在代码中搭配unmask(减去的权限)使用
//打开已有文件时不需要赋予权限
int open(const char *pathname, int flags);
- 实例:
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(){
umask(0); //不做权限删除
int fd = open("myfile", O_WRONLY|O_CREAT, 0644); //当前文件夹下创建后写入
if(fd < 0){
perror("open失败");
return 1;
}
close(fd);
return 0;
}
内存角度理解:
- 将指定路径下的文件及其相关信息结合为一个file结构体,存储在内存中
- 在进程file*所指向的files_struct的fd_array[]中加入该file结构体的文件指针
与fopen()区别:
- fopen()是用户层,对系统调用接口open()做了进一步封装
- fopen()返回值为文件指针,open()返回值为fd
- open()打开文件,但是不维护文件读写缓冲区。fopen()打开文件,且维护对应读写缓冲区
close():
用法:
- 参数:文件描述符
- 作用:关闭进程对应fd_array[]中指定的fd项对应文件
- 实例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl>
int main(){
umask(0); //不做权限删除
int fd = open("myfile", O_WRONLY|O_CREAT, 0644); //当前文件夹下创建后写入
if(fd < 0){
perror("open失败");
return 1;
}
close(fd);
return 0;
}
与fclose()区别:
- fclose()是用户层,对系统调用接口close()进行了封装
- close()关闭文件但不维护文件读写缓冲区,fclose()关闭文件且维护文件读写缓冲区
write():
-
作用:向fd_array[]中指定fd对应文件中写入内容
-
参数:fd,内容字节数,内容指针
-
返回值:本次写入数据的字节数
-
举例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
umask(0);
int fd = open("myfile", O_WRONLY|O_CREAT, 0644);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello world!\n";
for(int i=0; i<5; i++){
write(fd, msg, strlen(msg));
//fd:被读文件描述符。msg:要写入内容首地址。strlen():每次读取的字节数
}
close(fd);
return 0;
}
read():
- 作用:读取fd_array[]中指定fd对应文件中的内容
- 参数:fd,内容存储地址,内容大小
- 返回值:
- 未读取完:本次读取内容的字节数
- 读取完成:0
- 举例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello world!\n";
char buf[1024];
while(1){
ssize_t s = read(fd, buf, strlen(msg));
if(s > 0){
printf("%s", buf);
}else{
break;
}
}
close(fd);
return 0;
}
重定向:
fd分配规则:
最小分配:
1. 背景:每个进程的files_struct中的fd_array[]前三者已经被填充完:标准输入流 / 标准输出流 / 标准错误流
2. 分配规则:进程每通过open()打开一个文件时,选取fd_array[]中**没有对应file且下标最小的值作为访问该文件的下标**
实例证明:
- 直接打开一个文件,其fd值为3:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
int fd = open("文件路径",O_RDONLY);
if(fd<0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
- 关闭掉stdin/stdout/stderr,新打开的文件fd为0/1/2:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
close(0);
int fd = open("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
重定向的含义:
-
前提:
- 进程通过task_struct中file*指针所指向的files_struct中的文件指针数组fd_array[fd]寻找对应文件
- 每个进程所创建的file_struct中fd_array数组前三元素默认是 输入流stdin 输出流stdout 错误流stderr
- printf()函数输出对象始终是fd_array[1]
-
重定向:
- 含义:将fd_array[]中下标fd和具体文件的对应关系改变
- 举例:fd_array[0]原本指向键盘,现在指向具体路径下的某个文件
- 实现:
- dup2()函数实现重定向
- 利用文件描述符fd最小分配机制,close()前面的fd_array[]后实现重定向
close()实现重定向:
- 利用fd的最小分配机制:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
close(1); //为fd_array[1]重定向
int fd = open("当前路径下的文件可以直接用文件名", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd); //输出为1
fflush(stdout); //刷新缓冲区
close(fd);
exit(0);
}
dup2()实现重定向:
- dup2()函数作用:
将fd_array[newfd]和fd_array[oldfd]都指向fd_array[oldfd]所指向的文件
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 实例:通过修改fd_array[1]让printf()实现write()
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main(){
int fd = open("log.txt", O_RDWR|O_CREAT, 00644);
if(fd < 0) {
perror("open失败");
return -1;
}
close(1);
dup2(fd,1);
while(1){
char msg[1024];
ssize_t len = read(0, msg, sizeof(msg)-1);
if(len < 0){
perror("read结束");
break;
}
printf("%s", msg);
fflush(stdout);
}
close(fd);
return 0;
}
- 编译运行后查看此时新建的log.txt内容:
缓冲区:
分类:
- 文件由进程打开,不同进程打开同一文件而缓冲区不同,同一进程打开不同文件而缓冲区亦不同,初步说明缓冲区其实是属于进程地址空间布局内
- 用户区的缓冲区:
程序员可操控,如fflush(stdout) - 系统内核区的缓冲区:
程序员不可操控,由OS适时刷新 - 两者关系:
建立:
- 执行open(“路径”,打开方式,权限) / open(“路径”,打开方式)时:
- 对应文件内容和文件相关信息结合产生file结构体
- 进程对应fd_array内新增file*指针
- 进程地址空间内新增文件读写缓冲区
缓冲方式:
-
无缓冲:所有输入直接输出到指定文件
-
行缓冲:
- 适用情况:打印到屏幕
- 刷新时刻:遇到\n 或 进程return
-
全缓冲:
- 适用情况:写入到文件
- 刷新时刻:写入完毕 / fflush(stdout) / 进程return
缓冲区维护:
- 写入方式:
- printf()是C语言库函数
- fprintf()是C语言库函数
- write()是系统调用接口
- 写一段证明代码,探究文件读写缓冲区是由OS还是用户层的C语言维护:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
printf("C语言printf函数\n");
fprintf(stdout, "%s\n", "C语言fprintf函数");
char* msg = "系统调用write接口\n";
write(1, msg, strlen(msg));
fork();
return 0;
}
- 分别采用打印行刷新和文件写入全刷新的方式进行打印:
发现行刷新时虽然有子进程,但是只打印了一次;全刷新时虽然有子进程,但是只有C语言函数写入了两次 - 分析父子进程的写时拷贝:
-
写时拷贝前:
-
写时拷贝后:
-
过程分析:
- 父进程和子进程初始享有的程序/数据/files_struct/文件缓冲区一致
- 向屏幕打印时,printf() & fprintf()写入缓冲区后遇到\n,缓冲区内数据清空,转移到stdout内;子进程运行到return时需要清空缓冲区,关闭输入输出流,不论是否写时拷贝,此时缓冲区已经为空
- 向文件输出时,printf() & fprintf()写入缓冲区后等待手动fflush()或自动exit() return,此时缓冲区内保留数据,子进程运行到return时需要清空缓冲区,发生写时拷贝,子进程创建出了自己的缓冲区,且其中数据和父进程一致,父子进程return前清空各自缓冲区,关闭各自输入输出流
- 系统调用接口write()内数据未经过缓冲区,直接写入了文件中,说明缓冲区是由用户层的C语言创建和维护使用的