1. 流和FILE对象

在第三章的系统调用都是围绕文件描述符fd的。但是标准I/O库函数操作则是围绕流进行的。当使用标准I/O库打开或创建一个文件时,使用一个流与一个文件关联。

当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。FILE对象包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区长度、当前缓冲区中字符数、出错标志等

我们称指向FILE对象的指针(FILE*)称为文件指针

2. 标准输入、标准输出、标准错误

对每个进程预定义了三个流:标准输入、标准输出、标准错误。这些流引用的文件与STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO所引用的文件相同。

这三个标准I/O流分别是文件指针stdin、stdout、stderr

3. 缓冲

标准I/O库也是带缓冲的I/O,标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数,减少用户态到内核态切换次数。它对每个I/O流自动进行缓冲管理,让应用程序不用考虑缓冲区大小、管理等问题。

缓冲是标准IO库提供的,缓冲区存在于用户空间(不管是fread还是fwrite)。在调用fwrite的时候,先把数据放在缓冲区。不直接使用系统调用,切换到内核态。而是等到缓冲区满或是调用fflush等条件满足时,再一次性调用write,把数据拷贝到内核空间。对于fread而言,就是在调用fread的时候,切换到内核态,内核态读取缓冲区能够接受的大小的数据, 然后返回给fread函数。下次再调用fread的时候,可能只需要从缓冲区读取,而不需要再到内核空间拷贝了

标准I/O提供了以下三种缓冲:

  • 全缓冲

    在填满标准I/O缓冲区后再进行实际I/O操作。对于磁盘上的文件通常是由标准I/O实施全缓冲的

  • 行缓冲

    当在输入和输出中遇到换行字符,标准I/O库进行实际I/O操作。比如我们每次输出一个字符(fputc),但是只有写了一行之后才进行实际I/O操作。

    当流涉及终端(如标准输入、标准输出)时,通常使用行缓冲

    只要填满了缓冲区,那么即使还没有写一个换行符也进行实际I/O

  • 不带缓冲

    标准错误流stderr通常不带缓冲,使得错误信息能够尽快显示出来,而不管是否有换行符。

冲洗flush:

冲洗表名标准I/O的写操作。缓冲区可由标准I/O例程自动冲洗(比如填满缓冲区时),或者调用fflush冲洗一个流。在标准I/O库中,flush(冲洗)意为将缓冲区中的内容写到磁盘上;在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。

int fflush(FILE *stream);

使所有未写的(在缓冲区中的)数据都被传送至内核,如果stream是NULL,则导致所有输出流都被冲洗。

4. 更改流的缓冲类型

对于一个给定流,如果不喜欢系统默认的缓冲那个类型,可以修改缓冲类型

void setbuf(FILE *stream, char *buf);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

**函数setbuf:**主要用于打开和关闭缓冲机制

  • 如果要带缓冲,buf必须指向一个长度至少为 BUFSIZ 字节的缓冲区,BUFSIZ 是一个宏常量,表示数组的长度。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可以将其设置为行缓冲。

    # define BUFSIZ _IO_BUFSIZ
    #define _IO_BUFSIZ _G_BUFSIZ
    #define _G_BUFSIZ 8192 // 可见BUFSIZ大小是8192字节
    
  • 如果不要缓冲,buf为NULL

**函数setvbuf:**可以精确指定缓冲类型

  • mode参数指定缓冲类型:

    • _IOFBF:全缓冲,buf和size指定缓冲区和缓冲区大小
    • _IOLBF:行缓冲,buf和size指定缓冲区和缓冲区大小
    • _IONBF:不带缓冲,忽略buf和size参数
  • 如果mode指定为带缓冲的,但是buf参数是nullptr,则会自动为该流分配一个适当长度的缓冲区(即由BUFSIZ指定的缓冲区长度,某些C库实现使用stat结构体的st_blksize成员决定最佳I/O缓冲区长度)

需要注意,某些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于size。

如果由标准I/O库自动分配缓冲区的话,关闭该流会导致自动释放缓冲区;如果是自己指定缓冲区的话,关闭该流不会释放缓冲区。

5. 打开流

通过以下函数打开一个标准I/O流

FILE *fopen(const char *pathname, const char *mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *pathname, const char *mode, FILE *stream);

fdopen:

