最全的李慧芹APUE-标准IO笔记

打印 上一主题 下一主题

主题 935|帖子 935|积分 2805

标准 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()
打开操作
  1. // 打开文件操作, 运行成功时, 返回FILE指针, 失败则返回NULL且设置errno
  2. // params:
  3. // @path: 要打开的文件
  4. // @mode: 打开的权限(如: 只读/读写/只写...)
  5. FILE *fopen(const char *path, const char *mode);
复制代码
const char *

面试题:
  1. char *ptr = "abc";
  2. ptr[0] = 'x'; // 语句2
复制代码
问: 能否通过语句2得到值为"xbc"的字符串?
gcc编译会报错(修改常量值), 但Turbo C一类的编译器编译出的程序会运行通过
errno

ubuntu22系统中, 可以执行vim /usr/include/errno.h来查看相关信息
errno曾经是一个全局变量, 但目前已被私有化, 新建test.c:
  1. #include <errno.h>
  2. errno;
复制代码
执行gcc -E test.c对test.c进行预处理, 会得到:
  1. // MacOS操作系统上的运行结果:
  2. extern int * __error(void);
  3. (*__error());
  4. // Ubuntu22上的运行结果:
  5. (*__errno_location ());
复制代码
可以看到, errno已经被转化为宏 (而不是int类型全局变量)
再新建测试程序errno.c:
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <errno.h>
  4. int main(void)
  5. {
  6.     FILE *fp;
  7.     fp = fopen();
  8.     if (fp == NULL)
  9.     {   
  10.         fprintf(stderr, "fopen() failed! errno = %d\n", errno);
  11.         exit(1);
  12.     }   
  13.     puts("OK");
  14.     exit(0);
  15. }
复制代码
编译并运行该程序, 输出结果:
  1. 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可以忽略
面试题:
  1. FILE *fp;
  2. fp = fopen("tmp", "r+write"); // 语句2
复制代码
问: 语句2是否会报错?
并不会, fopen函数只会识别r+, 后面的字符会被忽略
FILE *

fopen返回的FILE结构体指针指向的内存块存在在哪里?
堆上
有逆操作的, 返回指针的函数, 其返回的指针一定指向上某一块空间
如无逆操作, 则有可能指向堆, 也有可能指向静态区
关闭操作

由于fopen返回的指针在堆上, 因此需要有一逆操作释放这一堆上的空间
  1. int fclose(FILE *fp);
复制代码
小例子

一个进程中, 打开的文件个数的上限?
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <errno.h>
  4. int main()
  5. {
  6.         FILE *fp;
  7.         int cnt = 0;
  8.         while (1)
  9.         {
  10.                 fp = fopen("tmp", "r");
  11.                 if (fp == NULL)
  12.                 {
  13.                         perror("fopen()");
  14.                         break;
  15.                 }
  16.                 cnt ++;
  17.         }
  18.         printf("count = %d\n", cnt);
  19.         exit(0);
  20. }
复制代码
运行结果:
  1. fopen(): Too many open files
  2. count = 1021
复制代码
在不更改当前默认环境的情况下, 进程默认打开三个流: stdin, stdout, stderr
ulimit -a可以查看当前默认环境的资源限制, 其中包括默认最多打开流的个数:
  1. $ ulimit -a
  2. real-time non-blocking time  (microseconds, -R) unlimited
  3. core file size              (blocks, -c) 0
  4. data seg size               (kbytes, -d) unlimited
  5. scheduling priority                 (-e) 0
  6. file size                   (blocks, -f) unlimited
  7. pending signals                     (-i) 7303
  8. max locked memory           (kbytes, -l) 251856
  9. max memory size             (kbytes, -m) unlimited
  10. open files                          (-n) 1024 # 默认最多1024个流
  11. pipe size                (512 bytes, -p) 8
  12. POSIX message queues         (bytes, -q) 819200
  13. real-time priority                  (-r) 0
  14. stack size                  (kbytes, -s) 8192
  15. cpu time                   (seconds, -t) unlimited
  16. max user processes                  (-u) 7303
  17. virtual memory              (kbytes, -v) unlimited
  18. file locks                          (-x) unlimited
复制代码
由于最多可以打开1024个stream, 而默认已经打开了三个, 因此程序输出的count的大小就等于1024 - 3 = 1021
文件权限

