文件操作

什么是文件

磁盘上的文件就是文件,分为普通文件和目录文件(文件夹);而在程序设计中,有两种文件:
程序文件:包括源程序文件(后缀为 .c),目标文件(windows 环境后缀为 .obj),可执行程序(windows 环境后缀为 .exe);
数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出 内容的文件;

  在以前学习的操作中,对数据的操作都是以终端为对象来操作的,从终端的键盘输入数据,运行结果显示到显示器上;不过这样的操作难以将数据保存下来,例如我前面博客中写的一个小项目——通讯录,你在运行这个程序的时候,当你把联系人的信息输入之后,你可以查看相应的数据,但是当你将程序关闭之后再打开,你之前录入的联系人信息就全部消失不见了;
  现在我们可以把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上面的文件。通过这样的方式我们就可以将数据有效的保存较长的时间。

文件名

  一个文件要有一个唯一的标识——文件名,文件名由三部分构成:文件路径+文件名主干+文件后缀;

文件路径是为了能找到文件在计算机中的位置,后续代码想要针对文件进行操作,就必须通过路径来找需要的文件。文件路径分为绝对路径和相对路径;
绝对路径:就是从打开文件夹开始,到找到你需要找的文件为止,这个路径就是绝对路径,例如:E:\git\c-\TheAddressBook\Debug\TheAddressBook.exe,这就是我的通讯录可执行程序的绝对路径;
相对路径:就是选定一个参考系,告诉系统从这个目录出发,然后查找文件,例如:选定E:\git\c-\TheAddressBook\Debug作为参考系,此时通讯录可执行程序就是.\TheAddressBook.exe;
在上面两种查找文件的方法中,我们可以看到在路径中出现 \这个符号,其实在 Windows 下,路径中不止 \这可以作为分割符,还可以使用 /作为分割符,在 Linux/Mac 下,只能使用 /作为分割符;那么在实际开发中,更推荐使用 /这个,因为在程序中, \这个符号也有转义字符的意思,所以会搞混淆;

  文件名就是为了在找到的路径中确定唯一的文件;

  文件后缀是为了区分文件的种类,告诉操作系统使用哪个程序来默认打开这个文件;

文件类型

根据数据的组织形式,可以把文件分成两类:文本文件和二进制文件;
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件;
如果要求在外存上以 ASCII 码的形式存储,则需要在存储前转换。以 ASCII 字符的形式存储的文件就是文本文件;

  其实有一种非常简单的方式就能分辨出到底是文本文件还是二进制文件,先打开系统软件——记事本,然后将你需要查看的文件移入记事本,展示出来的结果你要是能看得懂,那就是文本文件,否则就是二进制文件。

文件缓冲区

  ANSIC 标准采用 “缓冲文件系统” 处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序的每一个正在使用的文件开辟一块 “文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据 c 编译系统决定。
  使用文件缓冲区的目的就是为了提高读写文件的效率,因为访问内存的效率远远高于访问外存,所以在需要操作外存时,就先搬运一定量的数据到缓冲区中(将缓冲区填满了后),然后再一起送到外存中。

文件指针

  每个被使用的文件都会在内存中开辟一块相应的文件信息区,用来存放文件的相关信息,这些文件是保存在一个结构体中,结构体的名字叫做FILE。至于结构体内部是怎样排列的,我们不必关心,我们只需要定义一个FILE*类型的指针变量来指向某个文件的文件信息区

FILE* pf;//文件指针变量

  我们将这个指针称为文件指针,又称为句柄,意思就是遥控器,我们后续对文件进行任何操作都是通过这个遥控器。

打开文件

  既然知道了遥控器的概念,也知道了遥控器怎么制造出来,但是如何将遥控器和这个文件关联起来呢?也就是说如何让这个文件指针指向你想要的文件呢?
  我么可以通过fopen()函数来打开一个文件

