文件的输入/输出操作

10.1 文件I/O操作概述

在Linux系统中,文件I/O操作可以分为两类,一类是基于文件描述符的I/O操作,另一类是基于数据流的I/O操作。

10.1.1 文件描述符简介

在文件操作一章中,也经常提到文件描述符这个概念。所谓文件描述符,就是进程与打开的文件的一个桥梁,通过这个桥梁,才可以在进程中对这个文件进行读写等操作。

在Linux环境下,每打开一个磁盘文件,都会在内核中建立一个文件表项,文件表项中存储着文件的状态信息、存储文件内容的缓冲区和当前文件的读写位置。如果同一磁盘文件打开了3次,就会创建3个这样的文件表项(a,b和c),读写文件时,只会改变该文件表项中的文件读写位置。这3个文件表项存储在一个文件表数组table[3]中,在这里table[0]=a,table[1]=b,table[2]=c。这个文件表的下标就称之为文件描述符,将这个文件描述符存储在一个数组中des[3]={0,1,2},那么,在进程中就可以通过这个des数组下标引用文件表项。也就是说,通过文件描述符就可以访问到这个磁盘文件。

10.1.2 数据流概述

从数据操作方式这个角度来说,Linux系统中的文件(无论是普通文件还是设备文件)可以看做是数据流。对文件进行操作之前,必须先调用标准I/O库函数fopen()将数据流打开。打开数据流之后,就可以对数据流进行输入和输出的操作。

标准I/O库函数是C语言中所特有的用于高级接口的函数,这些库函数存放在C语言的stdio.h头文件中,因此这些用于数据流的I/O操作函数不仅适用于Linux系统,还适用于其他的操作系统。由此可见,此库函数的引用大大增加了程序的移植性。

要对数据流进行读写操作时,需要标准I/O库函数和FILE类型的文件指针一起来实现。这个文件指针石达开数据流时返回的指针,该指针用来表示要操作的数据流。

当执行程序时,有3个数据流不需要特定的函数进行打开的操作,他们会自动打开。这3个数据流是标准输入、标准输出和标准错误输出。他们是自动打开的,当不使用时,也会自动关闭。

然而,调用标准I/O库函数fopen()打开的数据流,在对数据流进行操作后,需要调用fclose()函数将其关闭。fclose()函数在关闭数据流之前,会清空在操作过程中分配的缓冲区并保存数据信息。

10.2 基于文件描述符的I/O操作

10.2.1 文件的打开与关闭

1)open()函数

#include<sys/types.h>

#include<sys/stat.h>

#include<fcntl.h>

int open(const char *pathname, int flags)

int open(const char *pathname, int flags, mode_t mode)

int creat(const char *pathname, mode_t mode)

上述的两个open()函数和一个creat()函数在调用成功时,都会返回其新分配的文件描述符;否则返值为-1,并设置适当的error。

2)close()函数

#incldue<unistd.h>

int close(int fd)

当一个进程终止时,内核对该进程的所有尚未关闭的文件描述符调用close()函数关闭,所以即使用户程序不调用close()函数,在终止时内核也会自动关闭它打开的所有文件。但是,对于网络服务器这种一直运行的程序,文件描述符一定要及时关闭,否则随着打开的文件越来越多,会占用大量文件描述符和系统资源。

有函数open()返回的文件描述符一定是该进程尚未使用的最小描述符。由于程序启动时自动打开标准输入、标准输出和标准错误输出,因此文件描述符0,1,2会存在,那么第一次调用open()函数打开文件时返回的文件描述符通常会是3,再调用open()函数就会返回4.可以利用这一点在标准输入、标准输出或标准错误输出上打开一个新文件,是想重定向的功能。例如,首先调用close()函数关闭文件描述符1,然后调用open()函数打开一个常规文件,则一定会返回文件描述符1,这是标准输出就不再是终端,而是一个常规文件了,再调用printf()函数就不打印到屏幕上,而是写到这个文件中了。在文件操作一章中讲到的dup2()函数就是另外一种在指定的文件描述符上打开文件的方法。

