流的读写

简介: 流的读写

按字符的形式实现输入/输出

字符输入/输出函数一次只能读写一个字符


fputc()

fputc() 函数用于向指定的流中写入一个字符,之所以不能描述为向文件中写入一个字符,完全是因为函数在对文件所对应的流指针操作时,首先会操作缓存区,最终再写入文件。但函数最终的运行结果已然是将字符写入文件。

#include <stdio.h>
int fputc(int c, FILE *stream);点击复制复制失败已复制


参数 c 用于表示写入的字符,函数原型定义参数 cint 型,而非 char 型,这是因为函数内部对该参数做了强制类型转换stream 则是文件相关联的流指针


使用示例如下

#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(int argc, const char *argv[]) {
  FILE *fp;
  if ((fp = fopen("test.txt", "w")) == NULL) {
    perror("fopen error");
    return -1;
  }
  if (fputc('a', fp) == EOF) {
    perror("fputc error");
  }
  fclose(fp);
  return 0;
}点击复制复制失败已复制


编译运行,将会向文件 test.txt 中写入字符 a ,可使用 $ od -c 查看文件中的数据,如下所示:

$ gcc main.c && ./a.out && od -c test.txt
0000000   a
0000001点击复制复制失败已复制


fgetc()

fputc() 相对应, fgetc() 用于从指定的流中读取一个字符。

#include <stdio.h>
int fgetc(FILE *stream);点击复制复制失败已复制


参数 stream 为已经打开的文件所对应的流指针。函数返回值为读取的字符,函数原型定义返回值的类型为 int 型,而非 char 型,同样也是因为函数内部做了强制类型转换。具体使用如下所示:

#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(int argc, const char *argv[]) {
  FILE *fp;
  int ch;
  if ((fp = fopen("test.txt", "r")) == NULL) {
    perror("fopen error");
    return -1;
  }
  if ((ch = fgetc(fp)) != EOF) {
    printf("ch = %c\n", ch);
  }
  fclose(fp);
  return 0;
}点击复制复制失败已复制


编译运行:

$ gcc main.c && ./a.out
ch = a点击复制复制失败已复制


按字符串的形式输入/输出

字符串输入/输出函数一次操作一个字符串


fputs()

#include <stdio.h>
int fputs(const char *s, FILE *stream);点击复制复制失败已复制


fputs() 函数用于向指定的流中写入字符串,不包含字符串的结束符 '\0' 。之所以不能描述为向文件写入,原因与读写字符一样,需要先操作缓存区


参数 s 指向需要写入的字符串, stream 为指定的流,具体使用如下所示:

#include <stdio.h>
#define N 32
#define errlog(errmsg)                                                         \
  perror(errmsg);                                                              \
  printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__);                \
  return -1;
int main(int argc, const char *argv[]) {
  FILE *fp;
  char buf[N] = "hello world";
  if ((fp = fopen("test.txt", "w")) == NULL) {
    errlog("fopen error");
  }
  if (fputs(buf, fp) == EOF) {
    errlog("fputs error");
  }
  fclose(fp);
  return 0;
}点击复制复制失败已复制


编译运行,得到如下结果:

$ gcc main.c && ./a.out && od -c test.txt
0000000   h   e   l   l   o       w   o   r   l   d
0000013点击复制复制失败已复制


fgets()

fgets() 函数用来从指定的流中读取字符串,其操作要比 fputs() 函数复杂一些。在 Linux 官方手册中,该函数的使用定义为:从指定的流中最多读取 size-1 个字符的字符串,并在读取的字符串末尾自动添加一个结束符'\0' (表示字符串结束)。

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);点击复制复制失败已复制


参数 s 用于存储读取的字符串, size 为期望读取的字符,程序可以自动设置, stream 为指定的流指针。具体使用如下所示:

#include <stdio.h>
#define N 32
#define errlog(errmsg)                                                         \
  perror(errmsg);                                                              \
  printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__);                \
  return -1;
int main(int argc, const char *argv[]) {
  FILE *fp;
  char buf[N] = "hello world";
  if ((fp = fopen("test.txt", "r")) == NULL) {
    errlog("fopen error");
  }
  if (fgets(buf, N, fp) != NULL) {
    printf("bug:%s\n", buf);
  }
  fclose(fp);
  return 0;
}点击复制复制失败已复制


编译运行:

$ gcc main.c && ./a.out
bug:hello world点击复制复制失败已复制


