零基础学C语言——文件IO

2023-12-27 10:34:58

这是一个C语言系列文章,如果是初学者的话,建议先行阅读之前的文章。笔者也会按照章节顺序发布。

在编程的世界中,我们主要打交道的对象是内存,但是很多实用的软件却也很难离开对文件的操作。例如,nginx这样著名的开源web server,它会读取文件中的配置。

因此,学习编程语言语法后,紧跟的也必然是文件IO——即文件输入(Input)输出(Output)。

在类UNIX操作系统环境下,通常都是把终端(初学者暂时理解为命令行界面吧)当作文件(/dev/tty…)来看待,因此标准库封装了同一套函数来处理终端的IO与文件的IO。

本文所讨论的函数,其声明都在stdio.h中,因此需要使用预处理指令include将其引入。

我们所要讨论的函数有:

  • fopen
  • fclose
  • fputc
  • fputs
  • fgetc
  • fgets
  • fread
  • fwrite
  • fseek
  • feof
  • fprintf
  • fscanf
  • printf
  • scanf

流式IO

上面列举出来的函数是用来操作系统中的文件的,文件内容都是以字节流或者字符流的形式被处理的。

什么叫做流?

我们对文件的操作就好比从一个水管接水。水管就是我们的文件,而水就是文件的内容。那么接水有两种方式:

  1. 要多少接多少
  2. 先按需求接水,但每次接的都先放在碗里,攒到一定量再倒出去处理

此时,第二种就是我们所谓的流式处理。即按照要求将文件内容处理好后放入缓冲区,攒到一定量或者达到某种特殊条件时,才进行后续处理。

fopen

既然我们将文件比作水管,那么要读取文件内容(水)必然需要先打开水管。

fopen就是用来打开文件的。我们看下函数原型:

FILE *fopen(const char * path, const char * mode);
  • path——文件的路径及文件名

  • mode——打开方式,打开方式有很多种组合,我们仅介绍如下几种:

    • r——字符只读模式,打开文件只可读不可写,且读取的内容将作为字符类型(char)
    • w——字符只写模式,打开文件只可写不可读,且写入的内容为字符类型(char),如果文件不存在则会新建一个文件,如果文件已存在则会清空文件(也称截断)
    • a——字符写追加模式,打开文件只可写且写入位置是从当前文件内容的末尾处开始,写入的内容为字符类型(char),如果文件不存在则新建文件
    • rb——二进制只读模式,打开文件只可读不可写,且读取的内容将作为无符号字符类型(unsigned char)
    • wb——二进制只写模式,打开文件只可写不可读,且写入的内容为无符号字符类型(unsigned char),如果文件不存在则会新建一个文件,如果文件已存在则会清空文件
    • ab——二进制写追加模式,打开文件只可写且写入位置是从当前文件内容的末尾处开始,写入的内容为无符号字符类型(unsigned char),如果文件不存在则新建文件

如果在上述打开方式后再加个+,例如:r+、a+、rb+,那么文件则变为即可读写模式,其余特性依赖于+前的前缀。

函数的返回值为FILE指针类型,读者不必纠结这个类型的具体定义,每个标准库的实现也有可能不同,因此不必记下。有些文章会管这个返回值叫做文件句柄,也可以简称其为fp(file pointer)。

我们看个例子:

#include <stdio.h>

int main(void)
{
  FILE *fp = fopen("a.txt", "r");
  if (fp == NULL) //为NULL时,表明打开失败,一般失败原因有:文件不存在,文件及其路径上的目录的权限不正确
    return -1;
  return 0;
}

除却利用fopen打开的句柄,标准库都会预定义三个句柄:stdin, stdout, stderr,分别代表终端的:标准输入、标准输出、标准出错。我们常用的printf其实就是对stdout的输出操作。

fclose

既然水管可以打开,自然也需要关闭。

函数原型如下:

int fclose(FILE *stream);

参数为fopen打开的文件句柄或者stdinstdoutstderr

返回值:如果成功关闭返回0,否则为EOF(是个宏,一般值为-1)。一般工程中,如果没有特殊需求,通常会忽略返回值。

fputc

这个函数是针对字符流句柄的处理。

函数原型:

int fputc(int c, FILE *stream);

函数功能:将字符c,注意是字符(char)不是整数(虽然是整型),写入stream句柄指代的文件的当前位置。