FILE* fopen ( const char* filename, const char* mode );

  可以看到,fopen()函数的返回值就是FILE*类型的,所以通过这个函数我么就可以将文件指针指向你所需的文件。先来看看他的简单使用:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include<string.h>
int main()
{
  FILE* fp = fopen("E:/test.txt", "r");
  if(fp == NULL)//如果文件 打开失败   
  {
    printf("文件打开失败!%s\n",strerror(errno));
    perror("文件打开失败!");//输出错误提示
  }
  //对文件进行操作...
  fclose(fp);
  return 0;
}
通过上面的实例,来总结一下使用的注意点:
①函数的第一个参数就是文件路径,使用时应要用双引号引起来;
②函数的第二个参数是文件的打开方式,使用时带上引号;关于打开方式有很多种,有的打开方式只支持读取不支持写入,有的打开方式在没有目标文件的时候会自行创建或是删除原有文件中的内容。具体可以参考下面我画出来的表格:
在这里插入图片描述
③文件打开的时候不总是成功的,也有失败的时候,如果打开文件失败,那么返回 NULL,所以在执行打开文件操作之后,一定要对返回值进行检验,否则将会对一个 NULL 进行操作,出现未定义行为。
④假如文件打开失败,想要查看失败的原因,那么可以使用下面两种方式输出错误信息:
//包含<string.h><errno.h>头文件
//使用strerror()函数打印出errno错误码对应的错误信息
 printf("文件打开失败!%s\n",strerror(errno));
//不用包含头文件,这一个函数就将文字打印出来
//并将错误码对应的错误信息打印出来,相当于是上面两个的合体
 perror("文件打开失败!");

关闭文件

  我们把文件打开肯定要自己将文件关闭,如果只开不关闭,那么文件的文件信息区就会不断地占据内存的空间,然后就会出现和内存泄漏一样的问题,我们称之为文件资源泄露。那么该怎么关闭文件呢?
  我么使用fclose()函数来将打开的文件关闭;

int fclose ( FILE * stream );

  在上一个代码中,我们看到在文件进行操作完毕之后,我就使用了fclose()函数将文件关闭了。

文件操作

  在文件打开成功之后,我们需要对其进行操作:读操作、写操作,下面我们来学习一些进行这些操作的函数。

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

  这个函数的功能用于把磁盘上的文件按块大小读到内存中;其中 ptr – 这是指向带有最小尺寸 size * count 字节的内存块的指针,也就是存储数据的内存块首地址。size – 这是要读取的每块元素的大小,以字节为单位。count – 这是块的个数,每块的大小为 size 字节。stream – 这是指向FILE对象的指针,也就是文件指针。
  成功读取的元素总数会以size_t对象返回,size_t对象是一个整型数据类型,它代表了成功读取的完整块个数;读取出错则返回 0,读取到文件末尾返回 0,读取的数据不足一块也返回 0,由于返回值的不确定性,所以建议在读取时将块大小设置为 1,块个数为数据长度,这样返回值就会是实际读取的文件大小,可以使用if语句进行判断;
  当我们将文件内容读取到内存之后,就可以使用printf()将内容打印到终端上;下面举个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“这是一个测试代码!”
    char ch[1024] = { 0 };
    size_t ret = fread(ch, 1, 1024, p);
    if (ret!=9){
        //如果返回值小于count值就表示读取完毕  
        printf("%s\n%d", ch, ret);
    }
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//结果:这是一个测试代码!
//       9
fwrite()
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream)

  这个函数的功能是把内存的数据按块大小写入到磁盘中;其中 ptr – 这是指向要被写入的元素数组的指针。size – 这是要被写入的每块元素的大小,以字节为单位。count – 预期要读的块的个数,每块的大小为 size 字节。stream – 这是指向FILE对象的指针,也就是文件指针。
  如果成功,该函数返回一个size_t对象,表示实际写入的总块数,该对象是一个整型数据类型,如果写入失败则返回 0;建议写入时将块大小设置为一个字节,块数量设置为总字节数,这样一来如果该数字与 count 参数不同,则表明写入失败。可以使用if语句进行判断;下面举个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“这是一个测试代码!”
    char* ch = "这是一个测试代码two!";
    size_t ret = fwrite(ch, 1, strlen(ch), p);
    if (ret != strlen(ch)) {
        perror("文件写入失败!");
    }
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//最终文本文件中打印出:这是一个测试代码two!

  上面这两个函数的使用比较麻烦,下面再介绍几个比较方便的函数:

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

  这个函数的作用是格式化写入,其实和printf()是等价的,只不过比printf()多了第一个参数–文件指针,然后剩下的格式和我们平时向终端上写入数据是一样的;
  如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。这个和printf()一样,但是在我以前写的代码中,这个返回值并没有使用到,所以了解一下就好;下面举个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“这是一个测试代码two!”
    char* ch = "three";
    fprintf(p, "这是一个测试代码%s!", ch);
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//结果:这是一个测试代码three!
fscanf()
int fscanf(FILE *stream, const char *format, ...)

  这个函数的作用是将文件内容读出,并格式化输入到变量中,如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。(如果读取过程中,碰到空格就会输入到下一个变量中或者结束,这取决于是否含有其他变量来接收数据,解释的不太清楚,多担待)下面举个例子看看:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“这是一个 测试代码 three!”
        //注意,这串字符中间是有空格的,
    char ch1[1024] = { 0 };
    char ch2[1024] = { 0 };
    char ch3[1024] = { 0 };
    fscanf(p, "%s %s %s", ch1, ch2, ch3);
    printf("%s%s%s", ch1, ch2, ch3);
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//结果输出:这是一个测试代码three!
feof()
int feof(FILE *stream)

  c 库函数int feof(FILE *stream)测试给定流 stream 的文件结束标识符。stream为一个文件指针。当设置了与流关联的文件结束标识符时,该函数返回一个非零值,否则返回零。