10.2.2 文件的读写操作

1)read()函数

#include<unistd.h>

ssize_t read(int fd, void *buf, size_t count)

2)write()函数

#include<unistd.h>

ssize_t write(int fd, const void *buf, size_t count)

10.2.3 文件的定位

每个文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个自己就会将读写位置往后移多少个字节。

以O_APPEND方式打开文件,每次写操作都会在文件末尾追加数据,然后间读写位置移动到新的文件末尾。

1)lseek()函数

lseek()函数可以移动当前读写位置,通常称为偏移量,该函数的定义形式如下:

#include<sys/types.h>

#include<unisd.h>

off_t lseek(int fildes, off_t offset, int whence)

10.3 基于数据流的I/O操作

10.3.1 文件的打开与关闭

在操作文件之前要用fopen()函数打开文件,操作结束后,要用fclose()函数关闭文件。

1)fopen()函数

#include<stdio.h>

FILE *foepn(cosnt char *path, cosnt char *mode)

2)fclose()函数

#include<stdio.h>

int fclose(FILE *fp)

10.3.2 字符输入/输出

1)fgetc()函数

fgetc()函数从指定的文件中读一个字节,该函数的定义形式如下:

#include<stdio.h>

int fgetc(FILE *stream)

在程序中,偶尔会遇到getchar()函数,也是用于读取一个字节,但它是从标准输入读一个字节。在程序中调用getchar()函数相当于调动fgetc(stdin)

在使用fgetc()函数时需要注意一下几点:

①调用fgetc()函数时,指定的文件的打开方式必须是可读的。

②函数fgetc()调用成功时,返回的是读到的字节,应该为unsigned char,但fgetc()函数在原型中返回值类型时int,原因在于函数调用出错或读到文件末尾时fgetc()会返回EOF,即-1,保存在int型的返回值是0xffffffff,如果读到字节0xff,由unsigned char型转换int 型时0x000000ff,只有规定返回值是int型才能把这种情况区分开,如果规定返回值是unsigned char型,那么当返回值是0xff时则无法区分到底是EOF还是字节0xff。

2)fputc()函数

#include<stdio.h>

int fputc(int c, FILE *stream)

10.3.3 字符串输入/输出

1)fgets()函数

#include<stdio.h>

char *fgets(char *s, int size, FILE *stream)

对于fgets()函数而言,‘\n‘是一个特别的字符,作为结束符;而‘\0‘并无任何特别之处,只用作普通字符串读入。正因为‘\0‘作为一个普通的字符串,因此无法判断缓冲区中的‘\0‘究竟是从文件读上来的字符还是有fgets()函数自主添加的结束符,所以fgets()函数只用于读文本文件而不提倡读二进制文件,并且文本文件中的所有字符串不能有‘\0‘。

2)fputs()

#include<stdio.h>

int fputs(const char *s, FILE *stream)

缓冲区s中保存的是以‘\0‘结尾的字符串,fputs()将该字符串写入文件stream,但并不写入结尾的‘\0‘,且字符串中可以有‘\n‘,也可以没有‘\n‘。

10.3.4 数据块输入/输出

1)fread()和fwrite()函数

#include<stido.h>

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)

10.3.5 格式化输入/输出

所谓格式化输入/输出,就是按照一定的格式将数据进行输入/输出操作。在程序中经常用到的printf()函数和scanf()函数是用于对终端设备文件的读写操作,这两个函数被称为格式化输入/输出,因为在使用这两个函数时,需要制定读写数据的数据类型并按照一定的格式进行读写。

1)格式化输入函数

#include<stdio.h>

int printf(const char *format,...)

int fprintf(FILE *stream, const char *format)

int sprintf(char *str, size_t size, const char *format,...)

2)格式化输出函数

#include<stdio.h>

int scanf(const char *format,...)

int fscanf(FILE *stream, const char *format,...)

int sscanf(const char *str, const char *format,...)