取一个标准I/O流与一个已经打开的文件描述符相结合。常用于由管道和网络通信函数返回的描述符,因为这些特殊类型的文件不能用标准I/O函数fopen打开。所以需要先用设备专门函数(如socket)以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述符结合。

freopen:

把一个新的文件名 filename 与给定的打开的流 stream 关联,同时关闭流中的旧文件。常用于将一个指定的文件打开为一个预定义的流:stdin、stdout、stderr。文件重定向。

参数mode:

对I/O的读、写方式

mode 说明 open(2)标志
r或rb 以只读方式打开文件,该文件必须存在。 O_RDONLY
w或wb 打开只写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。 O_WRONLY | O_CREAT | O_TRUNC
a或ab 以附加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件尾后 O_WRONLY | O_CREAT | O_APPEND
r+或r+b或rb+ 以读/写方式打开文件,该文件必须存在。 O_RDWR
w+或w+b或wb+ 打开可读/写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。 O_RDWR | O_CREAT | O_TRUNC
a+或a+b或ab+ 以附加方式打开可读/写的文件。若文件不存在,则会创建该文件,如果文件存在,则写入的数据会被加到文件尾后 O_RDWR | O_CREAT | O_APPEND
  • 其中字符’b’用于标准I/O库区分文本文件和二进制文件。但是UNIX内核并不对这两种文件进行区分,因此UNIX环境中指定字符’b’无用

  • 如果有多个进程同时用标准I/O追加写相同文件,那么来自每个进程数据都会正确写入到文件中(即该函数是线程安全的)。

  • 以读写方式打开一个文件时,具有以下限制:

    • 如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入

    • 如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出

  • 当使用标准I/O库创建文件时,默认新创建的文件访问权限位是0666:S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH。且这个访问权限是根据umask文件模式创建屏蔽字的。

  • 除非流引用终端设备,否则系统默认流被打开时是全缓冲的。若流引用终端设备,则该流是行缓冲。

调用fclose关闭一个打开的流

int fclose(FILE *stream);

在文件被关闭之前,冲洗缓冲中的输出数据(写入到文件中),缓冲区中的任何输入数据都被丢弃。如果标准I/O库为该流自动分配了缓冲区,则释放此缓冲区。

当一个进程正常关闭时(调用exit函数或者main函数返回),则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。

6. 每次从流中读写一个字符

以下函数用于一次读一个字符

int getc(FILE *stream);
int fgetc(FILE *stream);
int getchar(void);

函数getchar等同于getc(stdin)。

getc和fgetc的区别是:getc可以实现为宏,但是fgetc一定是个函数。因此fgetc可以得到其地址,这就允许将fgetc的地址作为一个参数传给另一个函数。

这三个函数返回流中的下一个字符时,将其unsigned char类型转换为int类型。这是因为可以返回有可能的字符值再加上一个已出错或已达到文件末尾的值(EOF)。常量EOF被要求为一个负值,通常是-1。

# define EOF (-1)

注意,这三个函数出错或者到达文件末尾可能返回相同的值(如-1),这时为了判断是出错还是EOF就要适用ferror函数或feof函数

int feof(FILE *stream);      // 判断该流的操作是否出错,出错返回非0,否则返回0
int ferror(FILE *stream);   // 判断该流是否到了文件尾,到末尾返回非0,否则返回0
void clearerr(FILE *stream);// 清除给定流的文件结束和错误标识符

每个流在FILE对象中维护了两个标志

  • 出错标志
  • 文件结束标志(eof)

调用clearerr函数可以清除这两个标志。

可以调用ungetc把字符 c推入到指定的流 stream(必须是输入流不能是输出流) 中,以便它是下一个被读取到的字符

int ungetc(int c, FILE *stream);

注意ungetc并没有把字符写入到底层文件或设备中,只是将它们写回标准I/O库的流缓冲区中

以下函数用于一次写入一个字符

int putc(int c, FILE *stream);
int fputc(int c, FILE *stream);
int putchar(int c);

putchar©相当于putc(c,stdout)。putc可以实现为宏,fputc只能实现为函数,可用作参数传递。

7. 每次从流中读写一行

7.1 每次输入一行

char *fgets(char *s, int size, FILE *stream);
char *gets(char *s); // 不推荐

gets从stdin中读,fgets从指定输入流中读。

fgets函数最多读取size-1个字符,因为缓冲区以null结尾。此函数一直读直到换行符为止,但是如果读到size-1个字符还没有换行符,则只能返回一个不完整的行,下一次fgets从该行继续读。