返回值:成功返回0,否则返回EOF。

这里有个当前位置的概念,即文件的读取和写入都是依赖于一个位置指示器,这个指示器指示的就是当前操作到的位置。例如,fputc前,这个位置为0,表示文件起始位置。当fputc一个字符后,当前位置就变为了1。

示例:

#include <stdio.h>

int main(void)
{
  FILE *fp = fopen("a.txt", "w");
  if (fp == NULL)
    return -1;
  int rc = fputc('c', fp);
  if (rc == EOF)
    return -1;
  fclose(fp);
  return 0;
}

打开a.txt可以看到刚刚写入的c。

fputs

fputc是用来写入单个字符的,fputs是用来写入字符数组的。

函数原型:

int fputs(const char *s, FILE *stream);

返回值:成功返回非负值,否则返回EOF。

fgetc

有了写入字符就有读取字符操作。

函数原型:

int fgetc(FILE *stream);

功能:从头stream指代的文件的当前位置读取一个字符。

返回值:如果成功,则返回字符,否则返回EOF。

示例:

#include <stdio.h>

int main(void)
{
  FILE *fp = fopen("a.txt", "r");
  if (fp == NULL)
    return -1;
  int rc = fgetc(fp);
  if (rc == EOF)
    return -1;
  printf("%c\n", (char)rc);
  fclose(fp);
  return 0;
}

输出结果为:c

fgets

同理,可以读取一个字符,也可以读取一段字符数组。

函数原型:

char *fgets(char *str, int size, FILE *stream);

功能:从stream指代的文件的当前位置处读取最多为size-1个字符到str中,如果读取时遇到了换行符,则读取的内容只截止到换行符及其以前。

返回值:成功,则返回字符数组首地址,否则返回NULL。

示例:

加入a.txt中的内容为:

hello
world

那么执行如下代码:

#include <stdio.h>

int main(void)
{
  char s[64] = {0}, *ret;
  FILE *fp = fopen("a.txt", "r");
  if (fp == NULL)
    return -1;
  ret = fgets(s, sizeof(s), fp);
  if (ret == NULL)
    return -1;
  printf("%s\n", ret);
  fclose(fp);
  return 0;
}

执行结果为:

hello

你没看错,hello自带了一个\n,printf中还有个\n,因此会是如此输出结果。

fseek

前面提到过当前位置这个概念,那这个位置可否修改呢?当然可以,正是利用fseek进行修改的。

函数原型:

int fseek(FILE *stream, long offset, int whence);

功能:将stream指代的文件中的当前位置相对于whence指定的位置移动offset字节。offset可以为负数,即向前移动。

其中,whence的值有:

  • SEEK_SET——文件开始处
  • SEEK_CUR——当前位置处
  • SEEK_END——文件末尾

示例:

#include <stdio.h>

int main(void)
{
  char s[64] = {0}, *ret;
  FILE *fp = fopen("a.txt", "r+");
  if (fp == NULL)
    return -1;
  fseek(fp, 5, SEEK_CUR);
  fputc(' ', fp);
  fclose(fp);
  return 0;
}

利用fseek定位到hello和world之间的换行符,然后用写入空格来覆盖换行符,此时a.txt的内容变为:

hello world

feof

当读取文件内容时,虽然读取函数会返回EOF来表示读取结束或者读取出错,但有时我们不希望改变当前位置,同时获知当前位置是否达到文件末尾。这时就要使用feof函数。

函数原型:

int feof(FILE *stream);

返回值:如果未到结尾,则返回0,否则返回非0值。

fprintf

接下来的这个是一种格式输入,即输入的字符数组中存在一种约定好的格式符,不同的格式符对应不同的数据类型,函数会利用后续参数的值来替换字符数组中对应的格式符,然后将替换好的内容写入到文件中。

函数原型:

int fprintf(FILE *stream, const char *format, ...);

最后的…不是省略的意思,而是一种特殊的参数,叫做可变参数,即后面的参数个数不确定,类型不确定。关于可变参的内容,本系列不打算讲解。

功能:将format字符数组中特殊的格式符利用后续参数替换后,写入stream指代的文件的当前位置。

返回值:如果出错,则返回负值,否则返回输出到文件中的字符数。