在文件读取过程中,不能用 feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
1.文本文件读取是否结束,判断返回值是否为 EOF ( fgetc),或者 NULL( fgets) 例如: fgetc判断是否为 EOF。 fgets判断返回值是否为 NULL。
2.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。 例如: fread判断返回值是否小于实际要读的个数。

  本函数与下面的几个函数要联合使用,举例请参考线面的例子。

fgetc()
int fgetc(FILE *stream)

  c 库函数int fgetc(FILE *stream)从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动。stream – 这是指向FILE对象的指针,该FILE对象标识了要在上面执行操作的流。
  该函数以无符号char强制转换为int的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“i love codeing!”
    char ch = 0;
    while (1) {
        if (feof(p)) {
            break;
        }
        ch = fgetc(p);
        printf("%c", ch);
    }
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//结果输出:i love codeing!
fputc()
int fputc(int char, FILE *stream)

  c 库函数int fputc(int char, FILE *stream)把参数char指定的字符(一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动。
  char – 这是要被写入的字符。该字符以其对应的int值进行传递。stream – 这是指向FILE对象的指针,也就是一个文件指针。如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。
  下面举个例子,是将 ASCLL 码值为 33 到值为 100 的字符写入到文件中:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“i love codeing!”
    for (int i = 33; i <= 100; i++) {
        fputc(i, p);
    }
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//结果:!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcd
fgets()
char *fgets(char *str, int n, FILE *stream)

  c 库函数char *fgets(char *str, int n, FILE *stream)从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
  str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串。n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。stream – 这是指向FILE对象的指针,该FILE对象标识了要从中读取字符的流。
  如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。下面举个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "r");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“i love codeing!”
    char ch[1024] = { 0 };
    if (fgets(ch, 1024, p) != NULL) {
        printf("%s", ch);
    }
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//输出结果:i love codeing!
fputs()
int fputs(const char *str, FILE *stream)

  c 库函数int fputs(const char *str, FILE *stream)把字符串写入到指定的流 stream 中,但不包括空字符。str – 这是一个数组,包含了要写入的以空字符终止的字符序列。stream – 这是指向FILE对象的指针,该FILE对象标识了要被写入字符串的流。该函数返回一个非负值,如果发生错误则返回 EOF。下面举个例子:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    FILE* p = fopen("D:/test.txt", "w");
    //文件打开时要进行判空检验
    if (p == NULL) {
        perror("文件打开失败!");
        return 1;
    }
    //文件的内容为“i love codeing!”
    fputs("这是一个测试代码four!", p);
    //文件不用了一定要记得关闭文件
    fclose(p);
    return 0;
}