不推荐使用gets,因为不能指定缓冲区长度,可能造成缓冲区溢出,将内容写入到缓冲区之后的存储空间中。

7.2 输出字符串

注意这两个函数并不是输出一行数据,而是输出以null结尾的字符串

int puts(const char *s); // 不推荐
int fputs(const char *s, FILE *stream);

puts将一个以null为结尾的字符串写到stdout

fputs将一个以null为结尾的字符串写到指定流中

不推荐使用puts,即使它不像gets那样不安全

8. 二进制I/O

通过fread和fwrite函数进行二进制I/O操作

相较于之前一次处理一个字符或者处理一行数据,有时候希望一次读写一个完整结构,且不像fputs函数那样遇见null就停止,此时二进制I/O就派上用场。

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
  • size – 这是要读写的每个元素的大小,以字节为单位。
  • nmemb – 这是元素的个数,每个元素的大小为 size 字节。
  • stream – 要操作的流

fread和fwrite返回读或写的对象数。对于fread,如果出错或到达文件末尾,返回值可能小于nmemb,这时应该通过ferror和feof判断到底是哪种情况;对于fwrite,如果返回值小于nmemb出错。

需要注意,在不同的系统之间进行读写可能存在问题,因为在一个结构体中同一成员的偏移量可能随编译器和系统的不同而不同(由于不同对齐需求)。比如同一个结构体由于对齐需求不同,导致该结构体对象大小在不同系统中不一致。并且存储多字节整数和浮点值的二进制格式在不同的系统间也可能不同。

9. 定位流(读写指针)

获取文件偏移量

long ftell(FILE *stream);
off_t ftello(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);

指定文件偏移量

int fseek(FILE *stream, long offset, int whence);
int fseeko(FILE *stream, off_t offset, int whence);
int fgetpos(FILE *stream, fpos_t *pos);

参数whence:

  • SEEK_SET:文件起始位置
  • SEEK_CUR:文件当前偏移量
  • SEEK_END:文件末尾

将文件读写指针重新定位至流开头

void rewind(FILE *stream);

10. 格式化I/O流

10.1 格式化输出

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
// 以上函数返回成功输出字符数,出错返回负值
int sprintf(char *str, const char *format, ...); //不推荐,不安全
int snprintf(char *str, size_t size, const char *format, ...); //推荐,安全
// 成功返回存入数组字符数,出错返回负值

printf写至stdout,fprintf写至指定流,dprintf写至指定文件描述符,sprintf和snprintf将格式化的字符写至数组str。

注意,sprintf可能造成由str指向的缓冲区溢出。因此应该使用snprintf函数,在该函数中缓冲区长度是一个显式参数,超过该长度的所有字符都会被丢弃。

printf函数族的变体:vprintf

vprintf与printf基本类似,只是可变参数(…)替换成了va_list(va_list是在C语言中解决变参问题的一组宏,用于获取不确定个数的参数。)

