标准 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:执行gcc -E test.c对test.c进行预处理, 会得到:- // MacOS操作系统上的运行结果:
- extern int * __error(void);
- (*__error());
- // Ubuntu22上的运行结果:
- (*__errno_location ());
复制代码 可以看到, errno已经被转化为宏 (而不是int类型全局变量)
再新建测试程序errno.c:- #include <stdlib.h>
- #include <stdio.h>
- #include <errno.h>
- 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类型:
类型序号含义EPERM1Operation not permittedENOENT2No such file or directoryESRCH3No such processEINTR4Interrupted system callEIO5I/O errorENXIO6No such device or addressE2BIG7Argument list too longENOEXEC8Exec format errorEBADF9Bad file numberECHILD10No child processesEAGAIN11Try againENOMEM12Out of memoryEACCES13Permission deniedEFAULT14Bad 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返回的指针在堆上, 因此需要有一逆操作释放这一堆上的空间小例子
一个进程中, 打开的文件个数的上限?- #include <stdlib.h>
- #include <stdio.h>
- #include <errno.h>
- 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 files
- count = 1021
复制代码 在不更改当前默认环境的情况下, 进程默认打开三个流: stdin, stdout, stderr
ulimit -a可以查看当前默认环境的资源限制, 其中包括默认最多打开流的个数:- $ ulimit -a
- real-time non-blocking time (microseconds, -R) unlimited
- core file size (blocks, -c) 0
- data seg size (kbytes, -d) unlimited
- scheduling priority (-e) 0
- file size (blocks, -f) unlimited
- pending signals (-i) 7303
- max locked memory (kbytes, -l) 251856
- max memory size (kbytes, -m) unlimited
- open files (-n) 1024 # 默认最多1024个流
- pipe size (512 bytes, -p) 8
- POSIX message queues (bytes, -q) 819200
- real-time priority (-r) 0
- stack size (kbytes, -s) 8192
- cpu time (seconds, -t) unlimited
- max user processes (-u) 7303
- virtual memory (kbytes, -v) unlimited
- file locks (-x) unlimited
复制代码 由于最多可以打开1024个stream, 而默认已经打开了三个, 因此程序输出的count的大小就等于1024 - 3 = 1021
文件权限
在上一案例中, 程序打开的tmp文件是由touch命令创造出来的, 其权限为0664, 为什么是0664?
公式: 权限 = 0666 & ~umask
umask的值可以通过umask命令查询, 该值主要用于防止权限过松的文件出现读/写字符操作
- // 以unsigned char转为int的形式返回读到的字符
- // 如读到文件末尾, 或发生错误, 返回EOF
- int 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 <stdlib.h>
- #include <stdio.h>
- int main(int argc, char **argv)
- {
- FILE *fps, *fpd;
- int ch;
- if (argc < 3)
- {
- fprintf(stderr, "Usage:%s <src_file> <dest_file>", 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 <stdlib.h>
- #include <stdio.h>
- int main(int argc, char **argv)
- {
- FILE *fp;
- long long cnt = 0;
- if (argc < 2)
- {
- fprintf(stderr, "Usage:%s <file_name>", 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'字符 (文件末尾处默认有换行符)
问题:
假设有一文件:问: 用fgets(buff, 5, file)语句读取该文件, 需要几次才能读完?
2次, 第一次读取到"abcd", 第二次读取到"\n"
- int fputs(const char *s, FILE *stream);
复制代码 重写 mycpy
- #include <stdlib.h>
- #include <stdio.h>
- #define BUFSIZE 1024
- int main(int argc, char **argv)
- {
- FILE *fps, *fpd;
- char buff[BUFSIZE];
- if (argc < 3)
- {
- fprintf(stderr, "Usage:%s <src_file> <dest_file>", 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指向的数据输出到stream
- size_t fwrite(const void *ptr, size_t size, size_t nemeb, FILE *stream);
复制代码问题:
要通过fread()从文件中读取字符串, 每次读取10个字符
- 假设文件中的有效字符数远大于10, 则2个语句各返回几?
- // 语句1
- fread(ptr, 1, 10, fp);
- // 语句2
- fread(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
- 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不能自行增长, 因此不能解决需要输出长字符串的需求
- 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 <stdlib.h>
- #include <stdio.h>
- 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 <stdlib.h>
- #include <stdio.h>
- int main(int argc, char **argv)
- {
- FILE *fp;
- if (argc < 2)
- {
- fprintf(stderr, "Usage:%s <file_name>", 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:注意:
- 不需要自己为缓冲区分配空间, 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 <stdlib.h>
- #include <stdio.h>
- #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 <= 0)
- {
- buffsize = DEFAULT_LINE_BUF_SIZE;
- linebuff = malloc(buffsize * sizeof(char));
- }
- if (linebuff == NULL)
- return -1;
- while ((ch = fgetc(stream)) != EOF)
- {
- if ((char)ch == '\n')
- break;
- linebuff[idx ++] = (char)ch;
- if (idx >= 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);
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |