chapter 9 I/O库函数

发布时间 2023-09-17 00:43:34作者: 20211108俞振阳

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函数的算法

  1. fread()的算法

  • 第一次调用fread()时,FILE结构体的缓存区是空的。
  • fread()使用保存的文件描述符fd通过系统调用函数read(fd, fbuffer, BLKSIZE)来填充内部fbuf[ ]的一个块数据。然后初始化fbuf[]的指针、计数器和状态变量,并指示内部缓冲区内有一个块数据。
  • 它然后尝试从内部缓冲区复制数据到程序的缓冲区区域。如果内部缓冲区没有足够的数据,则发出额外的read()系统调用来填充内部缓冲区,将数据从内部缓冲区传输到程序缓冲区,直到满足所需的字节数(或文件没有更多数据为止)。
  • 在复制数据到程序的缓冲区区域后,它更新内部缓冲区的指针、计数器等等,使其准备好下一个fread()请求。然后返回实际读取的数据对象的数量。
  1. fwrite()的算法

  • fwrite()的算法与fread()类似,但是数据传输方向不同。
  • 最初,FILE结构体的内部缓冲区是空的。在每次fwrite()调用中,它将数据写入内部缓冲区,并调整缓冲区的指针、计数器和状态变量以跟踪缓冲区中的字节数。如果缓冲区变满了,则发出write()系统调用来将整个缓冲区写入OS内核。
  1. 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系统调用

  1. I/O库模式

fopen()中的模式参数可以指定为 "r","w","a",表示为读、写、追加。

每个模式字符串可以包括一个 + 号,这意味着对于读、写,在写或者追加的情况下,如果文件不存在,则创建文件。

r+:打开文件进行读写,不截断文件。

w+:打开文件进行读写,但是会先截断文件,如果文件不存在则进行创建。

a+:打开文件进行读写(追加),如果文件不存在则进行创建。

  1. 字符模式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()的缩写。相反,它们可以被实现为宏,以避免额外的函数调用。

  1. 行模式I/O

char *fgets(char *buf, int size, FILE *fp):从 fp 中读取一行(以 \n 结尾),最多读取 size 个字符到 buf 中。

int fputs(char *buf, FILE *fp):将一行内容从 buf 中写入到 fp 中。

  1. 格式化I/O

这应该是最常用的输入/输出函数。包括:

  • 格式化输入:
scanf(char *FMT, &items); // 从 stdin 中读取
fscanf(fp, char *FMT, &items); // 从文件流中读取
  • 格式化输出:
printf(char *FMT, items); // 输出到 stdout
fprintf(fp, char *FMT, items); // 输出到文件流中
  1. 内存中的转换函数

  • 这两个函数不是 I/O 函数,而是内存中的数据转换函数:
sscanf(buf, FMT, &items); // 从 buf[] 中读取
sprintf(buf,FMT, items); // 存储到 buf[] 中
  1. 其他I/O库函数

  • 包括:
`fseek()`, `ftell()`, `rewind()`: 改变文件流的读写位置。

`feof()`, `ferr()`, `fileno()`: 检查文件流的状态。

`fdopen()`: 使用文件描述符打开文件流。

`freopen()`: 使用新名称重新打开现有流。

`setbuf()`, `setvbuf()`: 设置缓冲区的方案。

`popen()`: 创建一个管道,fork一个子进程来调用 `sh`。
  1. 限制混合 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 文件流缓冲和可变参数的函数

  1. 文件流缓冲

  • 文件流使用内部缓冲区进行读写操作。
  • 文件流可以有三种缓冲方案:无缓冲、行缓冲和完全缓冲。
  • 使用setvbuf()函数可以设置文件流的缓冲区、缓冲区大小和缓冲方案。
  • 对于行缓冲或完全缓冲的流,可以使用fflush()函数立即刷新缓冲区。
  1. 可变参数的函数

  • 函数可以使用可变数量的参数来调用,例如printf()函数。
  • 这是为了方便使用,在实现时必须至少声明一个参数,后跟...表示可变参数。
  • 在函数内部,可以使用va_start()va_arg()va_end()宏来访问参数。
  • va_start()用于从最后一个已知参数开始参数列表。
  • va_arg()用于获取下一个参数的类型。
  • va_end()用于清除参数列表。
  1. 示例程序

  • 以下是一个示例程序,展示了如何使用可变参数的函数:
#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 mint n,后面是m个整数和n个字符串。通过使用va_list宏提取参数,程序打印了出预期的结果。

  • 文件流缓冲可以通过设置缓冲区和缓冲方案来优化数据的读写操作。可变参数的函数使得函数调用更加灵活,可以接受不同数量和类型的参数。

1.6 类printf函数编写

项目要求编写类printf() 函数,可以格式化打印字符、字符串、无符号整数、有符号整数(十进制)和无符号整数(十六进制)。

  1. 基础代码实现

首先定义一个打印字符串的函数 prints(char *s),再基于此实现打印无符号整数(十进制)的函数 printu(),使用这个函数再实现打印有符号整数的函数 printd(),最后实现打印无符号整数(十六进制)地址的函数 printx()。

  1. 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() 函数。

  1. 项目细化

可以将 tab 键定义为 8 个空格,然后添加 %t 标记以表示 tab。我们还可以修改 %u、%d 和 %x,例如 %8d 表示在 8 个字符的空间内打印整数并右对齐。

1.7 学习笔记总结

  1. 库I/O函数的作用及其与系统调用的优势;
  2. 库I/O函数与系统调用之间的关系;
  3. 库I/O函数的算法,包括fread、fwrite和fclose的算法;
  4. 库I/O函数的不同模式,包括字符模式、行模式、结构化记录模式和格式化I/O操作;
  5. 文件流的缓冲机制,不同缓冲机制的效果;
  6. 具有不同参数的函数以及如何使用stdarg宏访问参数;

2.实操环节

3.苏格拉底挑战