【APUE】标准I/O库

目录

1、简介

2、FILE对象

3、打开和关闭文件

3.1 fopen

3.2 fclose

4、输入输出流

4.1 fgetc

4.2 fputc

4.3 fgets

4.4 fputs

4.5 fread

4.6 fwrite

4.7 printf 族函数

4.8 scanf 族函数

5、文件指针操作

5.1 fseek

5.2 ftell

5.3 rewind 

6、缓冲相关

6.1 fflush

6.2 setvbuf  

7、补充

7.1 getline

7.2 临时文件 


1、简介

I/O : input and output,是一切实现的基础

IO分为标准IO(stdio)和系统调用IO(sysio)

  • 系统调用IO根据操作系统的实现方式而定,不同OS有着不同的系统调用IO
  • 标准IO对不同OS下的系统调用IO进行了封装,从而提供了一套不同OS下相同IO实现库函数
系统调用IO与标准IO示意图

2、FILE对象

FILE对象通常是一个结构体,包含了标准I/O库为管理该流需要的所需要的所有信息

FILE类型贯穿始终,可以理解为FILE就代表流

一个进程默认打开了三个流,分别是标准输入 stdin、标准输出 stdout 和标准错误 stderr

一个进程默认打开1024个流,可通过如下命令查看LINUX控制shell程序的资源:

3、打开和关闭文件

3.1 fopen

FILE *fopen(const char *pathname, const char *mode);
// The fopen() function opens the file whose name is the string pointed to by pathname and associates a stream with it.
  • pathname — 字符串,表示要打开的文件名称
  • mode — 字符串,表示文件的访问模式,该指针指向以下面字符开头的字符串:
mode描述(man手册的直接翻译)
"r"为读取而打开文本文件。流定位到文件开头
"r+"为读写而打开。流定位到文件开头
"w"将文件截断至0长,或为写入而创建文本文件。流定位到文件开头
"w+"为读写而打开。文件不存在则创建,否则截断。流定位到文件开头
"a"为追加(在文件尾写)而打开。文件不存在则创建。流定位到文件末尾
"a+"为读和追加而打开。文件不存在则创建。读取文件的初始位置是文件的开头,但输出总是追加到文件的结尾

只有模式 "r" 和 "r+" 要求文件必须存在,其他模式都可以创建文件;

mode也可以包含字母 b,放在最后或者中间,表示二进制流。例如 "rb"、"r+b";

打开成功返回一个 FILE 指针,否则返回 NULL 并设置全局变量 errno 来标识错误。该全局变量在头文件 errno.h 中声明:(只展示部分)

#define EPERM 1 /* Operation not permitted */
#define ENOENT 2 /* No such file or directory */
#define ESRCH 3 /* No such process */
#define EINTR 4 /* Interrupted system call */
#define EIO 5 /* I/O error */
#define ENXIO 6 /* No such device or address */
#define E2BIG 7 /* Argument list too long */

为了通过全局变量 errno 的值得到对应的错误提示信息,可以利用C标准中定义的如下两个函数

#include <stdio.h>
void perror(const char *s);
// 在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用“某些”函数出错时,该函数已经重新设置了errno的值
// perror函数只是将你输入的一些信息和errno所对应的错误一起输出
#include <string.h>
char *strerror(int errnuum);
// 搜索错误号errnum,并返回一个指向错误消息字符串的指针

代码示例:


fopen函数解析:

由函数原型可知,fopen函数返回的是一个FILE类型的指针,FILE是一个结构体,由typedef进行了重命名,而指针实际上是指向结构体的指针

关键问题:指针指向的哪个区?也就是说FILE结构体放在内存的哪一块?是堆,是栈,还是静态区?

换句话说,在fopen函数内部,FILE对象是如何创建的?

如果创建在栈区,当程序退出这个块时,释放刚才为变量tmp分配的栈内存,因此,会返回一个被释放的内存地址,错误

