8. 标准I/O

目录

8.1 流和File对象

8.1.1 File对象

8.1.2 流定向

8.1.3 缓冲

8.1.4 流的冲洗

8.2 流的操作

8.2.1 打开流

8.2.2 关闭流

8.3.3 读写流

8.3.4 格式化输入输出

8.3.5 定位流

8.3 临时文件

8.4 内存流

8.5 系统I/O与标准I/O的比较


上一章中我们介绍了系统I/O,它的特点是不带缓冲而且是针对文件描述符标志操作的。本节将要介绍的标准I/O恰好不同,它是有缓冲的,而且针对的是流和FILE对象来操作。标准I/O库是由ISO 标准说明的,其实刚学C语言时我们就有用到,比如著名的stdio.h头文件。在介绍完标准I/O后,文末会对这两种I/O进行简单的比较与总结。

8.1 流和File对象

流的概念比较抽象,我们可以先来大致的理解下文件和流。文件是指一个有序的数据序列,而流我们可以想象成一个传送带,文件中的数据序列不断的放到传送带上,用户直接和这个传送带进行交互操作,这样传送带相当于一个流动的缓冲区,所以我们称之为流。我们一直强调,标准I/O是带缓冲的,这里的流大概就是所谓的缓冲区。

每当我们打开一个文件,相当于将文件和对应的流建立了连接。每个进程都会默认自动打开三个流,分别对应系统I/O中的STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO,它们是stdin,stdout和stderr。

8.1.1 File对象

Linux系统中用File对象来描述流,查看Gilbc中关于FILE的定义发现其中有一个成员就是前述的文件描述符fileno:

struct _IO_FILE
{
  ……..
  int _fileno;
  ……….
};
typedef struct _IO_FILE FILE;

其实FILE结构就是对fd进行了更高层次的封装,以支持更多的功能,比如我们一直强调的的缓冲机制。

8.1.2 流定向

流的定向是指基于流进行的读写操作是单字节操作的还是多字节操作的。流被创建的时候默认是未定向的,它随着我们第一次使用I/O函数违背定向。比如如果第一次使用I/O函数是面向单字节的,则流定向就被设置为单字节,否则就被设置为宽定向。可以用fwide 函数来设置流定向,freopen函数来清除流定向。

#include<stdio.h>
int fwide(FILE *fp, int mode);

参数:mode为正,设定流定向为宽定向;mode为负,设定为单字节定向,mode为0,不设置流定向,只返回流的当前定向方式。

返回值:若流是宽定向,返回正值;字节定向,返回负值;未定向,返回0.

需要注意的是,已经定向的流,fwide函数是无法设置。

8.1.3 缓冲

标准I/O库提供缓冲的目的是为了减少系统调用的次数,打开一个流后,第一次在这个流上执行I/O操作时,相关的I/O函数会调用malloc函数为这个流申请一个缓冲区。流的缓冲类型可以分为三种:

  • 全缓冲:在填满I/O缓冲区后才会进行实际的I/O操作;
  • 行缓冲:当在输入输出中遇到换行符就执行实际I/O操作,通常指向终端设备的流都是行缓冲;
  • 无缓冲:不进行缓冲,类似于直接的系统I/O的操作。

通常对于终端设备的流被设置为行缓冲,否则一般都设置为全缓冲。通过setbuf函数可以更改默认的缓冲类型:

#include<stdio.h>
int setbuf(FILE *restrict fp, char *restrict buf);
int setbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
//成功返回0,失败返回-1

setbuf函数用来设置或者清除缓冲机制,当buf为NULL时将流设置为无缓冲;否则buf指定缓冲区地址,对于终端设备,一般设置为行缓冲,其它设备一般设置为全缓冲。

setvbuf函数则可以根据mode精确的设置缓冲模式,若缓冲模式为无缓冲,则buf和size参数无效;如果缓冲模式为全缓冲或者行缓冲且buf和size为0,则系统默认分配缓冲区和大小。Mode参数有以下三种:

  • _IOFBF:全缓冲
  • _IOLBF:行缓冲
  • _IONBF:无缓冲

对于三个默认打开的标准流,标准错误流一定被设置为无缓冲;标准输入和输出流当指向交互式设备时通常被设置为行缓冲,否则一般被设置为全缓冲。

8.1.4 流的冲洗

流的冲洗是指将缓冲区所有的数据都传送至内核,下面的fflush函数可以实现流的冲洗,作为特例,如果fp参数为NULL,系统中所有输出流都将被冲洗。

#include<stdio.h>
int fflush(FILE *restrict fp);
//成功返回0,失败返回EOF

8.2 流的操作

8.2.1 打开流

可以用下面的三个函数来打开一个流。