此时,运行结果并没有产生太多意外。程序设置的参数 size 的值为 32 (期望读取的字符个数为 32 ),远比文件中字符的个数多,因此将文件中的数据全部读取。这是,重新换一种做法,将参数 size 的值进行修改,不妨将其设置为 11 ,注意此时文件中的数据为 hello world ,加上单词中间的空格符,一共是 11 个字符。


重新运行改动后的示例,结果如下:

$ gcc main.c && ./a.out
bug:hello worl点击复制复制失败已复制


由运行结果可以看出,读取的字符串少了一个字符 'd' 。此时,运行结果正如 Linux 官方手册中描述的一致,只能读取 size-1 个字符。正是因为函数本身在读取字符串时,需要在文件的末尾自动添加一个结束符 '\0' ,表示字符串读取到此结束。因此读取了 size-1 个字符,最后一位被结束符替换。


fgets() 函数本身在读取字符串时,会自动加上结束符 '\0' ,占用一个字符位置,表示字符串读取结束。


问题到此依然没有结束,假设此时文件中的数据改变,使用编辑器写入两行数据,如下所示:

hello world
hello world
点击复制复制失败已复制


注意每一行后面都会有换行符,使用 $ od -c 命令查看,如下所示:

$ od -c test.txt
0000000   h   e   l   l   o       w   o   r   l   d  \n   h   e   l   l
0000020   o       w   o   r   l   d  \n
0000030点击复制复制失败已复制


由上述查询可知,此时文件中的数据为 24 个字符,注意 '\n' 换行符。这时如果选择将参数 size 设置为 32 ,那么是否会将文件中的数据全部读取?答案是不会,原因就是换行符发挥了作用,运行结果如下所示:

$ gcc main.c && ./a.out
bug:hello world
点击复制复制失败已复制


很明显,输出的是一行数据,空行说明读到了第一行的换行符。说明 fgets() 函数在读取到换行符 '\n' 时会自动结束,并在末尾补充一个结束符 '\0' ,表示读取结束,无论 size 有多大。


通过代码示例,可以进行简短的示例总结:

文件中的数据 参数 size 的大小 读取的结果
abcdef 6 abcde'\0'
abcdef >6 abcdef'\0'
abcdef'\n' 6 abcde'\0'
abcdef'\n' 7 abcdef'\0'
abcdef'\n' >7 abcdef'\n''\0'


示例

求一个文件的行数:

#include <stdio.h>
#include <string.h>
#define N 32
#define errlog(errmsg)                                                         \
  perror(errmsg);                                                              \
  printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__);                \
  return -1;
int get_line(FILE *fp) {
  char buf[N] = "";
  int count = 0;
  while (fgets(buf, N, fp) != NULL) {
    if (buf[strlen(buf) - 1] == '\n')
      count++;
  }
  return count;
}
int main(int argc, const char *argv[]) {
  FILE *fp;
  int line = 0;
  if ((fp = fopen(argv[1], "r")) == NULL) {
    errlog("fopen error");
  }
  line = get_line(fp);
  printf("line = %d\n", line);
  fclose(fp);
  return 0;
}点击复制复制失败已复制


编译运行:

$ gcc main.c && ./a.out test.txt         
line = 2点击复制复制失败已复制


按数据大小的形式输入/输出

标准I/O也提供了按照数据块大小的形式对文件的输入/输出,而不管数据的格式如何。


fwrite()

#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);点击复制复制失败已复制


fwrite() 函数被用来向指定的流中输入数据,根据 Linux 官方手册的说明,该函数功能为向指定的流指针 stream 中,写入 nmemb 个单元数据,单元数据的大小为 size ,参数 ptr 用来指向需要写入的数据。


注意

参数 nmemb 表示的是单元数据的个数,而非字符的个数。因此,单元数据是什么格式,完全由程序自行定义,可以是字符串、数组、结构体,甚至是共同体。


数据将采用结构体的形式输入文件。


新建 source.h 头文件,定义结构体如下所示:

#ifndef _SOURCE_H
#define _SOURCE_H
#define N 32
struct data{
  int a;
  char b;
  char buf[N];
};
#endif点击复制复制失败已复制


新建 main.c 文件,写入如下内容:

#include "source.h"
#include <stdio.h>
#include <string.h>
#define errlog(errmsg)                                                         \
  perror(errmsg);                                                              \
  printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__);                \
  return -1;
struct data obj = {
    .a = 10,
    .b = 'q',
    .buf = "test",
};
int main(int argc, const char *argv[]) {
  FILE *fp;
  if ((fp = fopen("test.txt", "w")) == NULL) {
    errlog("fopen error");
  }
  if (fwrite(&obj, sizeof(struct data), 1, fp) < 0) {
    errlog("fwrite error");
  }
  fclose(fp);
  return 0;
}点击复制复制失败已复制