int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vdprintf(int fd, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

10.2 格式化输入

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

scanf函数族用于分析输入字符串。其中scanf从stdin中获取输入、fscanf从指定流中获取输入、sscanf从字符串中获取输入。

scanf函数族的变体:vscanf

int vscanf(const char *format, va_list ap);
int vsscanf(const char *str, const char *format, va_list ap);
int vfscanf(FILE *stream, const char *format, va_list ap);

11. fileno函数

标准I/O流(FILE对象)都有一个关联的进行实际I/O的文件描述符,通过fileno获取其描述符

int fileno(FILE *stream);

12. 临时文件

通过以下函数帮忙创建临时文件

char *tmpnam(char *buf);
FILE *tmpfile(void);

12.1 tmpnam函数:

返回一个与现有文件名不同的一个有效路径名字符串。每次调用时都返回一个不同的路径名,最多调用TMP_MAX次。

参数buf应该是一个长度至少是能保存L_tmpnam个字符的数组,所产生的路径名保存在buf中;若buff是NULL,则所产生的路径名存放在一个静态区中,并返回指向该静态区的指针,后续再次调用tmpnam时会覆盖该存储区(这意味着我们调用tmpnam函数后应该保存该路径名的副本而不是指针,防止再次调用导致路径名被覆盖)

#define TMP_MAX 238328
#define L_tmpnam 20

示例:

    for(int i = 0 ; i < 5 ; ++i) {cout << tmpnam(nullptr) << endl;}
/* 输出:
/tmp/filefGEnn1
/tmp/fileSWuc6Y
/tmp/fileck6gPW
/tmp/fileoUbuyU
/tmp/fileAS5ThS
*/

12.2 tmpfile函数:

创建一个二进制文件(wb+,但是注意UNIX对二进制文件不进行特殊区分),在关闭该文件或程序时自动删除该文件

tmpfile实现原理

先使用tmpnam产生一个唯一路径名,然后用该路径名创建一个文件,并立即unlink它(对一个文件解除链接并不删除其内容,关闭该文件时才删除其内容。关闭文件可以是显式的,也可以是程序终止时自动进行)。

12.3 mkdtemp和mkstemp函数

char *mkdtemp(char *template);
int mkstemp(char *template);

mkdtemp函数创建了一个目录,该目录有一个唯一的名字;mkstemp函数创建了一个文件,该文件有一个唯一的名字。名字是由template字符串参数决定的,这个字符串后6个字符(这六个字符必须是"XXXXXX")会被替换成其他字符以构建一个唯一的路径名

mkdtemp函数成功返回该路径字符串指针,创建的目录访问权限是0700(可以由umask限制)。

mkstemp创建一个普通文件并读写打开该文件,返回其文件描述符。创建的文件的访问权限是0600。

与tmpfile不同,这两个函数创建的目录和文件并不会自动删除,如果要删除的话必须手动进行删除(如调用unlink函数)。

不推荐使用tmpnam获取唯一文件名再创建文件,因为这两部操作不是原子的,可能有其他进程使用相同名字创建文件。因此建议使用tmpfile或mkstemp函数。

示例:

    for(int i = 0 ; i < 3 ; ++i) {char str[] = "abXXXXXX";mkdtemp(str);cout << "目录名 " << str << endl;char str1[] = "abXXXXXX";mkstemp(str1);cout << "文件名 " << str1 << endl;}
/* 输出:
目录名 ablQAJnP
文件名 ab5Ud5xT
目录名 abWgVCIX
文件名 abbMRhT1
目录名 abkLj435
文件名 abFutYea
*/

13. 内存流

在SUSv4中支持了内存流。这就是标准I/O流,虽然仍使用FILE指针进行访问,但是其实并没有底层文件。所有的I/O流都是通过在缓冲区和主存之间来回传送字节来完成的

使用第三方库的时候,很可能遇到这样的问题,我们需要处理某个文件,而这个文件不一定是从本地磁盘上读取,可能是分布式文件系统或者其他地方,而第三方库的接口却只提供了一个File *参数,意味着只能从磁盘加载,没法直接处理已经加载到内存的数据。

这个时候,fmemopen就可以派上用场了,完美的将FILE对象映射到内存上,无需从磁盘上读取了。

FILE *fmemopen(void *buf, size_t size, const char *mode);

fmemopen函数允许调用者提供缓冲区用于内存流:buf参数指向缓冲区开始的位置,size参数指定了缓冲区大小的字节数。如果buf参数为NULL,fmemopen函数分配size字节数的缓冲区。在这种情况下,流关闭时缓冲区被释放。

type参数用于指示如何使用流,如r、w、a、r+、w+、a+等

示例:

int main(int argc, char* argv[]) {char str[] = "demaxiya 123";int size = strlen(str);FILE* f = fmemopen(str,size,"r+");char c;while((c = fgetc(f)) != EOF) {putchar(c);}fclose(f);putchar('\n');
}
// 打印 demaxiya 123

13. 标准I/O库的问题

标准I/O库的不足之处是效率不高,这与它需要复制的数据量有关。通常需要复制两次数据:一次是在内核和标准I/O缓冲区之间(当调用read和write时),第二次是在标准I/O缓冲区和用户程序中的缓冲区之间。有一些其他的库实现了比标准I/O库更快的I/O库

《UNIX环境高级编程》笔记 第五章-标准IO库相关推荐

  1. UNIX 环境高级编程总结——第五章 标准I/O 库

    5.1 流和 FILE 对象 对于标准 I/O 库,它们的操作则是围绕流(stream)进行的. 当打开一个流时,标准 I/O 函数 fopen 返回一个指向 FILE 对象的指针. 为了引用一个流, ...

  2. Unix环境高级编程 笔记

    Unix环境高级编程(第二版)学习笔记 这是一次较长时间的整理,然而跳跃了一些章节和很多知识点,仍然是很不完善很不全面的. 前言 操作系统某些问题 严格意义上,可将操作系统定义为一种软件,它控制计算机 ...

  3. UNIX环境高级编程笔记之文件I/O

    一.总结 在写之前,先唠几句,<UNIX环境高级编程>,简称APUE,这本书简直是本神书,像我这种小白,基本上每看完一章都是"哇"这种很吃惊的表情.其实大概三年前,那会 ...

  4. UINX环境高级编程笔记 第3章 文件I/O

    UNIX环境高级编程 第三章 文件I/O 3.1 引言 3.2 文件描述符 3.3 函数open和openat 3.4 函数creat 3.5 函数close 3.6 函数lseek 3.7 函数re ...

  5. UNIX环境高级编程笔记

    1.setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, len);   SO_REUSEADDR套接口选项允许为以下四个不同的目的提供服务:   ...

  6. UNIX环境高级编程笔记(2)- STDIN_FILENO、STDOUT_FILENO和stdin、stdout的区别

    目录 前言 一.STDIN_FILENO.STDOUT_FILENO介绍 二.stdin.stdout介绍 三.代码例程 1.文件描述符的使用 2.流的使用 3.代码标记 总结 前言 本章主要通过UN ...

  7. 《Unix环境高级编程》Note——第一章基础知识

    文章目录 第一章 Unix基础知识 1.引言 2.Unix体系结构 3.登录 4.文件和目录 5.输入和输出 6.程序和进程 7.出错处理 8.用户标识 9.信号 10.时间值 11.系统调用和库函数 ...

  8. UNIX环境高级编程笔记之进程控制

    本章重点介绍了进程控制的几个函数:fork.exec族._exit.wait和waitpid等,主要需要掌握的是父进程和子进程之间的运行机制,怎么处理进程的正常和异常终止.以及怎么让进程执行不同的程序 ...

  9. UNIX环境高级编程笔记(14)- 函数sigsuspend 实现父进程子进程同步

    前言 本章主要介绍sigsuspend函数以及实现父进程子进程通过信号的同步. 一.函数sigsuspend #include<signal.h> int sigsuspend(const ...