10.3.6 操作读写位置的函数

1)fseek()函数

#inlcude<stdio.h>

int fseek(FILE *stream, long offset, int whence)

函数fseek()的作用是用来移动文件内部位置指针。

2)ftell()函数

#inlcude<stdio.h>

long ftell(FILE *stream)

ftell()函数的作用是得到stream指定的流式文件中的位置。

3)rewind()函数

void rewind(FILE *stream)

rewind()函数的作用是使位置指针重新返回文件的开头,该函数没有返回值。

10.3.7 C标准的I/O缓冲区

C标准库在调用fopen()函数时,都会给此文件分配一个I/O缓冲区,可以加速读写操作,原因在于用户程序需要调用C标准I/O库函数(如fread()、fwrite()等基于文件流I/O操作)读写文件,当缓冲区装满后,再由系统调用的I/O函数(如read()、write()等基于文件描述符的I/O操作)把读写请求传给内核,最终由内核驱动磁盘或设备完成I/O操作。

由此看来,为文件分配的内存缓冲区大小,直接影响到实际操作外村设备的次数,内存中为未见分配的缓冲区越大,操作外存的次数会越小,因此读写数据的速度会越来越快,效率就会随之增高。

然而,有时用户程序等不及将缓冲区都装满之后再传给内核,进行I/O操作,而是希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这种行为叫做flush操作,对应的库函数是fflush()。

C标准库的I/O缓冲区有全缓冲、行缓冲和无缓冲3种类型。

1)全缓冲:如果缓冲区写满了,就写回内核。普通文件通常是全缓冲的。

2)行缓冲:如果用户程序写的数据中有‘\n‘,就把这一行写回内核,或者缓冲区写满后就写回内核。标准输入和标准输出对应中断设备时通常是行缓冲。

3)无缓冲:用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的。这样用户程序产生的错误信息就可以尽快输出到设备。

使用缓冲区时,会使用到如下两类操作,一个是设置缓冲区属性,另外一个是清空缓冲区。

4)设置缓冲区属性

#include<stdio.h>

void setbuf(FILE *stream, char *buf)

void setbuffer(FILE *stream, char *buf, size_t size)

void setlinebuf(FILE *stream)

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

setbuf()函数主要实现了为参数buf所指定的缓冲区设置大小。此函数中,设定缓冲区大小的值只有两个,一个是常数BUFSIZ,另一个是NULL。当定义值为BUFSIZ时,代表设置缓冲区为全缓冲;若为NULL,则代表设置缓冲区为无缓冲形式。

setbuffer()与setbuf()功能相同,只是setbuffer()函数可以任意指定缓冲区大小为size

setlinebuf()函数实现了将stream指向的缓冲区设置为行缓冲。

setvbuf()函数融合了上述3种函数的功能,既可以设置缓冲区的任意大小size,也可以设置缓冲区的任意类型,如mode参数取值为_IOFBF(全缓冲类型)、_IOLBF(行缓冲类型)或_IONBF(无缓冲类型)。

5)清空缓冲区

#include<stdio.h>

int fflush(FILE *stream)

fflush()函数实现将缓冲区中的尚未写入文件的数据强制性地写进stream所指定的文件中,然后清空缓冲区。如果stream为NULL,此函数会将所有打开的文件数据更新。

10.4 小结

本章主要介绍了Linux系统下的文件I/O操作。在Linux系统下存在两种文件I/O操作,一种是基于文件描述符的I/O操作,这里面的I/O操作都是Linux系统中提供并直接作用于内核的,是非缓冲的I/O操作;另一种I/O操作是基于数据流的I/O操作,是由C语言的stdio库所提供的,需要在内存中开辟一块缓冲区,在缓冲区中进行快速地读写操作。本章主要结合典型实例介绍了上述两种I/O操作方式对文件的打开、关闭、读、写、文件定位等操作。

文件的输入/输出操作

上一篇:17款现代风格的免费英文字体,分享给设计师们


下一篇:JFreeChart DateAxis用法