11. 文件的读取与写入


该节主要讲述使用c语言如何实现文件的创建、读取、修改、写入文本文件以及二进制文件的

一、打开文件

  • fopen()函数:

函数定义

函数定义:FILE *fopen(const char *pathname, const char *mode);

可以使用 fopen() 函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE 的一个对象,类型 FILE 包含了所有用来控制流的必要的信息。其中pathname是一个字符串,是要打开的文件的文件名,mode是访问模式,具体如下:

Mode Description
r 读取文件。
r+ 打开文件实现读取与写入。
w 如果文件不存在,则创建文本文件并写入,程序会从文件的开头写入。如果文件存在,将文件截断为0长度(清空文件),再重新写入。
w+ 打开文件实现读取和写入。如果文件不存在就创建,如果存在就截断(清空)重新写入。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。

注意:mode:The argument mode points to a string beginning with one of the above sequences (possibly followed by additional characters)
也就是说:r --> rwere···只会识别符合上述字符串的前几位,后面的不会在意,并且不会报错。

如果处理的是二进制文件,在 Linux 环境下不需要使用,而在移植到 Windows 环境下时,则需要加b,使用下面的命令:

"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"
  • errno

如果函数调用成功的话,就会返回一个 FILE 指针,否则失败的话,会返回一个NULL(空指针),并且 errno 会被设置以表示出现的错误。errno是一个全局变量。

查看定义:

  • vim /usr/include/asm-generic/errno-base.h

image
image

  • vim /usr/include/asm-generic/errno.h

image

errno以前是整型变量,经后来多次重构之后变成了宏。
从上面的宏定义可以看出,使用errno的时候要包含头文件errno.h即可。该头文件也包含了errno-base.h

示例:验证 errno 不是一个整型变量

#include <errno.h>

errno;  //程序时运行不起来的,这里只看预处理后的情况

image

实例演示:errno输出

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <errno.h>  
  4 
  5 int main(int argc, char *argv[])
  6 {
  7     FILE *fp;
  8     fp = fopen("tmp", "r");  //"tmp" isn't exist in there
  9     
 10     if(fp == NULL)
 11     {
 12         fprintf(stderr, "fopen is wrong! errno = %d\n", errno);
 13         exit(1);
 14     }   
 15     
 16     puts("OK!");
 17 
 18     
 19     exit(0);
 20 }   

输出结果:

image

查看errno.h,等于2时表示++要读取的文件不存在++:

image

那么问题又来了,errno那么多值,怎么能记得住,要是可以直接打印输出“error message”错误信息就更好了。使用perrorstrerror即可实现输出错误信息。

  • perror
    先看定义:
SYNOPSIS
       #include <stdio.h>  //包含的头文件

       void perror(const char *s);  //语法格式形参为字符指针

       #include <errno.h>

       const char * const sys_errlist[];
       int sys_nerr;
       int errno;       /* Not really declared this way; see errno(3) */

示例代码:

//将上面的第12行代码改为:
perror("fopen");

输出结果:

image

  • strerror

定义:

SYNOPSIS
       #include <string.h>

       char *strerror(int errnum);  //形参是 errno
       const char *strerrorname_np(int errnum);
       const char *strerrordesc_np(int errnum);

       int strerror_r(int errnum, char *buf, size_t buflen);
                   /* XSI-compliant */

       char *strerror_r(int errnum, char *buf, size_t buflen);
                   /* GNU-specific */

       char *strerror_l(int errnum, locale_t locale);

示例代码:

//包含头文件
#include <string.h>

fprintf(stderr, "fopen(): %s !\n", strerror(errno));

输出结果:

image

  • 判断fopen一次可以最多打开多少文件

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <errno.h>
  4 
  5 int main()
  6 {
  7     FILE *fp = NULL;
  8     int count = 0;
  9 
 10     while(1)
 11     {
 12         fp = fopen("tmp", "r");
 13     
 14         if(fp == NULL)
 15         {
 16             perror("fopen()");
 17             break;
 18         }
 19     
 20         count++;
 21     }
 22     
 23     printf("count = %d \n", count);
 24     
 25     exit(0);
 26 }

image

这里发现最多可打开 1021 次,对吗?不对的!但是达到打开上限是真的。至于这个上限是多少次,分析如下:
在不更改当前默认环境的情况下,一个进程默认打开了3个流(stream)--> stdin stdout stder,而文件打开也属于是流操作,因此上限一共是:1021 + 3 = 1024

