Linux基于流的IO操作
流和FILE对象
基于文件的I/O函数都是针对文件描述符的,当打开一个文件时,返回一个文件的描述符,然后通过该文件描述符进行后续的I/O操作。
而对于标准的I/O库,它的操作是围绕流(stream)进行的,当用标准I/O库打开或创建一个文件时,已使一个流与一个文件相结合。当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针,该对象通常是一个结构体,它包含了I/O库为管理该流所需要的所有信息。
缓存
基于流的操作最终会调用read或write函数进行I/O操作,为了提高程序的运行效率,尽可能减少使用read和write调用的数量,流对象通常会提供缓冲区,以减少调用系统I/O库函数的次数。标准I/O提供了3种类型的缓存:
-
全缓存:直到缓冲区被填满,才调用系统I/O函数。
-
行缓存:直到遇到换行符“\n”,才调用系统I/O函数库。
-
无缓存:没有缓冲区,数据会立即读入或输出到外存文件和设备上。
缓冲区类型 | 定义的宏 |
---|---|
全缓存 | _IO_FULL_BUF |
行缓存 | _IO_LINE_BUF |
无缓冲 | _IO_UNBUFFERED |
检测缓冲区类型和大小,buf_test.c:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("stdin is "); // 判断标准输入流对象的缓冲区类型-------------
if(stdin->_flags&_IO_UNBUFFERED)
printf("unbuffered\n"); // 无缓存
else if(stdin->_flags&_IO_LINE_BUF)
printf("line-buffered\n"); // 行缓存
else
printf("fully-buffered\n"); // 全缓存
// 打印缓冲区的大小
printf("buffer size is %ld\n", stdin->_IO_buf_end-stdin->_IO_buf_base);
// 标准输入流的文件描述符
printf("file discriptor is %d\n\n", fileno(stdin));
printf("stdout is "); // 判断标准输出流对象的缓冲区类型-------------
if(stdout->_flags&_IO_UNBUFFERED)
printf("unbuffered\n"); // 无缓存
else if(stdout->_flags&_IO_LINE_BUF)
printf("line-buffered\n"); // 行缓存
else
printf("fully-buffered\n"); // 全缓存
// 打印缓冲区的大小
printf("buffer size is %ld\n", stdout->_IO_buf_end-stdout->_IO_buf_base);
// 标准输出流的文件描述符
printf("file discriptor is %d\n\n", fileno(stdout));
printf("stderr is "); // 判断标准出错流对象的缓冲区类型-------------
if(stderr->_flags&_IO_UNBUFFERED)
printf("unbuffered\n"); // 无缓存
else if(stderr->_flags&_IO_LINE_BUF)
printf("line-buffered\n"); // 行缓存
else
printf("fully-buffered\n"); // 全缓存
// 打印缓冲区的大小
printf("buffer size is %ld\n", stderr->_IO_buf_end-stderr->_IO_buf_base);
// 标准出错流的文件描述符
printf("file discriptor is %d\n\n", fileno(stderr));
return 0;
}
编译后执行:
$ ./buf_test
stdin is fully-buffered
buffer size is 0
file discriptor is 0
stdout is line-buffered
buffer size is 1024
file discriptor is 1
stderr is unbuffered
buffer size is 0
file discriptor is 2
对缓存的操作
在进行基于流的I/O操作时,缓存的使用将是不可或缺的。
设置缓存的属性
缓存的属性包括缓冲区的类型和大小,当调用fopen函数打开一个流时,就开辟了所需的缓冲区,系统通常会赋予其一个默认的属性值。可以通过如下函数设置缓冲区的属性值:
#include <stdio.h>
void setbuf(FILE *fp, char *buf);
void setbuffer(FILE *fp, char *buf, size_t size);
void setlinebuf(FILE *fp);
int setvbuf(FILE *fp, char *buf, int mode, size_t size);
-
setbuf用于将缓冲区设置为全缓存或无缓冲。参数buf为指向缓冲区的指针,当buf指向一个真实的缓冲区地址时,为全缓存;当buf为NULL时,为无缓存。
-
setbuffer的功能与setbuf类似,区别是由程序员自行指定缓冲区的大小size。
-
setlinebuf专用于将缓冲区设定为行缓存。
-
setvbuf函数比较灵活,可以方便地设置缓存的属性。
设置缓冲区属性,buf_set.c:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 1024 // 缓冲区的大小
int main(void)
{
char buf[SIZE]; // 缓冲区
if(setvbuf(stdin, buf, _IONBF, SIZE)!=0) // 将标准输入的缓冲类型设为无缓冲
{
printf("error!\n");
exit(1);
}
printf("OK, set successful!\n");
printf("stdin is"); // 判断标准输入流对象的缓冲区类型
if(stdin->_flags&_IO_UNBUFFERED)
printf("unbuffered\n");
else if(stdin->_flags&_IO_LINE_BUF)
printf("line-buffered\n");
else
printf("fully-buffered\n");
// 打印缓冲区大小
printf("buffer size is %ld\n", stdin->_IO_buf_end-stdin->_IO_buf_base);
// 输出文件描述符
printf("file discriptor is %d\n\n", fileno(stdin));
// 将标准输入的缓冲区类型设置为全缓冲,缓存大小为1024
if(setvbuf(stdin, buf, _IOFBF, SIZE)!=0)
{
printf("error!\n");
exit(1);
}
printf("OK, change successful!\n");
printf("stdin is"); // 再次判断标准输入流对象的缓冲区类型
if(stdin->_flags&_IO_UNBUFFERED)
printf("unbuffered\n");
else if(stdin->_flags&_IO_LINE_BUF)
printf("line-buffered\n");
else
printf("fully-buffered\n");
// 打印缓冲区大小
printf("buffer size is %ld\n", stdin->_IO_buf_end-stdin->_IO_buf_base);
// 输出文件描述符
printf("file discriptor is %d\n\n", fileno(stdin));
return 0;
}
编译后执行:
$ ./buf_set
OK, set successful!
stdin isunbuffered
buffer size is 1
file discriptor is 0
OK, change successful!
stdin isfully-buffered
buffer size is 1024
file discriptor is 0
可以看出,stdin的缓冲区类型发生了变化。
缓存的冲洗
缓存冲洗是指将I/O操作写入缓存中的内容清空,清空可以是将流的内容完全丢掉,也可以是将其保存到文件中:
#include <stdio.h>
int fflush(FILE *fp);
#include <stdio_ext.h>
void fpurge(FILE *fp);
-
fflush函数用于将缓冲区中尚未写入文件的数据强制性地保存到文件,调用成功返回0,失败返回EOF。
-
fpurge函数用于将缓冲区中的数据完全清除,该函数使用较少。
流的打开与关闭
当用户使用基于流的缓冲时会由C语言的库函数提供对缓冲的操作,用户则不用再耗费时间和精力控制缓冲区了。
流的打开
#include <stdio.h>
FILE *fopen(const char *pathname, const char *type);
FILE *freopen(const char *pathname, const char *type, FILE *fp);
FILE *fdopen(int fd, const char *type);
运行成功返回文件指针,出错返回NULL(空指针)。
-
fopen打开一个路径名为pathname的文件。
-
freopen在一个特定的流上(fp)打开一个指定的文件(pathname),若该流已经打开,则先关闭该流。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入、标准输出或标准出错。
-
fdopen取一个现存的文件描述符(可能从open、dup、dup2、fcntl、或pipe函数得到),并使一个标准的I/O流与该描述符相结合。次函数常用于由创建管道和网络通信函数获得的描述符,因为这些特殊类型的文件不能使用标准I/O的fopen函数打开,首先必须先调用设备专用函数以获得一个文件描述符,然后调用fdopen使一个标准I/O流与该描述符相结合。
type值 | 操作文件类型 | 是否新建文件 | 是否清空文件 | 可读 | 可写 | 读写开始位置 |
---|---|---|---|---|---|---|
r | 文本文件 | × | × | √ | × | 文件开头 |
r+ | 文本文件 | √ | × | √ | √ | 文件开头 |
w | 文本文件 | √ | √ | × | √ | 文件开头 |
w+ | 文本文件 | √ | √ | √ | √ | 文件开头 |
a | 文本文件 | × | √ | × | √ | 文件结尾 |
a+ | 文本文件 | × | √ | √ | √ | 文件结尾 |
rb | 二进制文件 | × | × | √ | × | 文件开头 |
r+b或rb+ | 二进制文件 | √ | × | √ | √ | 文件开头 |
wb | 二进制文件 | √ | √ | × | √ | 文件开头 |
w+b或wb+ | 二进制文件 | √ | √ | √ | √ | 文件开头 |
ab | 二进制文件 | × | √ | × | √ | 文件结尾 |
a+b或ab+ | 二进制文件 | × | √ | √ | √ | 文件结尾 |
fopen出错的一般原因:
-
指定的文件路径有误
-
type参数是一个非法字符串
-
文件的操作权限不够
流的关闭
#include <stdio.h>
int fclose(FILE *fp);
运行成功返回0,出错返回EOF。
EOF是一个定义在<stdio.h>中的宏定义,其值为-1。
打开与关闭一个流测试,stream.c:
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int fd;
// 以读写方式打开流,从文件开头开始读写
if((fp = fopen("hello.txt", "r+")) == NULL)
{
printf("fail to open!\n");
exit(1);
}
// 向该流输出一段信息
fprintf(fp, "Hello! I like Linux C program!\n");
fclose(fp); // 关闭流
// 以读写方式打开文件,基于文件描述符的方式
if((fd = open("hello.txt", O_RDWR)) == -1)
{
printf("fail to open!\n");
exit(1);
}
// 在打开的文件上打开一个流,并从文件尾开始读写
if((fp = fdopen(fd, "a+")) == NULL)
{
printf("fail to open stream!\n");
exit(1);
}
// 向该流输出一段信息
fprintf(fp, "I am doing Linux C programs!\n");
fclose(fp); // 关闭流
return 0;
}
编译后运行(运行前先在当前目录新建一个hello.txt空文件):
$ ./stream
程序没有输出(本来就没有),看一下hello.txt:
$ cat hello.txt
Hello! I like Linux C program!
I am doing Linux C programs!
再次运行程序后,查看hello.txt:
$ ./stream
$ cat hello.txt
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!
可以看到输出追加了一条,原因在于文件头的被覆盖,文件尾的被追加。
流的读写
一旦打开了流,则可在4种不同类型的I/O中进行选择,来对其进行读写操作:
-
基于字符的I/O:每次读写一个字符数据
-
基于行的I/O:当输入内容遇到换行符
\n
时,将之前的内容送到缓冲区 -
直接I/O:输入输出操作以记录为单位进行读写
-
格式化I/O:格式化输入输出为最常见的方式,如printf或scanf
基于字符的I/O
字符的输入
以下3个函数用于一次读取一个字符:
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
运行成功返回读入字符的值,出错返回EOF。
字符的输出
以下3个函数用于字符输出:
#include <stdio.h>
int putc(int c, FILE *fp);
int fputc(int c, FILE *fp);
int putchar(int c);
运行成功返回读入字符的值,出错饭返回EOF。
基于字符I/O方式的文字复制,char_copy.c:
#include <stdio.h>
#include <stdlib.h>
#define Src_File "./hello.txt"
#define Des_File "./test.txt"
int main(void)
{
FILE *fp1, *fp2; // 源文件和目标文件的文件指针
int c; // 要进行输入和输出的字符
// 以只读方式打开源文件hello.txt
if((fp1=fopen(Src_File, "rb")) == NULL)
{
printf("fail to open source file\n");
exit(1);
}
// 以只写方式打开目标文件test.txt
if((fp2=fopen(Des_File, "wb")) == NULL)
{
printf("fail to open source file\n");
exit(1);
}
// 开始复制文件,每次读写一个字符。直到文件内容全部读完
while((c=fgetc(fp1))!=EOF)
{
if(fputc(c, fp2) == EOF) // 将读入的内容写入目标文件
{
printf("fail to write\n");
exit(1);
}
if(fputc(c, stdout) == EOF) // 将读入的内容输出到屏幕
{
printf("fail to write\n");
exit(1);
}
}
fclose(fp1);
fclose(fp2);
return 0;
}
编译后运行:
$ ./char_copy
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!
查看test.txt:
$ cat test.txt
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!
可以看到,从hello.txt复制的内容已输出到屏幕和test.txt。
基于行的I/O
行的输入
fgets和gets函数实现输入一行字符串,函数原型如下:
#include <stdio.h>
char *fgets(char *buf, int n, FILE *fp);
char *gets(char *buf);
运行成功返回缓冲区的首地址,出错或已到文件尾返回EOF。
gets函数与fgets函数最大的不同是gets的缓冲区虽然由用户提供,但无法指定一次最多读入多少字节的内容。因此gets函数使用起来有风险,不推荐使用。
行的输出
fputs和puts函数实现输出一行字符串,函数原型如下:
#include <stdio.h>
char fputs(const char *buf, FILE *restrict fp);
char puts(const char *str);
-
fputs函数的第1个参数为输出缓冲区,第2个参数为要输出的文件。成功返回输出字节数,失败返回-1。
-
puts函数用于向标准输出输出一行字符串。
直接I/O
直接I/O,也称执行二进制I/O操作,可以直接读取NULL回换行符,函数原型如下:
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp);
size_t fwrite(const void *ptr, size_t size, size_t nmember, FILE *fp);
函数返回读或写的对象数。
-
fread用于执行直接输出操作
-
fwrite用于执行直接输入操作,此函数有两个常见的用法:
-
读写一个二进制组,如将一个浮点型数组的第2~5个元素写到一个文件上:
float data[10]; if(fwrite(&data[2], sizeof(float), 4, fp)!=4) printf("fwrite error!\n");
-
读或写一个结构:
struct { short count; long total; char name[NAME_SIZE]; }item; if(fwrite(&item, sizeof(item), 1, fp)!=1) printf("fwrite error!\n");
-
基于直接I/O方式的文件复制,direct_copy.c:
#include <stdio.h>
#include <stdlib.h>
#define Src_File "./hello.txt"
#define Des_File "./test.txt"
int main(void)
{
FILE *fp1, *fp2;
char buf[1024];
int n;
// 以只读方式打开源文件,读起始位置为文件头
if((fp1=fopen(Src_File, "rb"))==NULL)
{
printf("fail to open source file\n");
exit(1);
}
// 以只写方式打目标文件,写起始位置为文件尾
if((fp2=fopen(Des_File, "ab"))==NULL)
{
printf("fail to open des file\n");
exit(1);
}
// 开始复制文件,文件可能较大,缓冲一次装不下,需要使用一个循环进行读写
// 读源文件,直到将文件内容全部读完
while((n=fread(buf, sizeof(char), 1024, fp1))>0)
{
// 将读出的内容全部写到目标文件中去
if(fwrite(buf, sizeof(char), n, fp2)==-1)
{
printf("fail to write\n");
exit(1);
}
}
// 如果因为读入字节小于0而跳出循环,则说明出错了
if(n==-1)
{
printf("fail to read\n");
exit(1);
}
fclose(fp1);
fclose(fp2);
return 0;
}
该程序是将hello.txt中的内容复制追加到test.txt,编译后执行,并查看test.txt:
$ ./direct_copy
$ cat test.txt
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!
可以看到内容被复制到了test.txt。
格式化I/O
格式化输出
4个printf函数:
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *fp, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
-
printf用于向标准输出流中输出数据
-
fprintf用于向指定的流中输出数据,参数fp指向要进行输出的流
-
sprintf用于向指定的流中输出一个字符串
-
snprintf的作用与sprintf相似,不同的是snprintf可以处理缓冲区
格式化输入
3个scanf函数:
#include <stdio.h>
int scanf(const char *format, ...);
int fscanf(FILE *fp, const char *format, ...);
int sscanf(char *str, const char *format, ...);
-
scanf用于从标准输入流中输入数据
-
fscanf用于从指定的流中输入数据,参数fp指向该的流
-
sscanf用于从指定的字符串中输入数据,参数str指向该字符串
使用fprintf和fscanf函数实现输入和输出,format_io.c:
#include <stdio.h>
#define File_path "./hello.txt"
int main(void)
{
FILE *fp;
char buf[] = "Hello! Linux C";
char buf2[80];
// 以只写方式打开文件
fp = fopen(File_path, "w");
fprintf(fp, "%s", buf);// 向该文件流输出字符串数据buf
fprintf(fp, "\n");
fclose(fp);
// 再以只读方式打开文件
fp = fopen(File_path, "r");
fscanf(fp, "%s", &buf2);// 将该文件流中的数据读入到buf2
fclose(fp);
printf("%s\n", buf2);// 打印buf2
return 0;
}
编译后运行:
$ ./format_io
Hello!
查看hello.txt中的内容:
$ cat hello.txt
Hello! Linux C
可以看到buf中的字符串已写入到hello.txt(该文件原来有内容,现在被清除了)。另外,打印的buf2的结果只是一部分,原因是scanf读取文件流时,遇到空格符、制表符和回车符时会自动终止读取字符。
- 点赞
- 收藏
- 关注作者
评论(0)