#include<stdio.h>
FILE *fopen(const char *pathname, const char *restrict type);
FILE *freopen(const char *pathname, const char *restrict type, FILE *restrict fp);
FILE *fdopen(int fd, const char *restrict type);
//成功返回0,失败返回EOF

参数:pathname就是要打开的文件名;type指定指定读写的方式,可以有以下取值:

Type

Detail

Open (oflag)

r

读打开

O_RDONLY

w

将文件截断为0,或者为写而创建

O_WRONLY|O_CREAT|O_TRUNC

a

追加的方式,即在文件尾为写而打开或创建

O_WRONLY|O_CREAT|O_APPEND

r+

为读和写打开

O_WRONLY

w+

将文件截断为0,或者为读写而创建

O_WRONLY|O_CREAT|O_TRUNC

a+

为在文件尾读写而打开或者创建

O_WRONLY|O_CREAT|O_APPEND

注意:因为linux部分二进制文件和文本文件的,所以rb和r并无差别。

freopen函数在一个指定的流上打开新的文件,若该流已经打开,则关闭流,并清除流定向,这个函数通常用于将一个指定的文件在特定的流上打开:比如经常可以将标准输入输出和出错流流重定向到文件。

fdopen函数将一个已有的文件描述符与特定的流相结合。常用与将管道或者网络通信通道获取的文件描述符与特定流相结合。例如,创建管道可以获取两个文件描述符,但又不能用fopen之类的函数打开,我们就可以将管道对应的文件描述符与一个流结合使用。

此外,对于type类型为读写打开,即有‘+’号时还有以下两条限制要注意:

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

这么做的目的也很明显,就是为了不破坏读写的完整性,简单来说就是希望的读操作,真正读完了,才能去写;期望的写操作,真正写完了才能去读。

8.2.2 关闭流

关于fclose,需要注意的一点就是在文件被关闭之前,会自动flush缓冲区中的数据。

#include<stdio.h>
int flcose(FILE *);
//成功返回0,失败返回EOF

8.3.3 读写流

对流的读写分为以下三种方式:

  • 单字符的I/O:每次读写一个字符,如果流带缓冲,I/O函数会直接处理所有缓冲;
  • 行I/O:每次读写一行,每行都以一个换行符终止;
  • 直接I/O:fread和fwrite函数支持这种类型,每次I/O操作读写某种数量的对象。

1. 单字符的I/O读写

单字符的I/O读写函数如下:

#include<stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
//成功返回下一个字符,失败或者已到文件尾返回EOF
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
//成功返回c,失败返回EOF

关于这两组函数说明如下:

  • getchar等同于getc(stdin),putchar等同于putc(stdout);
  • getc与fgetc的区别是getc是由宏实现而fgetc是由函数实现;
  • 这里返回定义的类型int是因为这些函数返回值可能为负;

对于get的三个函数,不管是出错还是到达文件尾,返回都是EOF,那么如何去区别是哪种呢?可以由以下的这组函数来确认:

int ferror(FILE *fp);
int feof (FILE *fp);
//这两个函数若条件为真,返回非0,否则返回0
Void clearerr(FILE *fp);
//清除标志

还有一种常用的情况时当我们读出字符时,不需要想把它送回缓冲区怎么办?函数ugetc可以实现这个功能。

int ungetc (int c, FILE *fp);

关于这个函数的说明如下:

  • 每次只能回送一个字符,且读出顺序与回送顺序相反;
  • 不能回送EOF,但是当已经到达文件尾,仍然可以回送一个字符。下次再读时会读出这个字符,再读就是EOF。只所以能这样做是因为ungetc会清除该流的文件结束标志。

2. 行I/O

下面的这组函数提供了每次输入或者读出一行的操作。先来看看输入函数:

int gets(char resetrict *buf, FILE *fp);
int fgets (char resetrict *buf, size_t n, FILE *fp);
//若成功返回buf,出错或者已到文件尾则返回EOF

关于这组函数说明如下:

fgets函数指定缓冲区地址和大小,会一直读到下一个换行符为止,但是不能超过n-1个字符,因为最后一个字符必须写入null字符(\0)。需要主要的是读完n-1个字符还没有遇到换行符则fgets只返回一个不完整的行,下一次再调用时会继续从这里读。

gets函数因为没有指定缓冲区大小,所以容易产生缓冲区溢出,所以非常不推荐使用。而且gets函数不会讲换行符写入缓冲区。

说完了读函数,我们再来看看写函数:

int puts(char resetrict *buf, FILE *fp);
int fputs (char resetrict *buf, size_t n, FILE *fp);
//若成功,返回非负值,否则返回EOF

说明如下:

fputs函数将一个以null字节终止的字符串写入到指定的流,终止符null不输出;要注意的是fputs函数遇到换行符会继续输入(这是不是与行缓冲的定义有冲突?),换行符当做一个字符处理。

