一、学习内容
-
I/O进程
-
标准IO
-
概念
-
针对文件的读写操作 文件IO最终达成的目的:将一个临时存在于内存中的数据,永久性的存放于磁盘当中
-
-
操作
-
文件IO的操作,需要这样的2个指针 一个指针:指向源数据,提供读取操作的指针 另一个指针:指向用来接受数据的文件的地址 该指针需要提前准备:并且确定好该指针到底指向哪个文件
-
-
-
创建一个文件指针
-
fopen
-
函数原型
-
FILE *fopen(const char *pathname, const char *mode);
-
-
函数描述
-
打开一个文件,成功打开之后,返回指向该文件的文件指针
-
-
调用形式
-
FILE* fp = fopen("文件名","r"); FILE* fp = fopen("文件名","w"); FILE* fp = fopen("文件名","a"); FILE* fp = fopen("文件名","r+"); .... if(fp == NULL){ printf("文件打开失败\n") return 0; }
-
-
函数返回值
-
成功返回指向打开文件的文件指针,失败返回NULL
-
-
参数分析
-
参数 pathname
-
准备打开的文件的路径名
-
-
参数 mode
-
文件的打开形式
-
-
r
-
以只读的形式,打开文件,文件成功打开之后,光标位于文件的开头
-
注意:如果文件不存在,r打开会失败,并且fopen函数返回一个NULL
-
光标位于文件的开头:光标在文件中第一个数据的地址上
-
光标位于文件的末尾:光标在文件的结束符的地址上,文件结束符的前一个数据,大概率不是文件中的最后一个数据,而是一个换行符
-
-
r+
-
以读写的形式打开文件,其他内容和r一样
-
注意:r+存在读写2个光标 读写2个光标是独立管理的,但是如果不做特殊操作的话,读写两个光标是同时移动的
-
-
w
-
以只写的形式打完开文件,如果文件存在,则清空文件内容后打开,如果文件不存在,则创建文件后打开。文件成功打开后,光标定位在文件的开头 大概率上来说,以w打开文件,一般都会成功 只有在文件打开数量超过系统允许的上限的情况下,才会打开失败
-
系统默认允许的文件打开上限为:1024个 也可以使用shell指令: ulimit -a 查看
-
-
w+
-
以读写的形式打开文件,剩下的就和w是一样了
-
-
a
-
以追加的形式打开文件,如果文件不存在则创建文件后打开,如果文件存在,不会清空文件内容,直接打开。文件打开后,光标位于文件的末尾
-
-
a+
-
以追加写和读的形式打开文件啊,如果文件不存在,则创建后打开。如果文件存在,则直接打开。
-
注意:文件打开后,读光标位于文件的开头,并随着文件读取慢慢的向后移动。写光标总是位于文件的末尾,并不会随着读光标的移动而移动
-
-
-
-
-
关闭一个文件
-
fclose
-
函数原型
-
int fclose(FILE *stream);
-
-
函数描述
-
通过给定的文件指针stream关闭文件
-
-
函数调用
-
fclose(fp)
-
-
-
-
文件指针FILE*类型
-
FILE本质上是一个typedef的结构体
-
查看系统自带的结构体类型
-
cd /usr/include 专门用来存放头文件的目录 sudo ctags -R 在存放头文件的目录里面,创建一个搜索列表 vim -t 想要查看的数据名 前两步是一次性操作,之后想要查看其他内容,直接执行第三步即可
-
输入 vim -t FILE 显示如下画面,我们要的是 红框中的内容,所以键入1,按回车
-
查看后得知,FILE typedef 自 _IO_FILE 类型,继续追踪,鼠标点到 _IO_FILE 上面,键入 ctrl+ ]
-
此时又会找到2个搜索目标,应该是第一个,键入1,按回车
-
-
-
FILE的定义如
-
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; 当前读光标的位置 char* _IO_read_end; 文件结束符的位置 char* _IO_read_base; /* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr; 写光标的位置 char* _IO_write_end; 文件结束符为止 char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end; /* End of reserve area. */ int _fileno; 最核心的,最终通过该数据定位文件的 };
-
-
-
-
3个特殊的FILE* 文件
-
stdin
-
指向了终端标准输入流文件的指针
-
scanf,gets,getchar 等,都是默认的使用了这个文件流指针
-
-
-
stdout
-
指向了终端标准输出流文件的指针
-
printf,puts,putchar 等,都是默认的使用了这个文件流指针
-
-
-
stderr
-
指向了终端标准错误流的指针
-
标准错误流
-
目前来说流向只有2种,代码流向终端 以及 终端流向代码 标准错误流:属于代码流向终端
-
stdout也属于代码流向终端,那么stderr 和 stdout的区别
-
stderr的存在目的就是为了区分stdout,从而实现 正常信息通过 stdout输出到终端 错误信息通过 stderr 可选择的输出到任意文件中 从而实现信息的划分
-
-
谁在用stderr
-
perror函数
-
函数原型
-
void perror(const char *s);
-
-
功能描述
-
perror函数,先输出s(提示信息):然后会根据一个标准的错误编号,输出该错误编号所描述的错误内容 错误代码哪来的:最后一个调用的系统函数或者库函数,因为错误调用而产生的 说人话就是:perror输出因为什么原因发生了错误
-
-
-
-
-
-
-
-
-
针对文件的第一组读写函数
-
fputc
-
函数原型
-
int fputc(int c, FILE *stream);
-
-
功能描述
-
将c的字符形式,写入到stream指向的文件中去
-
-
调用形式
-
FILE* fp = fopen(文件名,"w") fputc(字符,fp)
-
-
参数解析
-
参数 c:想要输出的数据
-
参数 stream:准备接受数据的文件的地址
-
-
-
fgetc
-
函数原型
-
int fgetc(FILE *stream);
-
-
功能描述
-
从stream指向的文件中,读取1个字节的数据
-
-
参数描述
-
参数 stream:想要读取的文件的地址
-
-
返回值
-
成功读取,会以 unsigned char的形式,返回读取到的那一个字节的数据
-
如果读取到文件结束符,则返回 EOF (-1) 如果读取发生错误,也会返回 EOF
-
-
-
-
针对文件的第二组读写函数
-
fprintf / sprintf
-
函数原型
-
int printf(const char *format, ...); int fprintf(FILE *stream, const char *format, ...);
-
int sprintf(char *str, const char *format, ...);
-
-
功能描述
-
printf功能:将字符串format中的内容输出到stdout指向的文件中去(stdout指向终端) fprintf功能:将字符串format中的内容输出到stream指向的文件中去(stream指向哪个文件由fopen函数决定)
-
将format中的数据写入str指向的字符数组中去 相当于实现了把任意类型的数据,转换成字符串的功能
-
-
调用形式
-
FILE* fp = fopen(文件名,"w") fprintf(fp,"%d",123) fprintf(fp,"%c",'c') fprintf(fp,"%s","abc") fprintf(fp,"%s","123") ....
-
har str[20] = {0}; sprintf(str,"%lf",3.1416) 最终 str == "3.1416"
-
-
参数解析
-
第一个参数 stream:指向接受数据的文件的指针
-
第二个参数 format:想要输出到文件中的数据
-
第三个参数 ...:format中格式占位符具体表示的数据
-
注意:fprintf,无论将什么数据写入文件后,这些数据都会以字符串的形式进行保存
-
-
总结
-
fprintf/sprintf 整体上来说用法一模一样,只不过不同的printf将数据写入的地方是不一样的
-
printf:默认将数据写入终端
-
fprintf:将数据写入第一个参数stream指向的文件中去
-
sprintf:将数据写入第一个参数str指向的字符数组中去
-
-
-
fscanf / sscanf
-
fscanf
-
函数原型
-
int fscanf(FILE *stream, const char *format, ...);
-
-
功能描述
-
从stream指向的文件中读取数据,写入到 format 中格式占位符所代表的变量中去
-
-
调用形式
-
FILE* fp = fopen(文件名,"r") int a = 0; fscanf(fp,"%d",&a)
-
-
参数解析
-
第一个参数 stream:指向了想要读取数据的文件的指针
-
第二个参数 format:只含有格式占位符的字符串
-
第三个参数 ... :格式占位符代表的变量的地址
-
注意: %s除了空格和回车不吸收以外,其他都吸收 %d只吸收int类型数据,碰到int类型以外的数据,立刻停止吸收 %lf吸收int和浮点型,遇到单独.和除.以外的其他字符,立刻停止吸收 %c吸收一切,包括空格和回车
-
-
返回值
-
成功吸收,返回吸收到的数据的项数,一般就是描述符的个数
-
如果文件吸收完毕 或者 吸收发生错误,则返回 EOF
-
-
-
sscanf
-
函数原型
-
int sscanf(const char *str, const char *format, ...);
-
-
功能描述
-
从字符串/字符数组 str中吸收数据,写入到format中的格式占位符所代表的变量的地址上面去 实际实现:将字符串类型的数据,转换成可转换的任意类型数据
-
-
函数调用
-
char str[32] = "123abc" int a = 0; char b[20] = {0}; sscanf(str,"%d%s",&a,b); 最终 a == 123,b=="abc"
-
-
-
-
-
-
脑图
二、作业
题目1、
有如下结构体:
typedef struct Student{
char name[20];
int id;
double chinese;//语文成绩
double math;
double english;
double physical;
double chemical;
double biological;
}stu_t;
有一个 stu_t的结构体数组 arr[3];随便使用任何方式初始化这个数组中的3个结构体
编写2个函数 :save_stu 和 load_stu
save_stu:通过 fprintf 将arr数组中的3个学生的所有信息,保存到文件中去
load_stu:通过 fscanf 将文件中的3个学生的所有信息,读取到一个新的结构体数组中,并输出所有学生的信息
代码解答:
#include <stdio.h>
#include <string.h>
// 定义一个学生结构体,包含学生的姓名、学号及各科成绩
typedef struct Student {
char name[20]; // 学生姓名,最多 19 个字符(+1 个 '\0' 结束符)
int id; // 学生学号
double chinese; // 语文成绩
double math; // 数学成绩
double english; // 英语成绩
double physical; // 物理成绩
double chemical; // 化学成绩
double biological; // 生物成绩
} stu_t;
// 输入学生信息
void arr_input(stu_t arr[]) {
int i; // 循环变量
for(i = 0; i < 3; i++) { // 循环 3 次,输入 3 个学生的信息
printf("请输入第%d个学生信息\n", i + 1); // 提示输入第几个学生
printf("姓名: "); // 提示输入姓名
scanf("%s", arr[i].name); // 读取学生姓名并存入结构体
printf("学号: "); // 提示输入学号
scanf("%d", &arr[i].id); // 读取学生学号并存入结构体
printf("语文成绩: "); // 提示输入语文成绩
scanf("%lf", &arr[i].chinese); // 读取语文成绩
printf("数学成绩: "); // 提示输入数学成绩
scanf("%lf", &arr[i].math); // 读取数学成绩
printf("英语成绩: "); // 提示输入英语成绩
scanf("%lf", &arr[i].english); // 读取英语成绩
printf("物理成绩: "); // 提示输入物理成绩
scanf("%lf", &arr[i].physical); // 读取物理成绩
printf("化学成绩: "); // 提示输入化学成绩
scanf("%lf", &arr[i].chemical); // 读取化学成绩
printf("生物成绩: "); // 提示输入生物成绩
scanf("%lf", &arr[i].biological); // 读取生物成绩
}
}
// 保存学生信息到文件
void save_stu(stu_t arr[]) {
FILE *fp = fopen("students.txt", "w"); // 打开文件 "students.txt",以写入模式
if (fp == NULL) { // 检查文件是否成功打开
printf("文件打开失败!\n"); // 文件打开失败时输出错误信息
return; // 返回
}
for (int i = 0; i < 3; i++) { // 循环 3 次,保存 3 个学生的信息
// 使用 fprintf 将每个学生的信息格式化写入文件
fprintf(fp, "%s %d %.2f %.2f %.2f %.2f %.2f %.2f\n",
arr[i].name, arr[i].id, arr[i].chinese, arr[i].math,
arr[i].english, arr[i].physical, arr[i].chemical, arr[i].biological);
}
fclose(fp); // 关闭文件
printf("学生信息已保存到文件\n"); // 提示信息保存成功
}
// 从文件加载学生信息
void load_stu(stu_t arr[]) {
FILE *fp = fopen("students.txt", "r"); // 打开文件 "students.txt",以读取模式
if (fp == NULL) { // 检查文件是否成功打开
printf("文件打开失败!\n"); // 文件打开失败时输出错误信息
return; // 返回
}
for (int i = 0; i < 3; i++) { // 循环 3 次,读取 3 个学生的信息
// 使用 fscanf 从文件中读取学生的信息,并存入结构体数组
fscanf(fp, "%s %d %lf %lf %lf %lf %lf %lf",
arr[i].name, &arr[i].id, &arr[i].chinese, &arr[i].math,
&arr[i].english, &arr[i].physical, &arr[i].chemical, &arr[i].biological);
}
fclose(fp); // 关闭文件
// 输出从文件加载的学生信息
printf("从文件加载的学生信息:\n");
for (int i = 0; i < 3; i++) { // 循环输出每个学生的信息
printf("姓名: %s, 学号: %d, 语文: %.2f, 数学: %.2f, 英语: %.2f, 物理: %.2f, 化学: %.2f, 生物: %.2f\n",
arr[i].name, arr[i].id, arr[i].chinese, arr[i].math,
arr[i].english, arr[i].physical, arr[i].chemical, arr[i].biological);
}
}
int main() {
stu_t arr[3]; // 定义一个学生结构体数组,包含 3 个学生
arr_input(arr); // 调用函数输入 3 个学生的信息
save_stu(arr); // 调用函数将学生信息保存到文件
load_stu(arr); // 调用函数从文件加载学生信息并输出
return 0; // 程序正常结束
}
成果展现:
三、总结
学习内容概述:
1. I/O进程基础:
I/O进程是操作系统中的一个重要部分,负责管理输入输出操作。
涉及标准输入、标准输出、文件描述符等概念。
包括阻塞、非阻塞I/O、异步I/O等不同I/O模型。
2. 具体的I/O操作方式:
`read()`和`write()`:系统调用实现读取和写入操作。
缓冲区的管理:包括内核缓冲区与用户缓冲区的交互。
文件的打开、关闭、定位等操作,使用`open()`, `close()`, `lseek()`等系统调用。
3. 进程管理与I/O的关:
进程可以通过I/O操作与外部世界进行交互。
进程阻塞和非阻塞在I/O操作中的应用,如何避免因I/O操作导致的进程停滞。
学习难点:
1. 阻塞与非阻塞I/O模型的理解与应用:
如何区分阻塞和非阻塞操作,以及这两者对进程调度和CPU利用率的影响。
在实际应用中选择合适的I/O模型。
注意事项:
1. 系统调用的返回值检查:
对每次系统调用的返回值进行检查,确保I/O操作的成功与失败能够被正确处理,避免资源泄露。
2. 缓冲区大小的设置:
使用合适大小的缓冲区,以平衡系统资源与性能,避免内存浪费或I/O频繁的上下文切换。
3. 文件描述符的管理:
在处理大量文件描述符时,确保正确管理文件描述符,防止句柄泄漏。
未来学习重点:
1. 深入理解各类I/O模型的优缺点:
继续深入学习阻塞、非阻塞、异步I/O的工作机制,掌握在不同场景下选择合适的I/O模型。
2. 多进程/多线程与I/O操作的结合:
探索如何通过多进程或多线程编程来提高I/O处理的并发性和效率,特别是在高性能计算或服务器开发中的应用。
3. 优化I/O性能:
探讨如何通过系统级优化、缓存管理、批量操作等手段提高I/O操作的效率。
4. 掌握高效的文件I/O处理方法:
针对大数据量文件的读写操作,学习使用内存映射文件(mmap)等高效手段,进一步提升I/O性能。