FILE *fopen(const char *pathname, const char *mode)
{
    FILE tmp;

    // 给结构体成员赋值初始化
    tmp.xxx = xxx;
    tmp.yyy = yyy;
    ...

    return &tmp;
}

如果创建在静态区,假如多次调用 fopen 函数,也只能存在一个FILE实例(因为只有这一个内存区供指针指向),最后一次的FILE结构体内容会把前一次的结果覆盖掉,错误

FILE *fopen(const char *pathname, const char *mode)
{
    static FILE tmp;
    
    // 给结构体成员赋值初始化
    tmp.xxx = xxx;
    tmp.yyy = yyy;
    ...
    
    return &tmp;
}

创建在堆区,这是正确的,此时 tmp 具有动态存储期,从调用 malloc 分配内存到调用 free 释放内存为止,而 free 就在 fclose 函数中被调用

FILE *fopen(const char *pathname, const char *mode)
{
    FILE * tmp = malloc(sizeof(FILE));
    
    // 给结构体成员赋值初始化
    tmp->xxx = xxx;
    tmp->yyy = yyy;
    ...

    return tmp;
}

3.2 fclose

int fclose(FILE *stream);
// The fclose() function flushes the stream pointed to by stream (writing any buffered output data using fflush(3)) and closes the underlying file descriptor.
  • stream — 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流
  • 如果流成功关闭,则该方法返回零。如果失败,则返回 EOF

一般来说,fopen 和 fclose 一一对应,fopen 中为 FILE 对象分配动态内存,fclose 中利用 free 释放所分配的动态内存

代码示例:

4、输入输出流

下面仅给出部分字符和字符串的输入输出流操作,详细见 man 手册

  • man 3 fgetc
  • man 3 fputc
  • man 3 fread
  • man 3 fwrite

4.1 fgetc

int fgetc(FILE *stream);
// fgetc() reads the next character from stream and returns it as an unsigned char cast to an int, 
// or EOF on end of file or error.

功能:从指定流中获取下一个字符 

还有几个类似功能的:

int getc(FILE *stream);

int getchar(void);

getchar 等同于 getc(stdin);

getc 和 fgetc 使用方式完全相同,fgetc 通过函数实现,而 getc 通过宏定义实现;

fgetc 中的 f 代表的是 function 的意思,而不是 file 的意思;

宏只占用编译时间,不占用调用时间,而函数相反,因此内核的实现通常使用宏来定义函数,因为调用函数的时间通常长于调用宏;

4.2 fputc

int fputc(int c, FILE *stream);
// fputc() writes the character c, cast to an unsigned char, to stream.

功能:将指定字符写入指定流 

还有几个类似功能的:

int putc(int c, FILE *stream);

int putchar(int c);

putchar 等同于 putc(c, stdout);

putc 和 fputc 使用方式完全相同,fputc 通过函数实现,而 putc 通过宏定义实现;

fputc 中的 f 代表的是 function 的意思,而不是 file 的意思;

宏只占用编译时间,不占用调用时间,而函数相反,因此内核的实现通常使用宏来定义函数,因为调用函数的时间通常长于调用宏;

代码示例:实现一个拷贝文件的功能

将文件 src 拷贝为 dest 

./mycpy src dest

实现代码如下:

使用方法:

diff 对两个文件内容进行对比,如果两个文件完全相同,则什么也不输出

4.3 fgets

char *fgets(char *s, int size, FILE *stream);
// fgets() reads in at most one less than size characters from stream and stores them into the buffer pointed to by s.
// Reading stops after an EOF or a newline. 
// If a newline is read, it is stored into the buffer. 
// A terminating null byte ('\0') is stored after the last character in the buffer.

功能:从指定流中读取批量字符

  • s:指向一个 buffer 缓冲,用于储存从指定流中读取到的字符
  • size:用于限制每次读取的字符个数最多 size - 1 个
  • stream:指定流