puts函数一个以null字节终止的字符串写入标准输出流,终止符null不输出,但是puts函数默认自动添加换行符到标准输出。

3. 直接I/O

直接I/O可以一次可以处理多个对象。不同于上两节介绍的读写函数,下面的这组函数并不对换行符或者终止字符作特殊处理。

int fread(char resetrict *ptr, size_t size, size_t nobj, FILE *fp);
int fwrite(char resetrict *ptr, size_t size, size_t nobj, FILE *fp);
//返回读或写的对象数

这两个函数指定缓冲区的地址和大小以及对象数,其中size指每个对象的大小,nobj指对象数。这组函数常用来读取某个结构或者数组,例如下面这段代码读取一个item结构体的数据:

struct {
       short count;
       long total;
       char name[NAMESIZE];
}item;
fwrite(&item, sizeof(item),1,fp);

关于这组函数的说明如下:

  • 如果fread函数返回的值小于nobj,可能是因为出错或者到达文件尾,同样可以用ferror和feof来确认是哪种;对于fwrite函数返回值小于nobj,则出错。
  • 这组函数是通常是不能跨文件系统使用的,因为不同文件系统上,同一结构体成员的偏移量可能是不同的,用来存储多字节整数和浮点值的二进制格式也是不同的。

8.3.4 格式化输入输出

刚学C语言的时候我们都会学两个基本的格式化输入输出函数printf和scanf,下面我们来介绍下这两个函数以及它们的变种。

1. Printf函数族:

int printf(const char *restrict format, …..);
int fprintf(FILE *fp, const char *restrict format, …..);
int dprintf(int fd, const char *restrict format, …..);
//这三个函数若成功返回输出字节数,出错返回负值

int sprintf(char *restrict buf, const char *restrict format, …..);
//若成功返回存入数组的字符数,出错返回负值

int snprintf(char *restrict buf, size_t n, const char *restrict format, …..);
//若成功返回存入数组的字符数,出错返回负值

2. Scanf函数族:

int scanf(const char *restrict format, …..);
int fscanf (FILE *fp, const char *restrict format, …..);
int sscanf (char *restrict buf, const char *restrict format, …..);
//成功返回输入的项数,出错或者达到文件尾,返回EOF

8.3.5 定位流

系统I/O函数中定义了lseek函数来定位文件位置。标准I/O函数库中也定义了三组定位流的方法。

int ftell(FILE *fp);
//返回当前文件位置,出错返回-1L

int fseek (FILE *fp, long offset, int whence);
//成功返回0,出错返回-1