在上一案例中, 程序打开的tmp文件是由touch命令创造出来的, 其权限为0664, 为什么是0664?
公式: 权限 = 0666 & ~umask
umask的值可以通过umask命令查询, 该值主要用于防止权限过松的文件出现
  1. $ umask
  2. 0002
复制代码
读/写字符操作


  • 读字符
  1. // 以unsigned char转为int的形式返回读到的字符
  2. // 如读到文件末尾, 或发生错误, 返回EOF
  3. int fgetc(FILE *stream); // 函数
  4. int getc(FILE *stream);  // 宏
  5. // getchar()相当于getc(stdin)
复制代码

  • 写字符
  1. int fputc(int c, FILE *stream);
  2. int putc(int c, FILE *stream);
  3. // 相当于putc(c, stdout)
  4. int putchar(int c);
复制代码
mycpy

实现复制文件命令mycpy
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. int main(int argc, char **argv)
  4. {
  5.         FILE *fps, *fpd;
  6.         int ch;
  7.         if (argc < 3)
  8.         {
  9.                 fprintf(stderr, "Usage:%s <src_file> <dest_file>", argv[0]);
  10.                 exit(1);
  11.         }
  12.         fps = fopen(argv[1], "r");
  13.         if (fps == NULL)
  14.         {
  15.                 perror("fopen()");
  16.                 exit(1);
  17.         }
  18.         fpd = fopen(argv[2], "w");
  19.         if (fpd == NULL)
  20.         {
  21.                 perror("fopen()");
  22.                 fclose(fps);
  23.                 exit(1);
  24.         }
  25.         while (1)
  26.         {
  27.                 ch = fgetc(fps);
  28.                 if (ch == EOF)
  29.                         break;
  30.                 fputc(ch, fpd);
  31.         }
  32.         // 先关闭依赖别人的流, 再关闭被依赖的文件
  33.         fclose(fpd);
  34.         fclose(fps);
  35. }
复制代码
编译后执行以下命令:
  1. $ ./mycpy /etc/services ./out
  2. $ diff /etc/services ./out
复制代码
如果diff命令什么也没有输出, 则说明mycpy命令已正确执行
fsize

查看文件有效字符的个数
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. int main(int argc, char **argv)
  4. {
  5.         FILE *fp;
  6.         long long cnt = 0;
  7.         if (argc < 2)
  8.         {
  9.                 fprintf(stderr, "Usage:%s <file_name>", argv[0]);
  10.                 exit(1);
  11.         }
  12.         fp = fopen(argv[1], "r");
  13.         if (fp == NULL)
  14.         {
  15.                 perror("fopen()");
  16.                 exit(1);
  17.         }
  18.         while (fgetc(fp) != EOF)
  19.                 cnt ++;
  20.         printf("%lld\n", cnt);
  21.         fclose(fp);
  22.         exit(0);
  23. }
复制代码
读写字符串


  • 读字符串:
  1. // params:
  2. // @s: 缓冲区
  3. // @size: 缓冲区大小
  4. char *fgets(char *s, int size, FILE *stream);
复制代码
fgets有两种正常结束:

  • 读到size-1个字节 (缓冲区内剩余一个字节需要存放'\0')
  • 读到了'\n'字符 (文件末尾处默认有换行符)
问题:
假设有一文件:
  1. abcd
复制代码
问: 用fgets(buff, 5, file)语句读取该文件, 需要几次才能读完?
2次, 第一次读取到"abcd", 第二次读取到"\n"


  • 写字符串:
  1. int fputs(const char *s, FILE *stream);
复制代码
重写 mycpy
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #define BUFSIZE 1024
  4. int main(int argc, char **argv)
  5. {
  6.         FILE *fps, *fpd;
  7.         char buff[BUFSIZE];
  8.         if (argc < 3)
  9.         {
  10.                 fprintf(stderr, "Usage:%s <src_file> <dest_file>", argv[0]);
  11.                 exit(1);
  12.         }
  13.         fps = fopen(argv[1], "r");
  14.         if (fps == NULL)
  15.         {
  16.                 perror("fopen()");
  17.                 exit(1);
  18.         }
  19.         fpd = fopen(argv[2], "w");
  20.         if (fpd == NULL)
  21.         {
  22.                 perror("fopen()");
  23.                 fclose(fps);
  24.                 exit(1);
  25.         }
  26.         // === 1 ===
  27.         // 利用读写字符串函数来完成文件复制
  28.         while (fgets(buff, BUFSIZE, fps) != NULL)
  29.         {
  30.                 fputs(buff, fpd);
  31.         }
  32.         // ===   ===
  33.         // 先关闭依赖别人的流, 再关闭被依赖的文件
  34.         fclose(fpd);
  35.         fclose(fps);
  36. }