fgets 读取结束的条件,满足其一即可:

  • 读到 size-1 个字符时停止
  • 读到换行符 '\n' 时停止,换行符会被存进缓冲
  • 读到文件末尾 EOF

读取结束后,会往读取进 buffer 的最后一个字符后,再添加一个 '\0' ;

如果成功读取到字符,返回 s;

如果发生错误或者什么字符也没读取到,返回 NULL;

任何一个非空文件,末尾都有一个换行符 '\n'

#define SIZE 5
char buf[SIZE]; // 栈上的动态内存
fgets(buf, SIZE, stream);

如果stream = "abcde"
则buf = "abcd\0"(读到size-1),文件指针指向e

如果stream = "ab"
则buf = "ab\n\0"(读到换行符),文件指针指向EOF

极端的情况:
如果stream = "abcd"
则需要fgets读取两次才能读完
第一次读取的为"abcd\0"(读到SIZE-1),指针指向'\n'
第二次读取的为"\n\0"(读到换行符),指针指向EOF

4.4 fputs

int fputs(const char *s, FILE *stream);
// fputs() writes the string s to stream, without its terminating null byte ('\0').

功能:将批量字符(即字符串)写入流

写入流的不包括末尾空字符 '\0'

4.5 fread

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
// The  function fread() reads nmemb items of data, 
// each size bytes long, 
// from the stream pointed to by stream, 
// storing them at the location given by ptr.
  • stream — 这是指向 FILE 对象的指针,表示从该文件对象读取;
  • nmemb — 待读取元素的个数;
  • size — 读取的每个元素的大小,以字节为单位;
  • ptr — 将读取到的元素放进 ptr 所指的位置;

函数返回成功读取的元素的数目,如果出错或者达到 EOF,则返回值可能少于 nmemb

示例:

fread(buf, size, nmemb, fp);

// 情况1:数据量足够
// 情况2:文件只有5个字节

// 读10个对象,每个对象1个字节
fread(buf, 1, 10, fp);

// 情况1:
// 第一次读:返回10(读到10个对象),读到10个字节
// 情况2:
// 第一次读:返回5(读到5个对象),读到5个字节

//--------------------------------

// 读1个对象,每个对象10个字节
fread(buf, 10, 1, fp);

// 情况1:
// 第一次读:返回1(读到1个对象),也读到10个字节
// 情况2:
// 第一次读:返回0(读不到1个对象,因为1个对象要10字节,而文件只有5个字节)

 因此建议单字节读取

4.6 fwrite

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
// The function fwrite() writes nmemb items of data, 
// each size bytes long, 
// to the stream pointed to by stream, 
// obtaining them from the location given by ptr.
  • ptr — 从 ptr 所指的内存空间获取待写入元素;
  • nmemb — 写入元素的个数;
  • size — 写入的每个元素的大小,以字节为单位;
  • stream —  这是指向 FILE 对象的指针,表示将元素写入到 stream 输出流;

函数返回成功写入的元素的数目。如果该数字与 nmemb 参数不同,则会显示一个错误。

代码示例:用 fread 和 fwrite 代替 fgtec 和 fputc:

4.7 printf 族函数

#include <stdio.h>

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);    // write at most size bytes (including the terminating null byte ('\0')) to str
  • printf:发送格式化输出到标准输出流 stdout;
  • fprintf:发送格式化输出到流 stream 中。可以实现格式化输出的重定向;
  • sprintf:发送格式化输出到 str 所指内存。它能够将多种数据类型(整型、字符型)的数据综合为字符串类型;
  • snprintf:发送格式化输出到 str 所指内存。它能够将多种数据类型(整型、字符型)的数据综合为字符串类型,最多发送 size 个字符(包括末尾的 '\0')

辅助函数:将字符串初始部分转化为整数

#include <stdlib.h>

// convert a string to an integer

int atoi(const char *nptr);    // The atoi() function converts the initial portion of the string pointed to by nptr to int.
long atol(const char *nptr);
long long atoll(const char *nptr);