可以使用命令ulimit -a查看:

jxs@jxs-ubuntu:~/Desktop/unix_c$ 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) 15283
max locked memory           (kbytes, -l) 497896
max memory size             (kbytes, -m) unlimited
open files                          (-n) 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) 15283
virtual memory              (kbytes, -v) unlimited
file locks                          (-x) unlimited

//更改的话,类似使用:
ulimit -n 2048  

image

  • 关于使用fopen创建的文件的权限分配

查看权限:

image

6 6 4,为什么呢?

原因:权限分配 --> 0666 & ~umask
image

(0666)8 --> (110 110 110)2, ~umask --> ~(000 000 010)2 = (111 111 101)2;
(110 110 110)2 & (111 111 101)2 = (110 110 100)2 =++(6 6 4)++

可见umask值越大,创建的文件的权限越低。

二、关闭文件

  • fclose()函数:
函数定义:int fclose(FILE *stream);

如果流参数是非法指针,或者是已传递给先前调用的描述符,那么fclose()的行为就是未定义的。如果成功关闭文件,fclose( ) 函数返回零,如果关闭文件时发生错误,函数返回 EOF。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。

由于fopen返回的指针是存储在堆上的,属于动态分配的内存,因此要使用fclose函数对内存的释放,free关键词就是定义在该函数中的。

示例代码:

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <errno.h>
  4 #include <string.h>
  5 
  6 int main(int argc, char *argv[])
  7 {   
  8     FILE *fp;
  9     fp = fopen("tmp", "r");  //"tmp" isn't exist in there
 10     
 11     if(fp == NULL)
 12     {  
 13        // fprintf(stderr, "fopen is wrong! errno = %d\n", errno);
 14        // perror("fopen");
 15         fprintf(stderr, "fopen(): %s !\n", strerror(errno));
 16     
 17         
 18         exit(1);
 19     }
 20     
 21     puts("OK!");
 22     fclose(fp);  //谁打开谁关闭
 23     
 24     exit(0);
 25 }

输出结果:

image

三、写入文件

  • 使用fputc()函数时:
函数定义:int fputc( int c, FILE *fp );

该函数将字符c写入到fp所指向的输出流中。如果成功写入会返回写入的字符,否则会返回EOF可以使用下面的函数将以NULL结尾的字符串写入到流中。

  • 使用fputs()函数时:
函数定义:int fputs( const char *s, FILE *fp );

该函数可以将字符串s写入到fp执行的输出流中。如果写入成功则函数返回非负值,否则返回EOF,同时可以使用函数int fprintf(FILE *fp, const char* format, ···)把一个字符串写入到文件中。

EOF是宏常量,int类型的负值整数常量表达式。
该值通过fgets以及相似的函数返回值来表示文件的末尾EOF默认值为-1.

image

示例程序:

#include <stdio.h>

int main()
{
    FILE *fp = NULL;
    //定义对文件的操作权限
    fp = fopen("/tmp/test.txt", "w+");  //打开/tmp/test.txt文件实现读取和写入
    fprintf(fp, "This is a test for fprintf···\n");
    fputs("This is a test for fputs···", fp);
    fcose(fp);
    
    exit(0);
}

注意:请确保您有可用的 tmp目录,如果不存在该目录,则需要在您的计算机上先创建该目录。/tmp 一般是 Linux 系统上的临时目录,如果你在 Windows系统上运行,则需要修改为本地环境中已存在的目录,例如: C:\tmp、D:\tmp等。

实例1:实现文件的复制
//实现的目标:./mycp src dest 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

// 这里main 函数要传参:src dest
int main(int argc, char *argv[])
{
    //定义两个文件流
    FILE *fps;  //源文件
    FILE *fpd;  //目标文件
    int ret;
    
    //如果传入的参数较少,即 < 3 个,就给出提示,并退出程序
    if(argc < 3)
    {
        printf("Error Usage --> %s <source file> <destination file>");
        //break;  //指跳出循环,并不会退出程序
        exit(1);  //由于输入参数不够因此需要退出程序
    }
    
    //打开两个文件并返回指针类型,并检验是否打开成功
    fps = fopen(argv[1], "r");  //argv[1] --> main 函数中的第一个参数且为字符串
    if(fps == NULL)
    {
        perror("fopen");
        exit(1);  //程序结束
    }
    
    fpd = fopen(argv[2], "w");
    if(fpd == NULL)
    {
        perror("fopen");
        exit(1);  //程序结束
    }
    
    //打开成功后:src --> dest 实现数据传输
    while(1)
    {
        //使用fgetc实现字符获取
        ret = fgetc(fps);
        
        //直到end of file(文件末尾),退出循环
        if(ret == EOF)
            break;
            
        //使用fputc实现字符写入,但是fputc(int c, FILE *stream),因此fgetc需要接收一个返回值传入该函数中。
        fputc(ret, fpd);
        
    };
    
    //谁打开谁关闭
    fclose(fpd);  //优先关闭被写入的
    fclose(fps);
    
    exit(0);
}