最新文章

  1. thinkphp 使用外部php或html 原理
  2. oracle linux 5.8安装oracle 11g rac环境之grid安装
  3. Win7 VS2013环境编译Squirrel 3.0.7
  4. 判断数组有哪些方法,100%准确的方法
  5. #论文 《Wide Deep Learning for Recommender System》翻译
  6. 基于SEAL库实现PSI-报错实录1
  7. flask第二十四篇——模板【6】自定义过滤器
  8. VirtualBox使用技巧:关于undo差分盘与端口映射
  9. 图片裁剪和异步上传插件--一步到位(记录)
  10. HTML+CSS+JS实现 ❤️发光糖果泡泡动画特效❤️
  11. 如何优化Java GC
  12. JSP分页显示(前端处理)
  13. 计算机信函 教案模板,一年级信息技术课教案模板三篇
  14. 理解和解决requireJS的报错:MODULE NAME HAS NOT BEEN LOADED YET FOR CONTEXT
  15. idea2020的Run/Debug Configurations出现叉号
  16. 微信小程序介绍、区别
  17. 地铁 java_“地铁系统”简易代码
  18. Docker 问题集锦(32) - 云原生大趋势下的容器化技术现状与发展
  19. 文本分类中样本的筛选(基于VSM模型)
  20. java 冬季运动会_2015关于冬天的作文:寒冷的冬天续写

热门文章

  1. 《计算机系统与维护》— 计算机的发展历程
  2. linux设置网卡速度,linux系统下网卡通信速率修改方法
  3. Vivado 随笔(4) 创建及管理多个Runs?
  4. HTML期末大作业~ 海贼王6页代码质量好~ 学生网页设计作业源码(HTML+CSS+JS)
  5. 2018.07.12【2018提高组】模拟B组 【NOIP2015模拟10.27】魔道研究
  6. java 今天、明天、后天、明年今天,昨天、前天、过去七天、过去一年日期的整体封装;
  7. 当我们再看在线直播课的时候,到底在看什么?
  8. 华丰科技将于12月13日上会:业绩波动明显,海通证券等为股东
  9. 制作一个机器螳螂【内附资料下载链接】
  10. C++ 课本习题(程序设计题)