C语言可以不用,但不能不会的——文件操作(附上高阶版本通讯录)

Posted 敲代码的布莱恩特

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言可以不用,但不能不会的——文件操作(附上高阶版本通讯录)相关的知识,希望对你有一定的参考价值。


🎄什么是文件

磁盘上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类)


🎅程序文件

包括源程序文件(后缀为.c)
目标文件(windows环境后缀为.obj)
可执行程序(windows环境后缀为.exe)


🎅数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。

本章讨论的是数据文件

  • 在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
  • 其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件

🎅文件名

一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\\code\\test.txt
为了方便起见,文件标识常被称为文件名


🎄文件类型

  • 根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
  • 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
  • 如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
  • 字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
(VS2013测试)。


🎄文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。


🎄文件的打开和关闭


🎅文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE.

例如,VS2008编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

          struct _iobuf 
          {
                  char *_ptr;
                  int   _cnt;
                  char *_base;
                  int   _flag;
                  int   _file;
                  int   _charbuf;
                  int   _bufsiz;
                  char *_tmpfname;
          };
          typedef struct _iobuf FILE;

不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节


一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:

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

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件
比如:


🎅文件打开与关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件

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

  • 该函数返回一个 FILE 指针。否则返回 NULL,且设置全局变量 errno 来标识错误
  • filename – 这是 C 字符串,包含了要打开的文件名称
  • mode – 这是 C 字符串,包含了文件访问模式,模式如下:
文件使用方式含义如果指定文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件建立一个新的文件
“a”(追加)向文本文件尾添加数据出错
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向一个二进制文件尾添加数据出错
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建议一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

int fclose ( FILE * stream );

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流
  • 如果流成功关闭,则该方法返回零。如果失败,则返回 EOF

代码示例:

 /* fopen fclose example */
#include <stdio.h>
int main ()
{
  FILE * pFile;
  pFile = fopen ("test.txt","w");
  if (pFile!=NULL)
 {
    fputs ("RIP KOBE",pFile);
    fclose (pFile);
 }
  return 0;
 }

结果如下:


🎄文件的顺序读写

功能函数名适用于
字符输入函数fgetc所有输入流
字符输出函数fputc所有输出流
文本行输入函数fgets所有输入流
文本行输出函数fputs所有输出流
格式化输入函数fscanf所有输入流
格式化输出函数fprintf所有输出流
二进制输入fread文件
二进制输出fwrite文件

接下来依次展示使用实例


🎅字符输入函数 fgetc

描述
C 库函数 int fgetc(FILE *stream) 从指定的流 stream 获取下一个字符(一个无符号字符),并把位置标识符往前移动

声明
int fgetc(FILE *stream)

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流。

返回值
该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF

使用实例


🎅字符输出函数 fputc

描述
C 库函数 int fputc(int char, FILE *stream) 把参数 char 指定的字符一个无符号字符)写入到指定的流 stream 中,并把位置标识符往前移动

声明
int fputc(int char, FILE *stream)

参数

  • char – 这是要被写入的字符。该字符以其对应的 int 值进行传递
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流

返回值
如果没有发生错误,则返回被写入的字符。如果发生错误,则返回 EOF,并设置错误标识符。

使用实例


🎅文本行输入函数 fgets

描述
C 库函数 char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。

声明
char *fgets(char *str, int n, FILE *stream)

参数

  • str – 这是指向一个字符数组的指针,该数组存储了要读取的字符串
  • n – 这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。

返回值
如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。

使用实例


🎅文本行输出函数 fputs

描述
C 库函数 int fputs(const char *str, FILE *stream) 把字符串写入到指定的流 stream 中,但不包括空字符。

声明
int fputs(const char *str, FILE *stream)

参数

  • str – 这是一个数组,包含了要写入的以空字符终止的字符序列
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。

返回值
该函数返回一个非负值,如果发生错误则返回 EOF。

使用实例


🎅格式化输入函数 fscanf

描述
C 库函数 int fscanf(FILE *stream, const char *format, …) 从流 stream 读取格式化输入

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

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format – 这是 C 字符串,包含了以下各项中的一个或多个:
    空格字符、非空格字符 和 format 说明符。
  • format 说明符形式为 [=%[*][width][modifiers]type=],具体讲解如下:
参数描述
*这是一个可选的星号,表示数据是从流 stream 中读取的,但是可以被忽视,即它不存储在对应的参数中。
width这指定了在当前读取操作中读取的最大字符数。
modifiers为对应的附加参数所指向的数据指定一个不同于整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小: h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x) l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g) L :长双精度型(针对 e、f 和 g)
type一个字符,指定了要被读取的数据类型以及数据读取方式。具体参见下一个表格。

fscanf 类型说明符:

类型合格的输入参数的类型
c单个字符:读取下一个字符。如果指定了一个不为 1 的宽度 width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。char *
d十进制整数:数字前面的 + 或 - 号是可选的。int *
e,E,f,g,G浮点数:包含了一个小数点、一个可选的前置符号 + 或 -、一个可选的后置字符 e 或 E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4float *
o八进制整数。int *
s字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。char *
u无符号的十进制整数。unsigned int *
x,X十六进制整数。int *
  • 附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同

返回值
如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF

使用实例


🎅格式化输出函数 fprintf

描述
C 库函数 int fprintf(FILE *stream, const char *format, …) 发送格式化输出到流 stream 中

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

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • format – 这是 C 字符串,包含了要被写入到流 stream 中的文本。它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。format 标签属性是 %[flags][width][.precision][length]specifier,具体讲解如下:
specifier(说明符)输出
c字符
d 或 i有符号十进制整数
e使用 e 字符的科学科学记数法(尾数和指数)
E使用 E 字符的科学科学记数法(尾数和指数)
f十进制浮点数
g自动选择 %e 或 %f 中合适的表示法
G自动选择 %E 或 %f 中合适的表示法
o有符号八进制
s字符的字符串
u无符号十进制整数
x无符号十六进制整数
X无符号十六进制整数(大写字母)
p指针地址
n无输出
%字符
  • 附加参数 – 根据不同的 format 字符串,函数可能需要一系列的附加参数,每个参数包含了一个要被插入的值,替换了 format 参数中指定的每个 % 标签。参数的个数应与 % 标签的个数相同。

返回值
如果成功,则返回写入的字符总数,否则返回一个负数。

使用实例


🎅二进制输入 fread

描述
C 库函数 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) 从给定流 stream 读取数据到 ptr 所指向的数组中

声明
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)

参数

  • ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针
  • size – 这是要读取的每个元素的大小,以 字节为单位
  • nmemb – 这是元素的个数每个元素的大小为 size 字节
  • stream – 这是 指向 FILE 对象的指针 ,该 FILE 对象 指定了一个输入流

返回值
成功读取的元素总数会以 size_t 对象返回,size_t 对象是一个整型数据类型。如果总数与 nmemb 参数不同,如果返回 0 则可能发生了一个错误或者到达了文件末尾。

使用实例


🎅二进制输出 fwrite

描述
C 库函数 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) 把 ptr 所指向的数组中的数据写入到给定流 stream 中。

声明
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

参数

  • ptr – 这是 指向要被写入的元素数组的指针
  • size – 这是 要被写入的每个元素的大小以字节为单位
  • nmemb这是元素的个数,每个元素的 大小为 size 字节
  • stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。

返回值
如果成功,该函数返回一个 size_t 对象,表示元素的 总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。

使用实例

🎄对比一组函数


🎄文件的随机读写

再说接下来的操作之前,先明确一个东西:
文件偏移量(刚打开的文件偏移量为0)


🎅fseek

描述
C 库函数 int fseek(FILE *stream, long int offset, int whence) 设置流 stream 的文件位置为给定的偏移 offset参数 offset 意味着从给定的 whence 位置查找的字节数

声明
int fseek(FILE *stream, long int offset, int whence)

参数

  • stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
  • offset – 这是相对 whence 的偏移量,以 字节为单位。
  • whence – 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量描述
SEEK_SET文件的开头
SEEK_CUR文件指针的当前位置
SEEK_END文件的末尾

返回值
如果成功,则该函数返回零,否则返回非零值。

使用实例


🎅ftell

描述
C 库函数 long int ftell(FILE *stream) 返回给定流 stream 的当前文件位置

声明
long int ftell(FILE *stream)

参数

  • stream– 这是指向 FILE 对象的指针,该 FILE 对象标识了流。

返回值
该函数返回位置标识符的当前值。如果发生错误,则返回 -1L,全局变量 errno 被设置为一个正值。

使用实例


🎅rewind

描述
C 库函数 void rewind(FILE *stream) 设置文件位置为给定流 stream 的文件的开头

声明
void rewind(FILE *stream)

参数
stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。

返回值
该函数 不返回任何值

使用实例


🎄文件结束的判定


❗❗❗❗❗被错误使用的 feof

牢记:在文件读取过程中,不能feof函数的返回值直接用来判断文件的是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

🐂1. 文本文件读取是否结束,判断返回值是否为EOF (fgetc),或NULL(fgets)

例如:

  • fgetc判断是否为EOF.
  • fgets判断返回值是否为NULL.

🐂2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:

  • fread判断返回值是否小于实际要读的个数

正确的使用:

1.文本文件的例子:

#include <stdio.h>
#include <stdlib.h>
int main(void) {
    int c; // 注意:int,非char,要求处理EOF
    FILE* fp = fopen("test.txt", "r");
    if(!fp) {
        perror("File opening failed");
        return EXIT_FAILURE;
   }
 //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
   { 
       putchar(c);
   }
 //判断是什么原因结束的
    if (ferror(fp))
        puts("I/O error when reading");
    else if (feof(fp))
        puts("End of file reached successfully");
    fclose(fp);
}

2.二进制文件的例子:

#include <stdio.h>
enum { SIZE = 5 };
int main(void) {
    double a[SIZE] = {1.0,2.0,3.0,4.0,5.0};
    double b = 0.0;
    size_t ret_code = 0;
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof(*a), SIZE, fp); // 写 double 的数组
    fclose(fp);
    fp = fopen("test.bin","rb");
    // 读 double 的数组
    while((ret_code = fread(&b, sizeof(double), 1, fp))>=1)
   {
        printf("%lf\\n",b);
   }
    if (feof(fp))
        printf("Error reading test.bin: unexpected end of file\\n");
    else if (ferror(fp)) {
        perror("Error reading test.bin");
   }
    fclose(fp);
    fp = NULL; }

🐂🐸通讯录高阶版(动态内存+自定义类型+文件)

contact.c

#include "contact.h"

//静态初始化
//void InitContact(struct Contact* pc)
//{
//	pc->sz = 0;//默认没有信息
//	memset(pc->data, 0, MAX*sizeof(struct PeoInfo));
//	memset(pc->data, 0, sizeof(pc->data));
//}

void CheckCapacity(struct Contact* pc)
{
	if (pc->sz == pc->capacity)
	{
		struct PeoInfo* ptr = (struct PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(struct PeoInfo));
		if (ptr != NULL)
		{
			pc->data = ptr;
			pc->capacity += 2;

			printf("增容成功\\n");
		}
		else
		{
			printf("增容失败\\n");
			return;
		}
		
	}
}

//加载数据
void LoadContact(struct Contact* pc)
{
	FILE*pf = fopen("contact.txt", "r");
	if (NULL == pf)
	{
		perror("LoadContact::fopen");
		return;
	}

	struct PeoInfo temp = { 0 };
	while(EOF != fscanf(pf, "%s %d %s %s %s", temp.name, &temp.age, temp.sex, temp.tele, temp.addr))
		{
			CheckCapacity(pc);
			pc->data[pc->sz] = temp;
			pc->sz++;
		}
	

	fclose(pf);
	pf = NULL;
}

//动态初始化
void InitContact(struct Contact* pc)
{
	pc->sz = 0;
	pc->data = (struct PeoInfo*)malloc(DEFAULT_SZ * sizeof(struct PeoInfo));
	pc->capacity = DEFAULT_SZ;//初始最大容量为3
	
							  
	//加载文件信息
	LoadContact(pc);
}

//静态添加
//void AddContact(struct Contact* pc)
//{
//	if (pc->sz == MAX)
//	{
//		printf("通讯录满了\\n");
//	}
//	else
//	{
//		printf("请输入名字:>");
//		scanf_s("%s", pc->data[pc->sz].name, 30);
//		printf("请输入年龄:>");
//		scanf_s("%d", &(pc->data[pc->sz].age));
//		printf("请输入性别:>");
//		scanf_s("%s", pc->data[pc->sz].sex, 5);
//		printf("请输入电话:>");
//		scanf_s("%s", pc->data[pc->sz].tele, 12);
//		printf("请输入地址:>");
//		scanf_s("%s", pc->data[pc->sz].addr, 30);
//
//
//		printf("添加成功\\n");
//		pc->sz++;
//		ShowContact(pc);
//	}
//}

//动态添加
void AddContact(struct Contact* pc)
{
	if (pc->sz == pc->capacity)
	{
		struct PeoInfo* ptr = (struct PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(struct PeoInfo));
		if (ptr != NULL)
		{
			pc->data = ptr;
			pc->capacity += 2;

			printf("增容成功\\n");
		}
		else
		{
			printf("增容失败\\n");
			return;
		}
		
	}
	
	//录入新增人的信息
	    printf("请输入名字:>");
		scanf_s("%s", pc->data[pc->sz].name, 30);
		printf("请输入年龄:>");
		scanf_s("%d", &(pc->data[pc->sz].age));
		printf("请输入性别:>");
		scanf_s("%s", pc->data[pc->sz].sex, 5);
		printf("请输入电话:>

以上是关于C语言可以不用,但不能不会的——文件操作(附上高阶版本通讯录)的主要内容,如果未能解决你的问题,请参考以下文章

C 语言能不能在头文件定义全局变量?

C语言文件的基础操作

用C语言实现三子棋游戏(附上思路+项目展示+源代码)

C语言文件操作,请高手指点

求一个c语言程序能输出一个大的爱心;用符号拼成的不能用printf。

推荐几个用心分享的C语言嵌入式相关公众号