humid1ch blogs

本篇文章

手机用户建议
PC模式 或 横屏
阅读


C语言 2022 年 3 月 2 日

[C语言] C语言能对文件进行哪些操作?

文件按照功能, 区分为两类: 程序文件、数据文件

文件操作

一. 文件的分类

文件按照功能, 区分为两类: 程序文件、数据文件
两种文件类型的区分是相对的, 并不是绝对的
  1. 程序文件

    比如:

    C语言的源程序文件( .c 为后缀的文件 )

    目标文件( 在Windows环境中 以 .obj 为后缀 )

    可执行程序文件( 在Windows环境中 以 .exe 为后缀 )

  2. 数据文件

    数据文件的内容, 不一定是程序

    可以是程序运行时所需要读取、改变的数据

为什么说两种文件类型的区分是相对的?

比如:

存在两个源文件test1.c test2.c

如果test1.c文件可以对test2.c文件中的数据进行读取等操作, 那么test1.c就是程序文件, test2.c就是数据文件


PS: 以下讨论的均为数据文件

二. 文件的操作

2.1 文件指针

在学习文件指针之前, 先介绍一个概念: 文件信息区

文件信息区:

在C语言中, 每次打开一个文件, 操作系统都会在内存中开辟一块区域来存放该文件的各种信息(比如文件名、文件的状态、文件的地址、文件的大小等)

这些信息都存放在一个结构体变量中, 此结构体变量的类型默认被系统声明为 FILE

所以, 被使用文件的文件信息区, 本质上就是一个FILE 类型的结构体变量

每当一个文件打开后, 计算机会自动根据文件的状态、情况自动生成一个FILE类型的结构体变量, 并存入该文件的各种信息

FILE 类型的具体成员, 内容。在不同的编译器中是不完全相同的, 但是差别不大

我们使用FILE类型定义的结构体指针变量, 就是一个文件指针变量
FILE* pf;	//pf 文件指针变量
定义pf是一个指向FILE类型数据的指针变量, 可以指向某个文件的文件信息区, 通过文件信息区中存放的信息可以进一步访问该文件
所以, 通过文件指针变量能够找到与其相关联的文件

2.2 文件的打开与关闭

文件的打开操作及关闭操作, 需要使用两个函数 fopen(文件打开函数)fclose(文件关闭函数)

fopen()

FILE* fopen(const char *filename, const char *mode);
  1. 第一个参数filename

    应该传入 需打开文件的文件名尽量详细需要打开的文件名, 如: C:\\Program Files\\TEST.c

    若只传入TEST.c, 只会默认打开(创建), 运行可执行程序时, 用户所在路径的 TEST.c 文件

  2. 第二个参数 mode, 应该传入 表示文件打开模式(方式)的字符串

    具体的模式有:

    1. 表示读写权限的

      字符串权限说明
      "r"只读只允许读取, 不允许写入. 文件必须存在, 否则打开失败
      "w"写入若文件不存在, 则创建一个新文件; 若文件存在, 则清空文件内容
      "a"追加若文件不存在, 则创建一个新文件; 若文件存在, 则将写入的数据追加到文件的末尾
      "r+"读写既可以读取也可以写入. 文件必须存在, 否则打开失败
      "w+"写入既可以读取也可以写入. 若文件不存在, 则创建一个新文件;若文件存在, 则清空文件内容
      "a+"追加既可以读取也可以写入. 若文件不存在, 则创建一个新文件; 若文件存在, 则将写入的数据追加到文件的末尾
    2. 表示读写方式的:

      字符串说明
      "t"以文本文件方式读写
      "b"以二进制文件方式读写

    其实, 文件打开方式由 r、w、a、t、b、+ 六个字符拼成, 各字符的含义是:

    • r(read): 读取
    • w(write): 写入
    • a(append): 追加
    • t(text): 文本文件
    • b(binary): 二进制文件
    • +: 读取和写入

    mode, 传参时, 读写权限和读写方式 是可以结合使用的, 但必须将 读写方式 放在 读写权限中间或者尾部, 不过 读写方式可以忽略不写, 忽略的情况下, 默认为 "t", 即默认以文本文件的方式进行读写

    读写权限 及 读写方式 的结合使用, 例:

    读写方式放在读写权限的尾部 "rt""rb""r+t""r+b""wt""w+b""at"等等

    读写方式放在读写权限的中间 "rt+""rb+""wt+""wb+"等等

fopen函数的返回值 是FILE*类型的, 返回的是所打开的文件的文件信息区的首地址, 所以需要用 FILE* 类型的指针变量接收, 然后可以通过此指针变量操作此文件信息。

fclose()

int fclose(FILE* stream);

参数的类型是 FILE* 的指针变量, 此指针变量 需指向 已打开文件的文件信息区的地址

例如

#include 

int main () {
 //打开文件
	FILE * pf = fopen ("test.txt","w");
	if (pf != NULL) {
	//文件操作
  	//…………
   	//关闭文件
		fclose (pf);
 		pf = NULL;
   	}
    
	return 0;
}

2.3 文件的顺序读写

2.3.1 文件读写函数

功能函数名函数适用于
字符输入函数fgetcint fgetc(FILE* stream);所有输入流
字符输出函数fputcint fputc(int c, FILE* stream);所有输出流
文本行输入函数fgetschar *fgets(char* string, int n, FILE* stream);所有输入流
文本行输出函数fputsint fputs(const char* string, FILE* stream);所有输出流
格式化输入函数fscanfint fscanf(FILE* stream, const char* format [, argument ]...);所有输入流
格式化输出函数fprintfint fprintf(FILE* stream, const char* format [, argument ]...);所有输出流
二进制输入freadsize_t fread(void* buffer, size_t size, size_t count, FILE* stream );文件
二进制输出fwritesize_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream );文件
上面是对 打开的文件进行顺序读写时, 可用到的函数。

2.3.2 单个字符读写

使用上边的函数, 尝试向文件中写入字符
fputc()
#include 

int main() {
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL) {
		printf("打开文件失败\n");
        
		return 0;
	}
    
	fputc('c', pf);
    fputc('s', pf);
    fputc('b', pf);
    fputc('i', pf);
    fputc('t', pf);

	fclose(pf);
	pf = NULL;

	return 0;
}
我们使用fputc 函数成功在文件中写入了内容
fputc_FILE
fputc_FILE
不过这时候肯定会有疑惑, 比如: fputc不是字符输出函数吗?为什么能往文件中输入字符?
需要知道为什么, 就需要学习在文件操作中的以下两个概念:
  • 输入

    在一般的认知中, 用键盘打字, 就算是输入了

    但在文件操作中, 输入, 指 从键盘获取的内容 存入 内存中; 也可以指 文件中的内容 存入 内存中

    输入的终点, 是内存, 而不是文件

    input
    input
  • 输出

    与输入相反, 在文件操作中, 把 内存中的数据 输出显示到 屏幕上, 或是 输出到 文件中, 这才是输出操作

    所以, 用fputc 字符输出函数, 往文件中输入字符

    output
    output
fputc函数, 成功向文件中写入了字符, 那么如何向屏幕上输出字符呢?需不需要先类似打开文件的操作呢?
很显然不需要先打开屏幕什么的, 为什么呢?
在C语言程序运行时, 会默认打开三个流:
stdin: 标准输入流, 默认对应键盘
stdout: 标准输出流, 默认对应屏幕
stderr: 标准错误流, 默认对应屏幕
三个标准流, 都是FILE*类型的
当我们需要用fputc 函数, 向屏幕输出字符的时候, 只需要把目标文件地址改为 标准输出流 就可以了:
#include 

int main() {
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL) {
		printf("打开文件失败\n");
        
		return 0;
	}
    
	fputc('a', stdout);
    fputc('b', stdout);
    fputc('c', stdout);
    fputc('d', stdout);

	fclose(pf);
	pf = NULL;

	return 0;
}
fputc_STDOUT
fputc_STDOUT
上面测试了fputc 输出字符函数, 那么怎么样使用输入字符函数将文件内的数据, 输入至内存中呢?
fgetc()
首先在.c源文件的路径下创建test2.txt文件, 并输入相应的内容:
#include <stdio.h>

int main() {
    //以只读方式打开文件, 需要先创建文件
	FILE* pf = fopen("test2.txt", "r");
	if (pf == NULL) {
		printf("打开文件失败\n");
		return 0;
	}
	int ch;
    //将 fgetc 的返回值存入 ch, 再将 ch 内容输出
	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	fclose(pf);
	pf = NULL;

	return 0;
}
以上代码的运行结果如下(test2.txt 文件 在程序中被打开前 内容就为: abcdefg):
fgetc_FILE
fgetc_FILE
fgetc的返回值存入 变量ch 并输出, 屏幕上也输出了 abcd
!!! 这也说明了, 如果读取成功, fgetc函数的返回值就是 读取到的字符的 ASCII
但是ch为什么不用char类型呢?读取的内容不是字符吗?用char类型的变量来接收也可以吗?
答案是不可以, 为什么?
fgetc_RETURN
fgetc_RETURN
在这句话中我们可以看出, fgetc将读取到的字符以int类型返回 或者 返回EOF, 表示读取错误 或 文件结尾
说明fgetc的返回值, 并不一定全都是 字符, 也有可能是 EOF, 所以我们要用int类型的变量接收

fgetc函数, 传参传入的是 需要被存放到内存中的 文件的数据 的地址

并且, 在程序执行时 屏幕上输出的内容是不同的, 这意味着 fgetc 函数读到的数据是不同的, 但传入的参数均为 变量pf, 这说明, fgetc函数会将传入的地址向后移动一位(移动到下一次需要读取的数据的地址)

然后, 上边使用fputc字符输出函数的时候, 每次传入的参数也是同一个变量, 但是输出的字符却不一样, 所以 fputc函数每次传入相同的函数进行调用, 也会将传入的地址向后移动一位, 以便下一次输出不覆盖之前的输出


2.3.3 整行字符读写

对文件的内容一行一行的读写, 就需要用到这两个函数
fputs()
fputs函数 与 fputc函数的使用方法类似, 只不过本函数是输出一行, 而另一个是输出单个字符
以下示例:
#include <stdio.h>

int main() {
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL) {
		printf("打开文件失败\n");

		return 0;
	}
	fputs("Hello Bit\n", pf);
	fputs("Great\n", pf);

	fclose(pf);
	pf = NULL;

	return 0;
}
fputs_FILE
fputs_FILE
同样的, 会改变传入的地址, 会将传入的地址 向后移动输出的字符串位数 位
fgets()
fgets函数的使用方法, 就与fgetc函数有很大的不同了。
char* fgets(char* string, int n, FILE* stream);
三个参数分别代表:
  1. string: 需要输入的字符串地址
  2. n: 需要输入到第几位
  3. stream: 读取的文件的文件指针
使用方法如下:
#include <stdio.h>

int main() {
    // 程序运行前需要创建好 test2.txt, 并输入相应的内容
	FILE* pf = fopen("test2.txt", "r");
	if (pf == NULL) {
		printf("打开文件失败\n");
        
		return 0;
	}
    
	char ch[100] = { 0 };
	fgets(ch, 3, pf);
	printf("%s", ch);

	fgets(ch, 3, pf);
	printf("%s", ch);

	fclose(pf);
	pf = NULL;

	return 0;
}
fgets_FILE
fgets_FILE
fgets函数可以 自定义每次输入的字符长度 , 即 第二个参数 减 1
并且, 每次输入到内存中, 如果传参不变, 会将已经输入到内存中的数据覆盖
fgets_MEMORY
fgets_MEMORY
若, 传参大于文件中数据的长度, 则输入完整
fgets(ch, 3, pf); >>>>>> fgets(ch, 100, pf);
fgets_FLIE_LONGTH
fgets_FLIE_LONGTH

2.3.4 格式化数据读写