复制代码
重写后的代码改为利用读写字符串函数来完成复制文件的操作(见1处)
fread & fwrite

fread & fwrite用于二进制流的输入和输出
  1. // 从stream流中读取nmemb个数据
  2. // 每个数据的大小为size
  3. // 读取到的所有数据保存到ptr指向的内存空间
  4. size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  5. // 将ptr指向的数据输出到stream
  6. size_t fwrite(const void *ptr, size_t size, size_t nemeb, FILE *stream);
复制代码
问题:
要通过fread()从文件中读取字符串, 每次读取10个字符

  • 假设文件中的有效字符数远大于10, 则2个语句各返回几?
  1. // 语句1
  2. fread(ptr, 1, 10, fp);
  3. // 语句2
  4. 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


  • printf一族
  1. int printf(const char *format, ...);
  2. int fprintf(FILE *stream, const char *stream, ...);
  3. // 将format与参数综合的结果, 输入到str中
  4. int sprintf(char *str, const char *format, ...);
  5. // 与sprintf类似, 只是多了对str大小的规定(size)以防止写越界
  6. int snprintf(char *str, size_t size, const char *format, ...);
复制代码
注意:
尽管printf一族提供了大量的输出函数, 但是这些函数还是不能完全解决问题
sprintf和snprintf中, str不能自行增长, 因此不能解决需要输出长字符串的需求


  • scanf一族
  1. int scanf(const char *format, ...);
  2. int fscanf(FILE *stream, const char *format, ...);
  3. int sscanf(const char *str, const char *format, ...);
复制代码
注意:
使用scanf一族时, 是不清楚要输入进来的数据有多长的
因此要注意, 输入文本的长度是否大于缓冲区的大小
年-月-日

以year-month-day的格式打印日期:
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. int main()
  4. {
  5.     char buf[1024];
  6.     int year = 2014, month = 5, day = 13;
  7.     sprintf(buf, "%d-%d-%d", year, month, day);
  8.     puts(buf);
  9.     exit(0);
  10. }
复制代码
文件位置
  1. // 将文件指针定位到文件的某一位置(whence+offset)
  2. // whence有三个选项: SEEK_SET(文件开始位置), SEEK_CUR(当前位置), SEEK_END(文件末尾位置)
  3. // offset的单位为字节int fseek(FILE *stream, long offset, int whence);
  4. long fseek(FILE *stream, long offset, int whence);
  5. // 获得当前文件指针指向的文件位置
  6. long ftell(FILE *stream);
  7. // 相当于: (void) fseek(stream, 0L, SEEK_SET)
  8. void rewind(FILE *stream);
复制代码
文件位置指针

文件位置指针指向对文件进行操作的位置, 如:
  1. fp = fopen(...);
  2. for (i = 0; i < 10; i ++)
  3. {
  4.     fputc(fp);
  5. }
复制代码
在上述代码结束后, fp的文件位置指针指向文件第11个字节的位置
为了能将文件位置指针移动到文件内的任意位置上, 有了fseek等函数
flen

将原先的fsize.c复制为flen.c, 并重写为:
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. int main(int argc, char **argv)
  4. {
  5.         FILE *fp;
  6.         if (argc < 2)
  7.         {
  8.                 fprintf(stderr, "Usage:%s <file_name>", argv[0]);
  9.                 exit(1);
  10.         }
  11.         fp = fopen(argv[1], "r");
  12.         if (fp == NULL)
  13.         {
  14.                 perror("fopen()");
  15.                 exit(1);
  16.         }
  17.         fseek(fp, 0, SEEK_END);
  18.         printf("%ld\n", ftell(fp));
  19.         fclose(fp);
  20.         exit(0);
  21. }
复制代码
fseeko & ftello

由于ftell的返回值的类型为long, 而又不可能为负数, 因此其值域(在64位机器上)为$[0, 2^32 - 1]$, 因此利用ftell和fseek一同工作时, 只能定位2G大小的文件
由于ftell有值域限制, 因此有了fseeko和ftello:
  1. int fseeko(FILE *stream, off_t offset, int whence);
  2. off_t ftello(FILE *stream);