image

实例3:计算文件大小(size)
//实现的原理:使用 fgetc() 读取文件所包含的字符数,1个字符 --> 1 byte
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

//命令实现:./get_size file ,因此需要 main 函数进行传参
int main(int argc, char *argv[])
{
    FILE *fp;
    int count;  //计数计算文件所含的字符数
    
    fp = fopen(argv[1], "r");  //指针接收 fopen 的打开情况
    
    //判断是否打开成功
    if(fp == NULL)
    {
        perror("fopen");
        exit(1);
    }
    
    //读取文件所含有的字符数
    while(fgetc(fp) != EOF)
    {
        count++;
    }
        
    printf("The size of this file is %d Bytes \n", count);
    
    fclose(fp);
    
    exit(0);
}

image

四、读取文件

上面的程序会分别通过fprintffputs函数将相应的文本写入到test.txt文件中。那么如何读取文件中的内容呢?

  • 从文件中读取单个字符的函数:
int fgetc(FILE *stream);

fgetc函数将从fp指向的输入文件中读取一个字符,返回值是读取的字符,如果发生错误返回EOF

  • 从文件中读取一个字符串的函数:
char fgets(char *s, int n, FILE *stream);

函数将从fp指向的文件中读取n - 1个字符,会将读取的字符串存储到缓冲区s中,并在最后追加一个NULL-->'\0'字符来终止字符串。如果这个函数在读取到最后一个字符串之前就遇到\n或者文件的末尾EOF的话,只会返回所读取到的字节,也包括换行符。也可以使用int fscanf(FILE *fp, const char *format, ···)函数读取文件中的字符串,但是遇到空格或者换行符时会停止读取。
比如:文件中有 abcd'\n' 使用fgets函数读取时,假设n = 5,那么完全读取该文件++需要读两次++

  • 1 --> a b c d '\0'
  • 2 --> '\n' '\0'

示例函数:

//关于返回值:fgets() 成功的话返回字符串s, 错误或者读到文件尾已经没有字符可以读取了返回 NULL 
while(fgets(buff, SIZE, fps) != NULL)  
{
    fputs(buff, fpd);
};

五、fread&fwrite

1.1. 定义

  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • fread()fwrite从流中读取数据的nmemb项,每一项的数据大小为 size bytes,将读取到的数据存储至 ptr 中,读入的大小为 (size * nmemb) bytes.
  • 返回值为读取的项数,当 size = 1 时,等同于传输的字节数,如果读取失败或者读到文件尾,返回值是short item count (or zero)

1.2. 实例

//实现的目标是:使用 fread 与 fwrite 实现文件复制
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define SIZE 1024

int main(int argc, char **argv)
{
    FILE *fps;
    FILE *fpd;
    char buff[SIZE];
    int n;
    
    if(argc < 3)
    {
        printf("Usage Error!\n");
        exit(1);
    }
        
    fps = fopen(argv[1], "r");
    if(fps == NULL)
    {
        perror("fopen");
        exit(1);
    }
    
    fpd = fopen(argv[2], "w");
    if(fps == NULL)
    {
        perror("fopen");
        exit(1);
    }
    
    while((n = fread(buff, 1, SIZE, fps)) > 0)
    {
        fwrite(buff, 1, n, fpd);  //使用n的原因是,在size = 1时,函数fread()返回值就是实际读取的项数,如果不是n的话,是SIZE的话,如果fps对应的文件字符数小于SIZE的话,最后fpd对应的文件就会有多余的字符出现
    };
    
    fclose(fps);
    fclose(fpd);
    
    exit(0);
}

image

posted @ 2023-06-29 17:08  假行僧me  阅读(27)  评论(0编辑  收藏  举报