4.8 scanf 族函数

#include <stdio.h>

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

功能:按照格式说明符读取并解析输入对应位置的信息并存储于可变参数列表中对应的指针所指位置 

三者区别: 

The scanf() function reads input from the standard input stream stdin, fscanf() reads input  from the  stream pointer stream, and sscanf() reads its input from the character string pointed to by str. 

5、文件指针操作

5.1 fseek

#include <stdio.h>

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

// The  fseek() function sets the file position indicator for the stream pointed to by stream.

功能:设置文件位置指针指向

  • stream — 这是指向 FILE 对象的指针,该 FILE 对象标识了流
  • offset — 这是相对 whence 的偏移量,以字节为单位
  • whence — 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量描述
SEEK_SET文件的开头
SEEK_CUR文件位置指针当前所在位置
SEEK_END文件的末尾EOF,即文件中最后一个字符的下一个位置

如果成功,则该函数返回零,否则返回非零值

文件位置指针是什么?

  • 文件位置指针用来指示文件中的某个位置,因此在程序中进行读写操作时,位置指针也会随着文件的读写操作而改变
  • 在进行读写操作时,位置指针会自动更新到下一个读写的位置。 例如,当进行读操作时,位置指针会自动更新到下一个可读的位置;当进行写操作时,位置指针会自动更新到下一个可写的位置

5.2 ftell

long ftell(FILE *stream);

功能: 返回文件位置指针所指位置(从文件起始位置开始,并以字节为单位度量,相对起始位置的偏移)

5.3 rewind 

void rewind(FILE *stream);

功能:设置文件位置指针的位置为给定流 stream 的文件的开头 

使用时功能等同于:

(void) fseek(stream, 0L, SEEK_SET)

fseek 和 ftell 函数功能详解:

fseek 和 ftell 中偏移offset的修饰类型是 long,因此只能对2G左右大小的文件进行操作,否则会超出long的范围

fseeko 和 ftello 则将偏移的修饰类型使用typedef定义为offset_t,具体类型交由系统决定,因此不存在文件大小的限制。但是这两个函数不是C标准库函数,而是隶属于POSIX标准(POSIX是标准C库的超集,或者说,C库是普通话,而POSIX是方言)

代码示例:求文件的有效字节数

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

int main(int argc, char **argv){

    FILE *fp;
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    fp = fopen(argv[1], "r");
    if(fp == NULL) {
        perror("fopen()");
        exit(1);
    }
	// 将指针定位在文件末尾
    fseek(fp, 0, SEEK_END);

    printf("%ld\n", ftell(fp));

    exit(0);
}

6、缓冲相关

先看一个现象

 

发现 while 循环前的那行字符串并没有显示! 

原因:对于标准输出,输出缓冲区刷新的时机:

  • 输出缓冲区满
  • 或者遇到换行符\n
  • 强制刷新,或者进程结束

因此,上述 while 循环前的那行只是进入输出缓冲区了,并没有冲洗

标准 I/O 提供缓冲的目的是为了减少使用系统调用 read 和 write 的次数,增加程序的吞吐量

6.1 fflush

#include <stdio.h>

int fflush(FILE *stream);

功能:冲洗缓冲区

  • 如果参数为 stream 为 NULL,则冲洗所有的已打开的流
  • 如果成功,该函数返回零值。如果发生错误,则返回 EOF,且设置错误标识符(即 feof)

术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如, 当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。

可行的一些修改方式:

  • 标准输出遇到换行符 '\n' 自动冲洗

 

  • 通过 fflush 手动冲洗 stdout

即可得到期望的输出


标准I/O缓冲的分类(即除了手动 fflush 以外,不同类的缓冲有不同的默认冲洗(标准IO指写入磁盘)时机):

  • 全缓冲(块缓冲):在全缓冲的情况下,在填满标准I/O缓冲区后,才进行冲洗
  • 行缓冲:行缓冲指的是当遇到换行符时,或者缓冲区已经满了(一般1024字节),执行冲洗
  • 无缓冲:不会填缓冲区,可以理解为立即冲洗