复制代码
在一些机器上, off_t是32位的, 在定义宏_FILE_OFFSET_BITS值为64后, 可以保证off_t为64位
可以在Makefile中定义CFLAG, 使编译器在编译阶段得知_FILE_OFFSET_BITS被定义为64:
  1. CFLAGS+=-D_FILE_OFFSET_BITS=64
复制代码
注意:
fseeko和ftello是POSIX环境的方言, C89和C99标准对其没有定义
刷新缓冲区
  1. printf("Before while()"); // 第1行
  2. while(1);
  3. printf("After while()");
复制代码
上述代码什么也不会打印, 这是由于标准输出是行缓冲的, 而"Before while()"并非一行内容
可以将第1行代码修改为printf("Before while()\n");或者在第1行代码后增加fflush(stdout);来刷新标准输出的缓冲区
缓冲区的作用: 大多数情况下是好事, 合并系统调用
行缓冲:
换行的时候刷新, 缓冲区满了的时候刷新, 强制刷新
全缓冲:
缓冲区满了的时候刷新, 强制刷新(默认, 只要不是终端设备)
无缓冲:
如stderr, 需要立即输出的内容
可以利用setvbuf修改缓冲模式
读取完整一行

实现了完整读取一行内容的函数:
  1. // 从stream中读取一行内容, 存到lineptr指向的缓冲区
  2. ssize_t getline(char **lineptr, size_t *n, FILE *stream);
复制代码
使用时, 需要定义宏#define _GNU_SOURCE
可以在Makefile中, 添加CFLAG:
  1. CFLAGS+=-D_GNU_SOURCE
复制代码
注意:

  • 不需要自己为缓冲区分配空间, lineptr可为一个值为NULL的指针变量的地址
  • getline只能在GNU C环境中使用
mygetline

自行实现一个功能为从流中读取一行内容, 且在标准C环境中可以使用的工具:
  1. #ifndef _MY_GETLINE_H__
  2. #define _MY_GETLINE_H__
  3. #define DEFAULT_LINE_BUF_SIZE 120
  4. /*
  5. * 从stream中读取一行内容, 并将读取到的内容保存在*lineptr指向的缓冲区中
  6. * n指向缓冲区大小
  7. *      
  8. * 返回值:
  9. *      
  10. * 返回读取到的文本长度, 如果发生错误或读到文件末尾, 返回-1
  11. * */   
  12. long long mygetline(char **lineptr, size_t *n, FILE *stream);
  13. /*
  14. * 释放缓冲区占用的内存空间
  15. * */
  16. void mygetline_free(char *lineptr);
  17. #endif
复制代码
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include "mygetline.h"
  4. long long mygetline(char **lineptr, size_t *n, FILE *stream)
  5. {
  6.         size_t buffsize = *n;
  7.         char *linebuff = *lineptr;
  8.         long long idx = 0LL;
  9.         int ch;
  10.         if (*lineptr == NULL || *n <= 0)
  11.         {      
  12.                 buffsize = DEFAULT_LINE_BUF_SIZE;
  13.                 linebuff = malloc(buffsize * sizeof(char));
  14.         }
  15.         if (linebuff == NULL)
  16.                 return -1;
  17.         while ((ch = fgetc(stream)) != EOF)
  18.         {      
  19.                 if ((char)ch == '\n')
  20.                         break;
  21.                 linebuff[idx ++] = (char)ch;
  22.                 if (idx >= buffsize - 1)
  23.                 {
  24.                         buffsize += (buffsize >> 1);
  25.                         linebuff = realloc(linebuff, buffsize);
  26.                 }
  27.         }
  28.         linebuff[idx] = '\0';
  29.         *lineptr = linebuff;
  30.         *n = buffsize;
  31.         return ch != EOF ? idx : -1;
  32. }
  33. void mygetline_free(char *lineptr)
  34. {
  35.         if (lineptr != NULL)
  36.                 free(lineptr);
  37.         lineptr = NULL;
  38. }
复制代码
临时文件


  • 如何不冲突地创建临时文件
  • 及时销毁
可用函数: tmpnam/tmpfile
  1. // 获得一个可用的临时文件名
  2. // 注意: 使用该函数时, 需要:
  3. // 1.首先拿到临时文件名
  4. // 2.用该名称创建临时文件
  5. // 由于这两步不是原子操作, 可能与其他进程产生冲突
  6. // 因此该要谨慎使用该函数
  7. char *tmpnam(char *s);
  8. // 以二进制读写(w+b)模式打开一个临时文件
  9. FILE *tmpfile(void);
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

傲渊山岳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表