代码将头文件中定义的结构体进行初始化,并写入指定的流(文件)中,运行结果如下所示:

$ gcc main.c && ./a.out && od -c test.txt
0000000  \n  \0  \0  \0   q   t   e   s   t  \0  \0  \0  \0  \0  \0  \0
0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0
0000040  \0  \0  \0  \0  \0  \0  \0  \0
0000050点击复制复制失败已复制


注意

文件中的 '\0' 不是结束符,写入 test.txt 文件中的数据是一些机器码。其实也不难理解,结构体属于构造数据类型,它在内存中存储的形式与数组的线程存储不同。结构体存储需要按照字节对齐的形式存储。


fread()

fread() 函数被用来从指定的流中读取数据,其参数与 fwrite() 函数一致,不同的是数据传递的方向发生了变化。

#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);点击复制复制失败已复制


参数 ptr 用来保存读取的数据, nmemb 表示读取的单元数据的个数。具体使用如下所示:

#include "source.h"
#include <stdio.h>
#include <string.h>
#define errlog(errmsg)                                                         \
  perror(errmsg);                                                              \
  printf("--%s--%s--%d--\n", __FILE__, __FUNCTION__, __LINE__);                \
  return -1;
int main(int argc, const char *argv[]) {
  FILE *fp;
  struct data obj;
  if ((fp = fopen("test.txt", "r")) == NULL) {
    errlog("fopen error");
  }
  if (fread(&obj, sizeof(struct data), 1, fp) > 0) {
    printf("a=%d b=%c buf=%s\n", obj.a, obj.b, obj.buf);
  }
  fclose(fp);
  return 0;
}点击复制复制失败已复制


编译运行,结果如下:

$ gcc main.c && ./a.out 
a=10 b=q buf=test
目录
相关文章
|
机器学习/深度学习 数据挖掘
R实战|从文献入手谈谈logistic回归、Cox回归以及Lasso分析(一)
R实战|从文献入手谈谈logistic回归、Cox回归以及Lasso分析(一)
1424 0
|
8月前
|
SQL 人工智能 前端开发
JeecgBoot 低代码平台 v3.7.4 发布,后台架构大升级
JeecgBoot 是一款基于 SpringBoot2.x/3.x 和 SpringCloud Alibaba 的企业级 AI 低代码平台,采用前后端分离架构(Ant Design & Vue3),支持 Mybatis-plus 和 Shiro。它集成了强大的代码生成器,可一键生成前后端代码,无需手动编写,大幅减少重复工作。平台支持 DeepSeek、ChatGPT 和 Ollama 等主流大模型,提供 AI 对话
528 9
|
SQL Oracle 关系型数据库
Oracle 19c OCP 认证考试 082 题库(第26题)- 2024年修正版
这是关于Oracle 19c OCP认证考试(1Z0-082)的题库,包含90道题目,通过分数为60%,考试时间为150分钟。本文由CUUG原创整理,重点解析了全球临时表的特点和相关操作,并提供了正确答案及详细解释,帮助考生更好地理解和备考。
221 2
Mac 复制文件名目录路径
Mac 复制文件名目录路径
1803 0
|
测试技术 Python
python自动化测试中装饰器@ddt与@data源码深入解析
综上所述,使用 `@ddt`和 `@data`可以大大简化写作测试用例的过程,让我们能专注于测试逻辑的本身,而无需编写重复的测试方法。通过讲解了 `@ddt`和 `@data`源码的关键部分,我们可以更深入地理解其背后的工作原理。
349 1
|
SQL 前端开发 JavaScript
使用ueditor实现多图片上传案例
使用ueditor实现多图片上传案例
|
数据中心
蓝易云 - 1U和2U的服务器怎么选择
总的来说,选择1U还是2U服务器,主要取决于你的特定需求,包括空间、性能、扩展性和散热等因素。
1029 6
|
存储 分布式计算 负载均衡
|
Java API Maven
手把手教你使用Gradle脚本上传代码仓库
手把手教你使用Gradle脚本上传代码仓库
1152 0
手把手教你使用Gradle脚本上传代码仓库
|
关系型数据库 MySQL 数据安全/隐私保护
已解决:mysql: [Warning] Using a password on the command line interface can be insecure.
已解决:mysql: [Warning] Using a password on the command line interface can be insecure.
1003 0

热门文章

最新文章