//结果:这是一个测试代码four!

  下面再介绍两个与文件操作无关的函数,但是十分有用:

sprintf()
int sprintf(char *str, const char *format, ...)

  c 库函数int sprintf(char *str, const char *format, ...)发送格式化输出到 str 所指向的字符串。有一个很重要的用法就是将整数转换成字符串格式输入到str中;

sscanf()
int sscanf(const char *str, const char *format, ...)

  c 库函数int sscanf(const char *str, const char *format, ...)从字符串读取格式化输入。有一个很重要的作用就是将字符串转换为整数;

  上面两个函数的举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<errno.h>
#include<string.h>

int main() {
    char ch[1024] = { 0 };
    int num = 2048;
    sprintf(ch, "num的值为 %d", num);
    printf("%s\n", ch);
    //输出结果:num的值为 2048

    int day, year;
    char weekday[20], month[20], dtm[100];
    strcpy(dtm, "Saturday March 25 1989");
    sscanf(dtm, "%s %s %d  %d", weekday, month, &day, &year);
    printf("%s %d, %d = %s\n", month, day, year, weekday);
    //输出结果:March 25, 1989 = Saturday
    return 0;
}

随机读写

fseek()
int fseek ( FILE * stream, long int offset, int whence);
  • 功能:根据文件指针的位置和偏移量来定位文件指针,来达到跳转文件读写位置的目的;例如,当我们每次向文件写入数据之后,想要读文件时,此时读的起始位置则是上次写的结束位置,那么想要读出完整数据,就需要使用该函数跳转文件读写位置;
  • stream:操作句柄,打开文件返回的文件流指针;
  • offset:相对起始参照位置的偏移量,该值如果是负数则代表向前,如果是正数则代表了向后,然后从该位置进行读写操作;
  • whence:起始参照位置,也就是自己规定的起点,SEEK_SET—起始位置,SEEK_CUR—当前位置,SEEK_END—末尾位置;
  • 返回值:跳转成功返回 0,跳转失败返回 -1;
#include <stdio.h>
int main ()
{
  FILE * pFile;
  pFile = fopen ( "D:/test.txt" , "wb" );
  fputs ( "This is an apple." , pFile );
  fseek ( pFile , 9 , SEEK_SET );
  fputs ( " sam" , pFile );
  fclose ( pFile );
  return 0;
}
ftell()
long int ftell ( FILE * stream );

  返回文件指着相对于起始位置的偏移量;

#include <stdio.h>
int main ()
{
  FILE * pFile;
  long size;
  pFile = fopen ("D:/test.txt","rb");
  if (pFile==NULL) perror ("Error opening file");
  else
 {
    fseek (pFile, 0, SEEK_END);   // non-portable
    size=ftell (pFile);
    fclose (pFile);
    printf ("Size of myfile.txt: %ld bytes.\n",size);
 }
  return 0;
}
rewind()
void rewind ( FILE * stream );

  让文件指针的位置回到文件的起始位置;

#include <stdio.h>
int main ()
{
  int n;
  FILE * pFile;
  char buffer [27];
  pFile = fopen ("D:/test.txt","w+");
  for ( n='A' ; n<='Z' ; n++)
  fputc ( n, pFile);
  rewind (pFile);
  fread (buffer,1,26,pFile);
  fclose (pFile);
  buffer[26]='\0';
  puts (buffer);
  return 0;
}

博客园发表于 2020-11-28 19:47

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值