这里的这个格式化, 不是格式化清空的意思, 而是 有一定格式的数据, 就是格式化数据. 比如, 结构体等自定义类型
格式化的读写, 需要用到这两个函数fscanf 格式化输入函数fprintf 格式化输出函数
这两个函数, 与scanfprintf`长得很像
其实不仅长得像, 用法也很类似:
fprintf()
用结构体来举例:
#include <stdio.h>

struct student {
	char name[20];
	int age;
	char sex[10];
};

int main() {
    struct student xxs = {"July", 20, "male"};
    
    FILE* pf = fopen("test.txt", "w");
    if(pf == NULL) {
        printf("文件打开失败\n");
        return 0;
    }
    
    fprintf(pf, "%s %d %s", xxs.name, xxs.age, xxs.sex);

    fclose(pf);
    pf = NULL;

    return 0;
}
程序运行结果如下:
fprintf_FILE
fprintf_FILE
同样的, 可以将文件指针改为 标准输出流 将内存中的数据输出到 屏幕上, 这里就不演示了
fscanf()

还是用结构体来举例, 不过这次是将文件中的数据存入内存中:

#include 

struct student {
   char name[20];
   int age;
   char sex[10];
};

int main() {
   struct student xxs = { 0 };
   
   FILE* pf = fopen("test2.txt", "r");
   if(pf == NULL) {
       printf("文件打开失败\n");
       return 0;
   }
   
   fscanf(pf, "%s %d %s", xxs.name, &(xxs.age), xxs.sex);
   printf("%s %d %s", xxs.name, xxs.age, xxs.sex);

   fclose(pf);
   pf = NULL;

   return 0;
}

程序运行结果如下:

fscanf_FILE
fscanf_FILE
通过两个例子可以看出, fprintffscanf两个函数, 可以对内存或者文件中的 格式化的数据 进行读写的操作
并且, 两个函数的的使用方法与printf scanf两个函数的使用方法 十分的相似

2.3.5 二进制读写

二进制的读取和写入, 顾名思义, 就是将数据从内存以二进制的形式输出到文件(写入文件), 或者将二进制的数据从文件中写入到内存(读取文件)
fwrite()
size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream );
fwrite 函数, 数据从内存以二进制的形式输出到文件(写入文件)
此函数的参数表示的是:
  1. const void* buffer: 需要输出到文件的数据
  2. size_t size: 需要写入的数据的类型(大小)
  3. size_t count: 需要写入的数据的个数
  4. FILE* stream: 文件流
二进制输出示例:
#include <stdio.h>

struct Stu {
	char name[20];
	int age;
	char sex[10];
};

int main() {
	struct Stu stu[3] = { {"CSDN", 15, "Not"}, {"July", 19, "Male"}, {"Three", 20, "Male"} };
    
	FILE* pf = fopen("data.txt", "wb");		//以二进制输出形式打开文件(写入文件的形式)
	if (pf == NULL) {
		printf("打开文件失败\n");
        
		return 0;
	}
    
	fwrite(&stu, sizeof(struct Stu), 3, pf);

	fclose(pf);
	pf = NULL;

	return 0;
}
fwrite_FILE
fwrite_FILE
文件以记事本打开, 发现数据存在乱码, 那么究竟是不是二进制数据呢?
fwrite_READ
fwrite_READ
以二进制编辑器打开, 可以发现确实是二进制数据
fread 二进制输入
size_t fread(void* buffer, size_t size, size_t count, FILE* stream);
二进制输入与二进制输出相反, 可以将文件中的二进制数据, 输入到内存中(读取文件中的二进制数据)
fread 函数的参数表示的是:
  1. void* buffer: 需要输入内存的地址
  2. size_t size: 读取的文件中的数据的类型的大小
  3. size_t count: 读取的数据的个数
  4. FILE* stream: 需要读取的文件流
二进制输入(读取二进制数据)的示例:
#include <stdio.h>

struct Stu {
	char name[20];
	int age;
	char sex[10];
};

int main() {
	struct Stu stu[3] = {0};
    
	FILE* pf = fopen("data.txt", "rb");		//以二进制输入形式打开文件(读取文件的形式)
	// 打开的文件就是 上边的示例文件
	if (pf == NULL)
	{
		printf("打开文件失败\n");
        
		return 0;
	}
    
	fread(&stu, sizeof(struct Stu), 3, pf);

	printf("%s %d %s\n", stu[0].name, stu[0].age, stu[0].sex);
	printf("%s %d %s\n", stu[1].name, stu[1].age, stu[1].sex);
	printf("%s %d %s\n", stu[2].name, stu[2].age, stu[2].sex);

	fclose(pf);
	pf = NULL;

	return 0;
}
fread_FILE
fread_FILE

介绍过这些对文件顺序读写操作函数后, 可以发现 这些函数每执行一次, 文件指针 就会自然而然地按照排列顺序移动至下一个读写对象
这也就是为什么被称为顺序读写的原因

2.4 文件的随机读写

上面介绍的都是对文件进行顺序读写操作时, 能使用到的函数
被称为顺序读写操作, 是因为以上函数的每一次执行, 文件指针就会 按顺序移动至下一个需要读写对象
那么, 随机读写又是因为什么呢?
这里的随机并不是不可定的意思, 而是 不用按照顺序的的意思

2.4.1 定位(指定)文件指针

fseek()
int fseek(FILE *stream, long offset, int origin);
fseek 函数的功能是, 根据文件指针 的位置和偏移量 来定位文件指针(或 通过给定文件指针 的位置和偏移量 来指定文件指针的位置)
本函数的参数含义是:
  1. FILE *stream: 文件流

  2. long offset: 偏移量

    就是需要指定 文件指针 从初始位置偏移的位数

  3. int origin: 文件指针 开始偏移的初始位置

    此参数 C语言 给定了三个宏:

    SEEK_CUR

    文件指针当前在文件流内容中的位置

    即 不改变文件指针的位置, 使文件指针 从当前位置 开始偏移

    SEEK_END

    此文件流内容的末尾

    即 将文件指针指向文件流内容的末字符之后, 使文件指针 从文件流内容的末位 开始偏移

    SEEK_SET

    此文件流内容的开始

    即 将文件指针指向文件流内容的首位, 使文件指针 从文件流内容的首位 开始偏移

fseek函数到底如何使用呢?具体作用究竟是什么呢?:

首先, 我们先创建一个文件(我这里路径是 D:\TEST.txt ), 并输入内容

fseek_TEST
fseek_TEST

当我们不使用 fseek 函数时,

#include 
#include 
#include 

int main() {
  FILE* pf = fopen("D:\\TEST.txt", "r");
  if (pf == NULL) {
  	printf("fopen::%s", strerror(errno));
  	return 0;
  }
   
  int ch = 0;
  for (int i = 0; i < 10; i++) {
       // 进行 10 次循环 
  	ch = fgetc(pf);
  	printf("%c\n", ch);
  }

  fclose(pf);
  pf = NULL;

  return 0;
}

这段代码的运行结果是:

此时, 文件指针应该在 文件内容的 k 字符上

如果再使用ch = fgetc(pf), 并输出ch存入的字符, 将输出k

但是, 如果这时候我们使用 fseek 函数, 就可以将文件指针定位到文件内容的其他地方, 使文件指针指向的文件内容改变:

#include 
#include 
#include 

int main() {
   FILE* pf = fopen("D:\\TEST.txt", "r");
   if (pf == NULL) {
       printf("fopen::%s", strerror(errno));
       return 0;
   }

   int ch = 0;
   for (int i = 0; i < 10; i++) {
       // 进行 10 次循环 
       ch = fgetc(pf);
       printf("%c\n", ch);
   }

   fseek(pf, 10, SEEK_CUR);	//使文件指针, 从当前位置向后偏移 10 个字符
   //fseek(pf, 15, SEEK_SET);	//使文件指针, 从文件内容的首位, 向后偏移 15 个字符
   //fseek(pf, -5, SEEK_END);	//使文件指针, 从文件内容的末字符之后, 向后偏移 -5 个字符(向前偏移 5 个字符)

   ch = fgetc(pf);
   printf("ch = %c\n", ch);

   fclose(pf);
   pf = NULL;

   return 0;
}

fseek 函数, 三次使用的运行结果 分别为:

  1. fseek(pf, 10, SEEK_CUR);

    fseek_SEEK_CUR
    fseek_SEEK_CUR

    文件指针从当前位置向后偏移 10 个字符, 到 u

  2. fseek(pf, 15, SEEK_SET);

    fseek_SEEK_SET
    fseek_SEEK_SET

    文件指针从文件内容的首位, 想后偏移 15 个字符, 到 p

  3. fseek(pf, -5, SEEK_END);

    fseek_SEEK_END
    fseek_SEEK_END

    文件字符从文件内容的末字符之后, 向 前 偏移 5 个字符, 到 v


2.4.2 获取偏移量

因为对文件进行随机读写操作 可能造成操作者不知道 文件指针此时的位置
所以为了能够确定 文件指针此时指向的位置, 就可以使用 ftell 函数
ftell()
ftell 函数, 可以返回 文件指针相对于文件内容初始位置 的偏移量
long ftell(FILE *stream);
ftell 函数没有什么需要特别注意的地方, 了解一下如何使用就足够了:
#include 
#include 
#include 

int main() {
	FILE* pf = fopen("D:\\TEST.txt", "r");
	if (pf == NULL) {
		printf("fopen::%s", strerror(errno));
		return 0;
	}
    
	int ch = 0;
	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	long ret = ftell(pf);
	printf("%ld\n", ret);

	fclose(pf);
	pf = NULL;

	return 0;
}

上述代码的运行结果:

ftell_FILE
ftell_FILE

两次 fget(pf) 之后, 计算偏移量 为 2;

2.4.3 返回初始位置

rewind()
void rewind(FILE *stream);
rewind 函数可以将 文件指针 重新指向 文件内容的初始位置
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main() {
	FILE* pf = fopen("D:\\TEST.txt", "r");
	if (pf == NULL) {
		printf("fopen::%s", strerror(errno));
        
		return 0;
	}
    
	int ch = 0;

	ch = fgetc(pf);
	printf("%c\n", ch);

	ch = fgetc(pf);
	printf("%c\n", ch);

	long ret = ftell(pf);
	printf("%ld\n", ret);

	rewind(pf);
	ret = ftell(pf);
	printf("%ld\n", ret);

	fclose(pf);
	pf = NULL;

	return 0;
}

代码运行结果:

rewind_FILE
rewind_FILE

以上就是部分的文件操作函数, 并不是全部的文件操作函数, 但是文件操作函数就只介绍到这里
传统功夫, 以点到为止
如果想要了解 学习更多的 文件操作函数, 可以参考 Win32 API 或者 Cplusplus 等网站自行学习