void rewind((FILE *fp);

严格意义上说Linux中不区分二进制文件和 文本文件,例如r和rb这两种mode 是一样的,更严格的来说,linux中都是当二进制流处理的。ftell用来读取文件的当前位置,fseek函数用来定位到固定的位置,其中的whence和offset同lseek函数。

rewind函数则用来定位到文件的起始位置。

int ftello(FILE *fp);
//返回当前文件位置,出错返回(off_t)-1

int fseeko (FILE *fp, off_t offset, int whence);
//成功返回0,出错返回-1

ftellofseekoftell fseek,唯一的区别就是这两个函数的offset不是用long来表示,而是用offset_t 来解释。

int fgetpos(FILE *fp, fpos_t *restrict pos);
int fsetpos(FILE *fp, const fpos_t *pos);
//成功返回0,出错返回非0

最后的这组函数用来获取和设置文件当前位置,位置存放在一个定位为fpos_t类型的数据对象中。

8.3 临时文件

Linux中创建一个文件经常会遇到重名的问题,本节介绍的两组函数可以产生一个唯一的文件名,然后使用这个文件名创建文件。

char *tmpnam(char *ptr);
//返回值:指向唯一路径名的指针

tmpname函数可以获取一个系统中唯一的路径名。若ptr为空,则所产生的路径名存放在一个静态区,指向该静态区的指针作为函数值返回。如果多次以NULL参数调用这个函数,因为这个所谓的静态区是固定的,所以后面的调用会导致静态区被改写。

若ptr指针非空,返回的路径名放在ptr指向的地址中,需要注意的是ptr指定的内存区大小必须大于L_tmpnam。

Linux中也有两种方法来创建文件名唯一的文件:

#include<stdio.h>
FILE *tmpfile(void);
//返回值:成功返回文件指针,失败返回NULL

#inlcude<string.h>
int mkstemp(char *template)
//成功返回文件描述符,失败返回-1

上面的这组函数都用来创建用一个唯一的路径名创建一个文件。它们的区别如下:

  • tmpfile函数创建的文件是临时文件,关闭该文件或程序结束时会自动删除;mtkstemp函数创建的并不是临时文件;
  • tmpfile函数是用wb+的类型创建一个二进制文件返回一个FILE类型,而mkstemp是用S_IRUSE|S_IWUSR的方式打开一个普通文件,并返回一个文件描述符;
  • tmpfile函数没有参数,而mkstemp有一个数组结构的参数,用来指定文件名称中的部分字段。

注意:mkstemp函数的template必须是一个数组,其基本形式是:

 char template[] = "template-XXXXXX";

template可以由用户自行指定,后面的六位字符由系统写入template。这里可以看到最后六位字符系统会写入template执行,如果这里的template指定的不是数组,而是一个字符串常量,字符串常量我们是没有办法改写的。

下面的例程印证了这个想法:

#include<stdio.h>
#include <stdlib.h>
#include<errno.h>
#include<string.h>

void make_tempfile(char *template);

int main()
{
	char good_template[] = "/tmp/dirXXXXXX";
	char *bad_template = "/tmp/dirXXXXXX";

	make_tempfile(good_template);
	make_tempfile(bad_template);

	return 0;
}

void make_tempfile(char *template)
{
	int fd;
	fd = mkstemp(template);
	if(fd < 0)
	{
		printf("Error %s\n",strerror(errno));
		return;
	}
	close(fd);
	printf("temp name:%s\n",template);
	unlink(template);

}

从执行结果来看:使用good_template 创建的文件返回的文件名为/tmp/diezwP4Lq。我们创建完这个文件后,立即调用close函数关闭这个fd,但事实上只有等到unlink之后这个文件才会被删除。去掉代码最后的unlink函数会发现及时程序结束,这个文件还是存在。

当使用bad_template去调用这个api时,系统直接发生的crash。

下面的mkdtemp函数则用来创建一个目录:

char *mkdtemp(char *template);
//返回值:成功返回一个目录名指针,失败返回NULL

mkdtemp函数创建的目录有以下的访问权限:S_ISUSR|S_IWUSR|S_IXUSR。我们也可以调用进程的文件模式创建屏蔽字来限制这些权限。

8.4 内存流

上面介绍的各种流都是基于文件的,本节介绍的流是基于内存的。其实原理与操作方式基本相同,区别就是内存流其实并没有对应的底层文件,而是直接对应一个内存空间。换言之,内存流就是用来和内存空间来回传递数据。理解内存流的使用方法时我们可以将这块内存想象成一个文件。内存流的特征使得它更适用于字符串操作。有三个创建内存流的方式,我们逐一来进行分析:

1. fmemopen

#include<stdio.h>
FILE *fmemopen(void *restrict buf, size_t size, const char *restrict type);
//返回值:成功返回文件指针,失败返回NULL

其中buf 和size指定内存空间,对于fmemopen函数,这两个参数不能为“0”,为0则意味着由系统分配缓冲区,而我们没有办法找到其地址,也就没有办法和内存流建立读写通道。这里的buf我理解和fputs函数中的buf不是同一个概念。这里的buf其实是指内存,而fpus函数中的buf是缓冲区。

type参数同fopen函数的type参数含义基本相同,但也有微小的差别:

  • 如果以追加的方式打开,当前文件位置设为buf中第一个null字节,如果没有null字节,则设置为buf结尾的后一个字节;
  • 如果以非追加的方式打开,当前文件位置设为buf的开始位置。

最后需要注意的是,增加流缓冲区中的数量,以及调用fcolse,fflush,fseek,fseeko以及fsetpos都会在当前位置写入一个null字节。

2. open_memstream & opemwmemstream

#include<stdio.h>
FILE *open_memstream(char **bufp, size_t size);

#include<wchar.h>
FILE *open_wmemstream(wchar_t  **bufp, size_t size);
//返回值:成功返回流指针,失败返回NULL

这两个函数也可以用来创建内存流,open_memsteam创建的是面向字节的,而wmemstream创建的是面向宽字节。它们与fmemopen的区别有:

  • 不能指定自己的内存,但恶意但是bufp和size访问到内存;
  • 创建的流只能写打开;
  • 关闭流后需要用户自行释放内存空间;
  • 对流添加字节会增加内存空间的大小。

8.5 系统I/O与标准I/O的比较

说完了这两种I/O的方式,我们来做个简单的比较:

  • 系统I/O是没有缓冲的,对其操作总是会直接进行系统调用;标准I/O是带缓冲区的。这里的缓冲区是指用户层的缓冲区,其实在内核空间,这两者都是由缓冲机制的;
  • 系统I/O操作的是文件描述符,而标准I/O操作的是文件指针(FILE);
  • 系统I/O可以用来处理管道,fifo等多种文件;而标准I/O只能用来处理普通文件。

写在文末:本文作为个人对APUE的学习笔记,章节安排和内容基本参考APUE。文中有疏漏或者错误的地方,还请不吝赐教。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值