不同的标准I/O有默认的缓冲类别 

  • 磁盘上的文件默认是全缓冲的
  • 标准输入和标准输入默认是行缓冲的
  • 一般指向终端设备的流默认是行缓冲,而指向文件时,则默认是全缓冲
  • 为了立即显示错误信息,标准错误默认是无缓冲的

关于缓冲这段的 man 手册:

6.2 setvbuf  

int setvbuf(FILE *stream, char *buf, int mode, size_t size);
// The setvbuf() function may be used on any open stream to change its buffer

功能: 用于改变流的缓冲类别(即改变流在不调用 fflush 的情况下的冲洗时机)

  • stream — 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流
  • buf — 这是用户指定的用于存缓冲内容的位置。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲空间
  • size — the buf argument should point to a buffer at least size bytes long
  • mode — 这指定了文件缓冲的类别:
mode描述
_IOFBF全缓冲
_IOLBF行缓冲
_IONBF无缓冲

7、补充

7.1 getline

之前介绍的函数,都不能获得完整的一整行(有缓冲区大小的限制),而下面介绍的getline函数则可以动态分配内存,当装不下完整一行时,又会申请额外的内存来存储

getline会生成一个包含一串从输入流读入的字符的字符串,直到以下情况发生会导致生成的此字符串结束:

  • 到文件结束
  • 遇到函数的定界符
  • 输入达到最大限度
#define _GNU_SOURCE // 通常将这种宏写在makefile中,现在的编译器没有了该宏,直接使用即可
#include <stdio.h>
ssize_t getline(char **lineptr, size_t *n, FILE *stream);

功能:用于从流中读取完整的行 

要理解这些参数的含义,必须要知道 getline 的工作原理

这样再对照 getline 的声明,就知道各种参数和返回值的含义了

ssize_t getline(char **lineptr, size_t *n, FILE *stream);
  • lineptr:用于存放开辟空间的首地址,*lineptr 指向所开辟空间
  • n:用于存放开辟空间的字节数,*n 为开辟空间的字节数
  • 返回值为读取到的字符数,读取失败返回 -1 

注意 man 手册中的一句特殊的使用要求:

If *lineptr is set to NULL and *n is set 0 before the call, then getline() will allocate a buffer for storing the line.  This buffer should be freed by the user program even if getline() failed.

使用示例:

注意区分开辟空间的字节数和读取到的字符数! 

7.2 临时文件 

临时文件产生的问题:

  • 如何命名不冲突
  • 如何保证及时销毁

tmpnam:生成并返回一个有效的临时文件名,该文件名之前是不存在的。如果 str 为空,则只会返回临时文件名。

存在并发问题,可能会产生两个或多个名字相同的临时文件

可能两个不同进程运行该函数时,检查文件名后,生成文件名前发生了进程切换

#include <stdio.h>
char *tmpnam(char *s);
  • s — 这是一个指向字符数组的指针,其中,临时文件名将被存储为 C 字符串
  • 返回一个指向 C 字符串的指针,该字符串存储了临时文件名。如果 s 是一个空指针,则该指针指向一个内部缓冲区,缓冲区在下一次调用函数时被覆盖
  • 如果 s 不是一个空指针,则返回 s。如果函数未能成功创建可用的文件名,则返回一个空指针

另一个函数:

tmpfile:以二进制更新模式(wb+)创建临时文件。被创建的临时文件会在流关闭的时候或者在程序终止的时候自动删除。

该文件没有名字(匿名文件),函数只返回指向FILE的指针,因此不存在命名冲突的问题,同时会自动删除,因此可以及时销毁。

#include <stdio.h>
FILE *tmpfile(void);
  • 如果成功,该函数返回一个指向被创建的临时文件的流指针。如果文件未被创建,则返回 NULL

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林沐华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值