标准 IO
注: 李慧芹老师的视频课程请点这里, 本篇为标准IO一章的笔记, 课上提到过的内容基本都会包含
I/O (Input & Output): 是一切实现的基础
stdio (标准IO)
sysio (系统调用IO / 文件IO)
系统IO是内核接口, 标准IO是C标准库提供的接口, 标准IO内部使用了系统IO
标准IO会合并系统调用, 可移植性好, 因此在两者都可以完成任务的情况下, 优先使用标准IO
stdio 的一系列函数
详细参考man(3);
FILE
类型贯穿始终, FILE类型是一个结构体
fopen()
: 产生FILE
fclose()
fgetc()
fputc()
fgets()
fputs()
fread()
fwrite()
pintf()
一族
scanf()
一族
fseek()
ftell()
rewind()
fflush()
打开操作
// 打开文件操作, 运行成功时, 返回FILE指针, 失败则返回NULL且设置errno// params:// @path: 要打开的文件// @mode: 打开的权限(如: 只读/读写/只写...)FILE *fopen(const char *path, const char *mode);
const char *
面试题:
char *ptr = "abc";ptr[0] = 'x'; // 语句2
问: 能否通过语句2得到值为
"xbc"
的字符串?
gcc
编译会报错(修改常量值), 但Turbo C
一类的编译器编译出的程序会运行通过
errno
ubuntu22系统中, 可以执行vim /usr/include/errno.h
来查看相关信息
errno曾经是一个全局变量, 但目前已被私有化, 新建test.c
:
#include errno;
执行gcc -E test.c
对test.c
进行预处理, 会得到:
// MacOS操作系统上的运行结果:extern int * __error(void);(*__error());// Ubuntu22上的运行结果:(*__errno_location ());
可以看到, errno已经被转化为宏 (而不是int类型全局变量)
再新建测试程序errno.c:
#include #include #include int main(void){ FILE *fp; fp = fopen(); if (fp == NULL) { fprintf(stderr, "fopen() failed! errno = %d\n", errno); exit(1); } puts("OK"); exit(0);}
编译并运行该程序, 输出结果:
fopen() failed! errno = 2
标准C中定义的errno类型:
类型 | 序号 | 含义 |
---|---|---|
EPERM | 1 | Operation not permitted |
ENOENT | 2 | No such file or directory |
ESRCH | 3 | No such process |
EINTR | 4 | Interrupted system call |
EIO | 5 | I/O error |
ENXIO | 6 | No such device or address |
E2BIG | 7 | Argument list too long |
ENOEXEC | 8 | Exec format error |
EBADF | 9 | Bad file number |
ECHILD | 10 | No child processes |
EAGAIN | 11 | Try again |
ENOMEM | 12 | Out of memory |
EACCES | 13 | Permission denied |
EFAULT | 14 | Bad address |
… | … | … |
根据上表中展示的errno类型, 可以得知, 2代表了文件或目录不存在
可以调用perror()
或strerror()
来将errno转化为error message
mode
mode必须以表格中的字符开头
符号 | 模式 |
---|---|
r | 以只读形式打开文件, 打开时定位到文件开始处 |
r+ | 读写形式打开文件, 打开时定位到文件开始处 |
w | 写形式打开文件, 有则清空, 无则创建 |
w+ | 读写形式打开文件,有则清空, 无则创建 |
a | 追加只写的形式打开文件, 如文件不存在, 则创建文件;打开时定位到文件末尾处 (文件最后一个有效字节的下一个位置) |
a+ | 追加读写的形式打开文件, 如文件不存在, 则创建文件; 读位置在文件开始处, 而写位置永远在文件末尾处 |
注意:
r
和r+
要求文件必须存在mode可以追加字符
b
, 如rb
/r+b
,b
表示二进制流, 在POSIX环境(包括Linux环境)下,b
可以忽略
面试题:
FILE *fp;fp = fopen("tmp", "r+write"); // 语句2
问: 语句2是否会报错?
并不会, fopen函数只会识别
r+
, 后面的字符会被忽略
FILE *
fopen
返回的FILE结构体指针指向的内存块存在在哪里?
堆上
有逆操作的, 返回指针的函数, 其返回的指针一定指向堆上某一块空间
如无逆操作, 则有可能指向堆, 也有可能指向静态区
关闭操作
由于fopen
返回的指针在堆上, 因此需要有一逆操作释放这一堆上的空间
int fclose(FILE *fp);
小例子
一个进程中, 打开的文件个数的上限?
#include #include #include int main(){ FILE *fp; int cnt = 0; while (1) { fp = fopen("tmp", "r"); if (fp == NULL) { perror("fopen()"); break; } cnt ++; } printf("count = %d\n", cnt); exit(0);}
运行结果:
fopen(): Too many open filescount = 1021
在不更改当前默认环境的情况下, 进程默认打开三个流: stdin
, stdout
, stderr
ulimit -a
可以查看当前默认环境的资源限制, 其中包括默认最多打开流的个数:
$ ulimit -areal-time non-blocking time (microseconds, -R) unlimitedcore file size (blocks, -c) 0data seg size (kbytes, -d) unlimitedscheduling priority (-e) 0file size (blocks, -f) unlimitedpending signals (-i) 7303max locked memory (kbytes, -l) 251856max memory size (kbytes, -m) unlimitedopen files (-n) 1024 # 默认最多1024个流pipe size (512 bytes, -p) 8POSIX message queues (bytes, -q) 819200real-time priority (-r) 0stack size (kbytes, -s) 8192cpu time (seconds, -t) unlimitedmax user processes (-u) 7303virtual memory (kbytes, -v) unlimitedfile locks (-x) unlimited
由于最多可以打开1024个stream
, 而默认已经打开了三个, 因此程序输出的count
的大小就等于1024 - 3 = 1021
文件权限
在上一案例中, 程序打开的tmp文件是由touch
命令创造出来的, 其权限为0664, 为什么是0664?
公式: 权限 = 0666 & ~umask
umask的值可以通过umask
命令查询, 该值主要用于防止权限过松的文件出现
$ umask0002
读/写字符操作
- 读字符
// 以unsigned char转为int的形式返回读到的字符// 如读到文件末尾, 或发生错误, 返回EOFint fgetc(FILE *stream); // 函数int getc(FILE *stream); // 宏// getchar()相当于getc(stdin)
- 写字符
int fputc(int c, FILE *stream);int putc(int c, FILE *stream);// 相当于putc(c, stdout)int putchar(int c);
mycpy
实现复制文件命令mycpy
#include #include int main(int argc, char **argv){ FILE *fps, *fpd; int ch; if (argc < 3) { fprintf(stderr, "Usage:%s ", argv[0]); exit(1); } fps = fopen(argv[1], "r"); if (fps == NULL) { perror("fopen()"); exit(1); } fpd = fopen(argv[2], "w"); if (fpd == NULL) { perror("fopen()"); fclose(fps); exit(1); } while (1) { ch = fgetc(fps); if (ch == EOF) break; fputc(ch, fpd); } // 先关闭依赖别人的流, 再关闭被依赖的文件 fclose(fpd); fclose(fps);}
编译后执行以下命令:
$ ./mycpy /etc/services ./out$ diff /etc/services ./out
如果diff
命令什么也没有输出, 则说明mycpy
命令已正确执行
fsize
查看文件有效字符的个数
#include #include int main(int argc, char **argv){ FILE *fp; long long cnt = 0; if (argc < 2) { fprintf(stderr, "Usage:%s ", argv[0]); exit(1); } fp = fopen(argv[1], "r"); if (fp == NULL) { perror("fopen()"); exit(1); } while (fgetc(fp) != EOF) cnt ++; printf("%lld\n", cnt); fclose(fp); exit(0);}
读写字符串
- 读字符串:
// params:// @s: 缓冲区// @size: 缓冲区大小char *fgets(char *s, int size, FILE *stream);
fgets
有两种正常结束:
读到size-1个字节 (缓冲区内剩余一个字节需要存放’\0′)
读到了’\n’字符 (文件末尾处默认有换行符)
问题:
假设有一文件:
abcd
问: 用
fgets(buff, 5, file)
语句读取该文件, 需要几次才能读完?2次, 第一次读取到”abcd”, 第二次读取到”\n”
- 写字符串:
int fputs(const char *s, FILE *stream);
重写 mycpy
#include #include #define BUFSIZE 1024int main(int argc, char **argv){ FILE *fps, *fpd; char buff[BUFSIZE]; if (argc < 3) { fprintf(stderr, "Usage:%s ", argv[0]); exit(1); } fps = fopen(argv[1], "r"); if (fps == NULL) { perror("fopen()"); exit(1); } fpd = fopen(argv[2], "w"); if (fpd == NULL) { perror("fopen()"); fclose(fps); exit(1); } // === 1 === // 利用读写字符串函数来完成文件复制 while (fgets(buff, BUFSIZE, fps) != NULL) { fputs(buff, fpd); } // === === // 先关闭依赖别人的流, 再关闭被依赖的文件 fclose(fpd); fclose(fps);}
重写后的代码改为利用读写字符串函数来完成复制文件的操作(见1处)
fread & fwrite
fread
& fwrite
用于二进制流的输入和输出
// 从stream流中读取nmemb个数据// 每个数据的大小为size// 读取到的所有数据保存到ptr指向的内存空间size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);// 将ptr指向的数据输出到streamsize_t fwrite(const void *ptr, size_t size, size_t nemeb, FILE *stream);
问题:
要通过
fread()
从文件中读取字符串, 每次读取10个字符
- 假设文件中的有效字符数远大于10, 则2个语句各返回几?
// 语句1fread(ptr, 1, 10, fp);// 语句2fread(ptr, 10, 1, fp);
语句1返回10(读到了10个大小为1的对象)
语句2返回1(读到了1个大小为10的对象)
- 假设文件中的有效字符数不足10个(比如5个), 则2个语句各返回几?
语句1返回5, 而语句2返回0
另外, 语句二返回0后, 它究竟读了多少个字符, 也无从得知; 因此, 如果要通过
fread()
从文件中读取字符串, 则一定要使用语句1的方法!
注意:
用
fread()
或fwrite()
操作文件, 最好还是只做存取单一大小的数据(例如: 单一类型的结构体数据)的操作; 尽管如此, 这样的操作依然是有风险的, 因为一旦文件中由于各种原因含有了其他的数据, 那么fread()
就会彻底失灵
printf & scanf
- printf一族
int printf(const char *format, ...);int fprintf(FILE *stream, const char *stream, ...);// 将format与参数综合的结果, 输入到str中int sprintf(char *str, const char *format, ...);// 与sprintf类似, 只是多了对str大小的规定(size)以防止写越界int snprintf(char *str, size_t size, const char *format, ...);
注意:
尽管printf一族提供了大量的输出函数, 但是这些函数还是不能完全解决问题
sprintf
和snprintf
中, str不能自行增长, 因此不能解决需要输出长字符串的需求
- scanf一族
int scanf(const char *format, ...);int fscanf(FILE *stream, const char *format, ...);int sscanf(const char *str, const char *format, ...);
注意:
使用scanf一族时, 是不清楚要输入进来的数据有多长的
因此要注意, 输入文本的长度是否大于缓冲区的大小
年-月-日
以year-month-day
的格式打印日期:
#include #include int main(){ char buf[1024]; int year = 2014, month = 5, day = 13; sprintf(buf, "%d-%d-%d", year, month, day); puts(buf); exit(0);}
文件位置
// 将文件指针定位到文件的某一位置(whence+offset)// whence有三个选项: SEEK_SET(文件开始位置), SEEK_CUR(当前位置), SEEK_END(文件末尾位置)// offset的单位为字节int fseek(FILE *stream, long offset, int whence);long fseek(FILE *stream, long offset, int whence);// 获得当前文件指针指向的文件位置long ftell(FILE *stream);// 相当于: (void) fseek(stream, 0L, SEEK_SET)void rewind(FILE *stream);
文件位置指针
文件位置指针指向对文件进行操作的位置, 如:
fp = fopen(...);for (i = 0; i < 10; i ++){ fputc(fp);}
在上述代码结束后, fp的文件位置指针指向文件第11个字节的位置
为了能将文件位置指针移动到文件内的任意位置上, 有了fseek
等函数
flen
将原先的fsize.c复制为flen.c, 并重写为:
#include #include int main(int argc, char **argv){ FILE *fp; if (argc < 2) { fprintf(stderr, "Usage:%s ", argv[0]); exit(1); } fp = fopen(argv[1], "r"); if (fp == NULL) { perror("fopen()"); exit(1); } fseek(fp, 0, SEEK_END); printf("%ld\n", ftell(fp)); fclose(fp); exit(0);}
fseeko & ftello
由于
ftell
的返回值的类型为long
, 而又不可能为负数, 因此其值域(在64位机器上)为$[0, 2^32 – 1]$, 因此利用ftell
和fseek
一同工作时, 只能定位2G大小的文件
由于ftell
有值域限制, 因此有了fseeko
和ftello
:
int fseeko(FILE *stream, off_t offset, int whence);off_t ftello(FILE *stream);
在一些机器上, off_t
是32位的, 在定义宏_FILE_OFFSET_BITS
值为64后, 可以保证off_t
为64位
可以在Makefile中定义CFLAG, 使编译器在编译阶段得知_FILE_OFFSET_BITS
被定义为64:
CFLAGS+=-D_FILE_OFFSET_BITS=64
注意:
fseeko
和ftello
是POSIX环境的方言, C89和C99标准对其没有定义
刷新缓冲区
printf("Before while()"); // 第1行while(1);printf("After while()");
上述代码什么也不会打印, 这是由于标准输出是行缓冲的, 而”Before while()”并非一行内容
可以将第1行代码修改为printf("Before while()\n");
或者在第1行代码后增加fflush(stdout);
来刷新标准输出的缓冲区
缓冲区的作用: 大多数情况下是好事, 合并系统调用
行缓冲:
换行的时候刷新, 缓冲区满了的时候刷新, 强制刷新
全缓冲:
缓冲区满了的时候刷新, 强制刷新(默认, 只要不是终端设备)
无缓冲:
如stderr, 需要立即输出的内容
可以利用
setvbuf
修改缓冲模式
读取完整一行
实现了完整读取一行内容的函数:
// 从stream中读取一行内容, 存到lineptr指向的缓冲区ssize_t getline(char **lineptr, size_t *n, FILE *stream);
使用时, 需要定义宏#define _GNU_SOURCE
可以在Makefile中, 添加CFLAG:
CFLAGS+=-D_GNU_SOURCE
注意:
不需要自己为缓冲区分配空间, lineptr可为一个值为NULL的指针变量的地址
getline
只能在GNU C环境中使用
mygetline
自行实现一个功能为从流中读取一行内容, 且在标准C环境中可以使用的工具:
#ifndef _MY_GETLINE_H__#define _MY_GETLINE_H__#define DEFAULT_LINE_BUF_SIZE 120/* * 从stream中读取一行内容, 并将读取到的内容保存在*lineptr指向的缓冲区中 * n指向缓冲区大小 * * 返回值: * * 返回读取到的文本长度, 如果发生错误或读到文件末尾, 返回-1 * */ long long mygetline(char **lineptr, size_t *n, FILE *stream);/* * 释放缓冲区占用的内存空间 * */void mygetline_free(char **lineptr);#endif
#include #include #include "mygetline.h"long long mygetline(char **lineptr, size_t *n, FILE *stream){ size_t buffsize = *n; char *linebuff = *lineptr; long long idx = 0LL; int ch; if (*lineptr == NULL || *n = buffsize - 1) { buffsize += (buffsize >> 1); linebuff = realloc(linebuff, buffsize); } } linebuff[idx] = '\0'; *lineptr = linebuff; *n = buffsize; return ch != EOF ? idx : -1;}void mygetline_free(char **lineptr){ if (*lineptr != NULL) free(*lineptr);*lineptr = NULL;}
临时文件
如何不冲突地创建临时文件
及时销毁
可用函数: tmpnam
/tmpfile
// 获得一个可用的临时文件名// 注意: 使用该函数时, 需要:// 1.首先拿到临时文件名// 2.用该名称创建临时文件// 由于这两步不是原子操作, 可能与其他进程产生冲突// 因此该要谨慎使用该函数char *tmpnam(char *s);// 以二进制读写(w+b)模式打开一个临时文件FILE *tmpfile(void);