其中,format支持的常用格式符有:

  • %d——对应int型数值
  • %u——对应unsigned int型数值
  • %ld——long型数值
  • %lu——unsigned long型数值
  • %lld——long long型数值
  • %llu——unsigned long long数值
  • %f——float和double数值
  • %lf——long double数值
  • %c——char型字符
  • %s——char型字符数组(必须以\0结尾)

示例:

#include <stdio.h>

int main(void)
{
  char s[64] = {0};
  FILE *fp = fopen("a.txt", "w");
  if (fp == NULL)
    return -1;
  fprintf(fp, "Hello %s", "World");
  fclose(fp);
  return 0;
}

a.txt的内容变为:

Hello World

fscanf

与fprintf相反,fscanf用于格式输出,即给定一个格式字符数组(字符串),其格式刚好匹配文件内容的格式,那么将字符数组中的特殊格式符处的值,写入到其后相应的参数变量中。

函数原型:

int fscanf(FILE *stream, const char *format, ...);

返回值:成功则返回成功匹配格式符并赋值的个数,失败则返回EOF。

format支持的格式符与fprintf的一致。

这里要注意的是,format后的参数,都是指针,因为fscanf在其内部要对变量赋值,而函数参数是自动变量,在函数生命周期结束后就会销毁回收,因此修改的内容无法传递给调用方,所以需要传递指针。

示例:

#include <stdio.h>

int main(void)
{
  char s[64] = {0};
  FILE *fp = fopen("a.txt", "r");
  if (fp == NULL)
    return -1;
  int n = fscanf(fp, "Hello %s", s);
  fclose(fp);
  printf("n:%d s:%s\n", n, s);
  return 0;
}

输出的结果为:

n:1 s:World

注意:本例中,World的长度远小于数组s的长度,因此如此使用没有问题。但是如果s只有2字节长度,那么调用fscanf就会导致缓冲区溢出。缓冲区溢出是一种bug,轻则程序崩溃,重则会被黑客利用夺取本机远程操作权限。对于任何一个C开发人员来说,都应慎重对待此类bug。

printf

printf相当于fprintf(stdout, format, …);

其函数原型为:

int printf(const char *format, ...);

scanf

scanf相当于fscanf(stdin, format, …);

其函数原型为:

int scanf(const char *restrict format, ...);

fwrite

下面要介绍的这两个函数都是针对二进制流的操作。

二进制流(字节流)与字符流一样,都需要有输入操作,即向文件中写入数据。

函数原型:

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);

功能:将ptr所指向的数组(nitems个size字节数据)写入到stream指代的文件的当前位置中。

返回值:成功,返回写入的size大小的数据个数(理论上应该等于nitems),否则返回0或者一个小于nitems的数。

示例:

#include <stdio.h>

int main(void)
{
  int i = 65536;
  FILE *fp = fopen("a.dat", "wb");
  if (fp == NULL)
    return -1;
  size_t n = fwrite(&i, sizeof(i), 1, fp);
  fclose(fp);
  return 0;
}

此时,a.dat的内容我们用xxd看下文件中的十六进制:

$ xxd a.dat
00000000: 0000 0100 

可以看到,文件中的十六进制值为0000 0100刚好是十进制的65536。

fread

二进制流(字节流)输出操作。

函数原型:

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);

功能:从stream指向的文件的当前位置中,读取nitems个size字节大小(一共nitems*size字节)的数据到ptr指向的数组中。

返回值:成功,返回读取的size大小的数据个数(理论上应该等于nitems),否则返回0或者一个小于nitems的数。

示例:

#include <stdio.h>

int main(void)
{
  int i = 0;
  FILE *fp = fopen("a.dat", "rb");
  if (fp == NULL)
    return -1;
  size_t n = fread(&i, sizeof(i), 1, fp);
  fclose(fp);
  printf("%d\n", i);
  return 0;
}

输出结果为:65536

到此,零基础学C语言系列文章完结。读者如果通读过这个系列的文章,并且学会其中的各个知识点,那么恭喜你,你已经站在了C语言大门内了,虽然依旧是贴着门站。

笔者推荐下一步学习《UNIX环境高级编程》以及一些操作系统相关知识,奠定一些实用基础。



喜欢的小伙伴可以关注码哥,也可以给码哥留言评论,如有建议或者意见也欢迎私信码哥,我会第一时间回复。
感谢阅读!

文章来源:https://blog.csdn.net/weixin_40960130/article/details/135232981
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。