基于流的I/O操作与基于文件描述符的I/O操作过程十分相似,同样是使用相关的函数调用打开文件或设备,然后对文件进行读写,最后关闭文件。流I/O是由C语言的标准函数库提供的,这些I/O可以替代系统中提供的read和write函数。

流与缓存

流和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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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;
}

编译后执行:

1
2
3
4
5
6
7
8
9
10
11
12
$ ./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函数打开一个流时,就开辟了所需的缓冲区,系统通常会赋予其一个默认的属性值。可以通过如下函数设置缓冲区的属性值:

1
2
3
4
5
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#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;
}

编译后执行:

1
2
3
4
5
6
7
8
9
10
$ ./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操作写入缓存中的内容清空,清空可以是将流的内容完全丢掉,也可以是将其保存到文件中:

1
2
3
4
#include <stdio.h>
int fflush(FILE *fp);
#include <stdio_ext.h>
void fpurge(FILE *fp);
  • fflush函数用于将缓冲区中尚未写入文件的数据强制性地保存到文件,调用成功返回0,失败返回EOF。
  • fpurge函数用于将缓冲区中的数据完全清除,该函数使用较少。

流的打开与关闭

当用户使用基于流的缓冲时会由C语言的库函数提供对缓冲的操作,用户则不用再耗费时间和精力控制缓冲区了。

流的打开

1
2
3
4
#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参数是一个非法字符串
  • 文件的操作权限不够

流的关闭

1
2
#include <stdio.h>
int fclose(FILE *fp);

运行成功返回0,出错返回EOF。

EOF是一个定义在<stdio.h>中的宏定义,其值为-1。

打开与关闭一个流测试,stream.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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空文件):

1
2
$ ./stream

程序没有输出(本来就没有),看一下hello.txt:

1
2
3
4
$ cat hello.txt
Hello! I like Linux C program!
I am doing Linux C programs!

再次运行程序后,查看hello.txt:

1
2
3
4
5
6
$ ./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个函数用于一次读取一个字符:

1
2
3
4
5
#include <stdio.h>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);

运行成功返回读入字符的值,出错返回EOF。

字符的输出

以下3个函数用于字符输出:

1
2
3
4
5
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#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;
}

编译后运行:

1
2
3
4
5
$ ./char_copy
Hello! I like Linux C program!
I am doing Linux C programs!
I am doing Linux C programs!

查看test.txt:

1
2
3
4
5
$ 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函数实现输入一行字符串,函数原型如下:

1
2
3
4
#include <stdio.h>
char *fgets(char *buf, int n, FILE *fp);
char *gets(char *buf);

运行成功返回缓冲区的首地址,出错或已到文件尾返回EOF。

gets函数与fgets函数最大的不同是gets的缓冲区虽然由用户提供,但无法指定一次最多读入多少字节的内容。因此gets函数使用起来有风险,不推荐使用

行的输出

fputs和puts函数实现输出一行字符串,函数原型如下:

1
2
3
4
#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回换行符,函数原型如下:

1
2
3
4
#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个元素写到一个文件上:

      1
      2
      3
      4
      float data[10];
      if(fwrite(&data[2], sizeof(float), 4, fp)!=4)
      printf("fwrite error!\n");

    • 读或写一个结构:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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:

1
2
3
4
5
6
7
8
9
$ ./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函数:

1
2
3
4
5
6
#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函数:

1
2
3
4
5
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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;
}

编译后运行:

1
2
3
$ ./format_io
Hello!

查看hello.txt中的内容:

1
2
3
$ cat hello.txt
Hello! Linux C

可以看到buf中的字符串已写入到hello.txt(该文件原来有内容,现在被清除了)。另外,打印的buf2的结果只是一部分,原因是scanf读取文件流时,遇到空格符、制表符和回车符时会自动终止读取字符。

参考:《精通Linux C编程》- 程国钢