目录
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 临时文件
I/O : input and output,是一切实现的基础
IO分为标准IO(stdio)和系统调用IO(sysio)
FILE对象通常是一个结构体,包含了标准I/O库为管理该流需要的所需要的所有信息
FILE类型贯穿始终,可以理解为FILE就代表流
一个进程默认打开了三个流,分别是标准输入 stdin、标准输出 stdout 和标准错误 stderr
一个进程默认打开1024个流,可通过如下命令查看LINUX控制shell程序的资源:
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.
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
void perror(const char *s);
// 在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用“某些”函数出错时,该函数已经重新设置了errno的值
// perror函数只是将你输入的一些信息和errno所对应的错误一起输出
#include
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;
}
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.
一般来说,fopen 和 fclose 一一对应,fopen 中为 FILE 对象分配动态内存,fclose 中利用 free 释放所分配的动态内存
代码示例:
下面仅给出部分字符和字符串的输入输出流操作,详细见 man 手册
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 的意思;
宏只占用编译时间,不占用调用时间,而函数相反,因此内核的实现通常使用宏来定义函数,因为调用函数的时间通常长于调用宏;
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 对两个文件内容进行对比,如果两个文件完全相同,则什么也不输出
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.
功能:从指定流中读取批量字符
fgets 读取结束的条件,满足其一即可:
读取结束后,会往读取进 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
int fputs(const char *s, FILE *stream);
// fputs() writes the string s to stream, without its terminating null byte ('\0').
功能:将批量字符(即字符串)写入流
写入流的不包括末尾空字符 '\0'
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.
函数返回成功读取的元素的数目,如果出错或者达到 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个字节)
因此建议单字节读取
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.
函数返回成功写入的元素的数目。如果该数字与 nmemb 参数不同,则会显示一个错误。
代码示例:用 fread 和 fwrite 代替 fgtec 和 fputc:
#include
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
辅助函数:将字符串初始部分转化为整数
#include
// 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);
#include
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.
#include
int fseek(FILE *stream, long offset, int whence);
// The fseek() function sets the file position indicator for the stream pointed to by stream.
功能:设置文件位置指针指向
常量 | 描述 |
SEEK_SET | 文件的开头 |
SEEK_CUR | 文件位置指针当前所在位置 |
SEEK_END | 文件的末尾EOF,即文件中最后一个字符的下一个位置 |
如果成功,则该函数返回零,否则返回非零值
文件位置指针是什么?
long ftell(FILE *stream);
功能: 返回文件位置指针所指位置(从文件起始位置开始,并以字节为单位度量,相对起始位置的偏移)
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
#include
#include
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);
}
先看一个现象
发现 while 循环前的那行字符串并没有显示!
原因:对于标准输出,输出缓冲区刷新的时机:
因此,上述 while 循环前的那行只是进入输出缓冲区了,并没有冲洗
标准 I/O 提供缓冲的目的是为了减少使用系统调用 read 和 write 的次数,增加程序的吞吐量
#include
int fflush(FILE *stream);
功能:冲洗缓冲区
术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗(例如, 当填满一个缓冲区时),或者可以调用函数 fflush 冲洗一个流。值得注意的是,在UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上(该缓冲区可能只是部分填满的)。在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中的数据。
可行的一些修改方式:
即可得到期望的输出
标准I/O缓冲的分类(即除了手动 fflush 以外,不同类的缓冲有不同的默认冲洗(标准IO指写入磁盘)时机):
不同的标准I/O有默认的缓冲类别
关于缓冲这段的 man 手册:
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 的情况下的冲洗时机)
mode | 描述 |
_IOFBF | 全缓冲 |
_IOLBF | 行缓冲 |
_IONBF | 无缓冲 |
之前介绍的函数,都不能获得完整的一整行(有缓冲区大小的限制),而下面介绍的getline函数则可以动态分配内存,当装不下完整一行时,又会申请额外的内存来存储。
getline会生成一个包含一串从输入流读入的字符的字符串,直到以下情况发生会导致生成的此字符串结束:
#define _GNU_SOURCE // 通常将这种宏写在makefile中,现在的编译器没有了该宏,直接使用即可
#include
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
功能:用于从流中读取完整的行
要理解这些参数的含义,必须要知道 getline 的工作原理
这样再对照 getline 的声明,就知道各种参数和返回值的含义了
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
注意 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.
使用示例:
注意区分开辟空间的字节数和读取到的字符数!
临时文件产生的问题:
tmpnam:生成并返回一个有效的临时文件名,该文件名之前是不存在的。如果 str 为空,则只会返回临时文件名。
存在并发问题,可能会产生两个或多个名字相同的临时文件。
可能两个不同进程运行该函数时,检查文件名后,生成文件名前发生了进程切换
#include
char *tmpnam(char *s);
另一个函数:
tmpfile:以二进制更新模式(wb+)创建临时文件。被创建的临时文件会在流关闭的时候或者在程序终止的时候自动删除。
该文件没有名字(匿名文件),函数只返回指向FILE的指针,因此不存在命名冲突的问题,同时会自动删除,因此可以及时销毁。
#include
FILE *tmpfile(void);