chapter 9 I/O库函数
chapter 9 I/O库函数
1.学习笔记
1.1Library I/O 函数 vs. 系统调用
使用库I/O函数和使用系统调用函数进行文件I/O的不同方法。系统调用函数包括open(),read(),write(),lseek()和close(),在Unix/Linux中,库I/O函数是基于系统调用函数之上构建的。库I/O函数包括fopen(),fread(),fwrite(),fseek()和fclose()。
系统调用函数 | 描述 |
---|---|
open() | 打开或创建一个文件 |
read() | 从打开的文件中读取数据 |
write() | 向打开的文件写入数据 |
lseek() | 将文件指针移动到新的位置 |
close() | 关闭已打开的文件 |
库I/O函数 | 描述 |
---|---|
fopen() | 打开或创建一个文件 |
fread() | 从打开的文件中读取数据 |
fwrite() | 向打开的文件写入数据 |
fseek() | 将文件指针移动到新的位置 |
fclose() | 关闭已打开的文件 |
在使用系统调用函数的程序中,文件描述符是一个整数,而在使用库I/O函数的程序中,文件流指针是一个FILE结构体指针。使用系统调用函数进行I/O时需要逐个字节写入,效率较低,而使用库I/O函数可以通过使用内部缓冲来提高效率。
相比于系统调用函数,使用库I/O函数的优势是简单易学,适合处理小文件,但对于大文件,使用系统调用函数会更好。库I/O函数具有更好的移植性和可移植性,因为是标准C库的一部分,在不同的操作系统上使用方法相同。
1.2 Library I/O函数的算法
-
fread()的算法
- 第一次调用fread()时,FILE结构体的缓存区是空的。
- fread()使用保存的文件描述符fd通过系统调用函数read(fd, fbuffer, BLKSIZE)来填充内部fbuf[ ]的一个块数据。然后初始化fbuf[]的指针、计数器和状态变量,并指示内部缓冲区内有一个块数据。
- 它然后尝试从内部缓冲区复制数据到程序的缓冲区区域。如果内部缓冲区没有足够的数据,则发出额外的read()系统调用来填充内部缓冲区,将数据从内部缓冲区传输到程序缓冲区,直到满足所需的字节数(或文件没有更多数据为止)。
- 在复制数据到程序的缓冲区区域后,它更新内部缓冲区的指针、计数器等等,使其准备好下一个fread()请求。然后返回实际读取的数据对象的数量。
-
fwrite()的算法
- fwrite()的算法与fread()类似,但是数据传输方向不同。
- 最初,FILE结构体的内部缓冲区是空的。在每次fwrite()调用中,它将数据写入内部缓冲区,并调整缓冲区的指针、计数器和状态变量以跟踪缓冲区中的字节数。如果缓冲区变满了,则发出write()系统调用来将整个缓冲区写入OS内核。
-
fclose()的算法
- 如果该文件被打开为WRITE,则fclose()首先清空FILE流的本地缓冲区。
- 然后它发出一个close(fd)系统调用来关闭文件描述符。最后,它释放FILE结构体,并将FILE指针重置为NULL。
1.3 库I/O函数和系统调用函数的使用
- 对于以BLKSIZE为单位读/写数据,read()只需要一个拷贝操作,而fread()需要两次拷贝操作,因为它需要先将数据拷贝到内部缓冲区,再将数据拷贝到程序缓冲区。
- 如果内部缓冲区没有足够的数据时,fread()需要发出额外的read()系统调用来补充内部缓冲区,而read()它可以直接从内核中进行数据拷贝,因此相对于feof()来说是非常有效的。
- 如果要按字节大小进行读/写,则fread()和fwrite()会比read()和write()更好,因为它们只需进入操作系统内核填充或刷新内部缓冲区,而不是每个字节都进入操作系统内核。
1.4 I/O库使用or系统调用
-
I/O库模式
fopen()
中的模式参数可以指定为 "r","w","a",表示为读、写、追加。
每个模式字符串可以包括一个 + 号,这意味着对于读、写,在写或者追加的情况下,如果文件不存在,则创建文件。
r+
:打开文件进行读写,不截断文件。
w+
:打开文件进行读写,但是会先截断文件,如果文件不存在则进行创建。
a+
:打开文件进行读写(追加),如果文件不存在则进行创建。
-
字符模式I/O
int fgetc(FILE * fp)
:从 fp
中获取一个字符,转换为整数。
int ungetc(int c, FILE *fp)
:将之前通过 fgetc()
获取的字符放回流中。
int fputc(int c, FILE *fp)
:向 fp
输出一个字符。
注意,fgetc()
返回的是整数而不是字符。这是因为它必须在文件末尾返回 EOF。EOF符号通常是整数-1,它与 FILE 流中的任何字符区分开来。
对于 fp = stdin
or stdout
,可以使用 c = getchar(); putchar(c); 代替。为了提高运行效率,getchar()
和putchar()
经常不是getc()
和putc()
的缩写。相反,它们可以被实现为宏,以避免额外的函数调用。
-
行模式I/O
char *fgets(char *buf, int size, FILE *fp)
:从 fp
中读取一行(以 \n 结尾),最多读取 size
个字符到 buf
中。
int fputs(char *buf, FILE *fp)
:将一行内容从 buf
中写入到 fp
中。
-
格式化I/O
这应该是最常用的输入/输出函数。包括:
- 格式化输入:
scanf(char *FMT, &items); // 从 stdin 中读取
fscanf(fp, char *FMT, &items); // 从文件流中读取
- 格式化输出:
printf(char *FMT, items); // 输出到 stdout
fprintf(fp, char *FMT, items); // 输出到文件流中
-
内存中的转换函数
- 这两个函数不是 I/O 函数,而是内存中的数据转换函数:
sscanf(buf, FMT, &items); // 从 buf[] 中读取
sprintf(buf,FMT, items); // 存储到 buf[] 中
-
其他I/O库函数
- 包括:
`fseek()`, `ftell()`, `rewind()`: 改变文件流的读写位置。
`feof()`, `ferr()`, `fileno()`: 检查文件流的状态。
`fdopen()`: 使用文件描述符打开文件流。
`freopen()`: 使用新名称重新打开现有流。
`setbuf()`, `setvbuf()`: 设置缓冲区的方案。
`popen()`: 创建一个管道,fork一个子进程来调用 `sh`。
-
限制混合 fread-fwrite
-
当一个文件流既是 R | W 时,对于使用混合
fread()
和fwrite()
的调用,存在限制。规范要求在每对fread()
和fwrite()
之间至少有一个fseek()
或ftell()
。 -
Example 9.5: Mixed fread-fwrite
:在 HP-UX 和 Linux 下运行此程序会产生不同的结果。Linex 会修改文件字节,HP-UX 则会在原文件末尾添加字节。如果在fread()
和fwrite()
之间插入fseek(fp, (long)20, 0)
,则结果将相同(并且正确)。
1.5 文件流缓冲和可变参数的函数
-
文件流缓冲
- 文件流使用内部缓冲区进行读写操作。
- 文件流可以有三种缓冲方案:无缓冲、行缓冲和完全缓冲。
- 使用
setvbuf()
函数可以设置文件流的缓冲区、缓冲区大小和缓冲方案。 - 对于行缓冲或完全缓冲的流,可以使用
fflush()
函数立即刷新缓冲区。
-
可变参数的函数
- 函数可以使用可变数量的参数来调用,例如
printf()
函数。 - 这是为了方便使用,在实现时必须至少声明一个参数,后跟
...
表示可变参数。 - 在函数内部,可以使用
va_start()
、va_arg()
和va_end()
宏来访问参数。 va_start()
用于从最后一个已知参数开始参数列表。va_arg()
用于获取下一个参数的类型。va_end()
用于清除参数列表。
-
示例程序
- 以下是一个示例程序,展示了如何使用可变参数的函数:
#include <stdio.h>
#include <stdarg.h>
int func(int m, int n, ...)
{
int i;
va_list ap;
va_start(ap, n);
for (i = 0; i < m; i++)
{
printf("%d ", va_arg(ap, int));
}
for(i = 0; i < n; i++)
{
printf("%s ", va_arg(ap, char *));
}
va_end(ap);
}
int main()
{
func(3, 2, 1, 2, 3, "test", "ok");
}
-
该程序假设函数
func()
有两个已知参数int m
和int n
,后面是m
个整数和n
个字符串。通过使用va_list
宏提取参数,程序打印了出预期的结果。 -
文件流缓冲可以通过设置缓冲区和缓冲方案来优化数据的读写操作。可变参数的函数使得函数调用更加灵活,可以接受不同数量和类型的参数。
1.6 类printf函数编写
项目要求编写类printf() 函数,可以格式化打印字符、字符串、无符号整数、有符号整数(十进制)和无符号整数(十六进制)。
-
基础代码实现
首先定义一个打印字符串的函数 prints(char *s),再基于此实现打印无符号整数(十进制)的函数 printu(),使用这个函数再实现打印有符号整数的函数 printd(),最后实现打印无符号整数(十六进制)地址的函数 printx()。
-
myprintf() 算法
假设格式字符串fmt = “char=%c string=%s integer=%d u32=%x\n",它需要 4 个额外参数。myprintf() 算法如下:
- 扫描格式字符串 fmt,打印任何非 % 的字符。对于每个'\n'字符,多打印一个'\r'字符;
- 遇到一个 '%' 字符,获取下一个字符,它必须是‘c’、‘s’、‘u’、‘d’或‘x’之一。使用 va_arg(ap, type) 提取相应的参数,调用相应的打印函数;
- 当扫描 fmt 字符串结束时,算法结束。
这个算法可以实现类 printf() 函数。
-
项目细化
可以将 tab 键定义为 8 个空格,然后添加 %t 标记以表示 tab。我们还可以修改 %u、%d 和 %x,例如 %8d 表示在 8 个字符的空间内打印整数并右对齐。
1.7 学习笔记总结
- 库I/O函数的作用及其与系统调用的优势;
- 库I/O函数与系统调用之间的关系;
- 库I/O函数的算法,包括fread、fwrite和fclose的算法;
- 库I/O函数的不同模式,包括字符模式、行模式、结构化记录模式和格式化I/O操作;
- 文件流的缓冲机制,不同缓冲机制的效果;
- 具有不同参数的函数以及如何使用stdarg宏访问参数;
2.苏格拉底挑战
open
read
write
read
write
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)