C语言拯救者(数据文件的使用与操作,流的概念- -14)

目录

1. 什么是文件

1.1 程序文件

1.2 数据文件

2 文件的打开和关闭

2.1 文件指针

2.2 文件的打开方式

2.3 文件的使用方法及注意事项

2.4 以只读的形式打开文件

 2.5 以只写的形式打开文件

如果已有原文件,文件中存在内容时。用只写“w”的方式打开文件后,原文件内容会被销毁

3. 文件的顺序读写

3.1 以“字符输出函数”从程序中写入数据存放到文件中

 3.2 以“字符输入函数”从文件中读取数据,放到程序中

3.3 流

你可以把流理解为水流,流其实是一种信息的转换

3.4文本行输出函数:fputs

3.5 文本行输入函数:fgets

3.6 格式化输出函数:fprintf

3.7 格式化输入函数:fscanf

3.8 二进制输出函数:fwrite

 3.9 二进制输入函数:fread

4. 对比函数面试题

5.  文件的随机读写

5.1 fseek:根据文件指针的位置和偏移量来定位文件指针

5.2 ftell:返回文件指针相对于起始位置的偏移量

5.3 让文件指针的位置回到文件的起始位置:rewind        

6. 文本文件和二进制文件 

7. 文件读取结束的判定

 7.1 被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。

8. 文件缓冲区


1. 什么是文件

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

一个文件要有一个唯一的文件标识(文件名),以便用户识别和引用。

文件名包含3部分:文件路径+文件名主干+文件后缀   例如: c:\code\test.txt

1.1 程序文件

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

1.2 数据文件

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

我们写出了test.c程序,其中程序操作了文件,程序在文件中进行读写修改。文件存放了程序所写入的数据、或者拿取数据到程序中,这个文件就被称为数据文件


2 文件的打开和关闭

2.1 文件指针

每个被使用的文件(只要操作了文件)都在内存中开辟了一个相应的文件信息区(和文件强制关联),用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.

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

我们可以创建一个FILE*的指针变量来维护这个FILE结构的变量,使pf指向某个文件的文件信息区,通过文件指针变量能够找到与它关联的文件。在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

FILE* pf;//文件指针变量,pf是一个指向FILE类型数据的指针变量

2.2 文件的打开方式

文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。

 注意:每当fopen打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构变量的文件信息区(内存),返回文件信息区的起始地址FILE*指针变量中,并填充其中的信息, 使用者不必关心细节。

FILE *fopen( const char *filename, const char *mode );//打开文件,mode指打开文件方式

int fclose( FILE *stream );//关闭文件

2.3 文件的使用方法及注意事项

 当我们尝试打开一个文件信息区,完全有可能打开失败,打开失败时fopen会返回空指针,所以不能是下面这种写法。我们必须得判断接收的该指针是否为NULL,否则会造成非法访问内存

FILE* pf = fopen("test.txt", "r");

正确的写法:

int main()
{
	FILE* pf = fopen("test.txt", "r");
 // FILE* pf = fopen("D:\\code\\test.txt", "r");//想更改路径也可以
 //路径分隔符“\”在 c 语言里面是转义字符,所以表达路径分隔符需要用“\\”
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//打开文件成功,读文件"r";

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

2.4 以只读的形式打开文件

 如果在我们创建的路径下并没有test.txt文件,打开失败

如果在路径下存在test.txt文件,再次运行代码,可以看到代码成功运行

 

 2.5 以只写的形式打开文件

int main()
{
	//打开文件
	FILE* pf = fopen("test1.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	
	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

 如果在我们创建的路径下并没有test1.txt文件,创建一个新文件

如果已有原文件,文件中存在内容时。用只写“w”的方式打开文件后,原文件内容会被销毁

运行代码后


3. 文件的顺序读写

功能 函数名 适用于
字符输入函数(从文件中读取,放到程序中) fgetc 所有输入流
字符输出函数(从程序中写入数据放到文件) fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件

3.1 以“字符输出函数从程序中写入数据存放到文件中

int fputc( int c, FILE *stream );//c是要写的字符

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件 - 输出操作
	//abcdef
	char ch = 'a';
	for (ch = 'a'; ch <= 'z'; ch++)
	{
		fputc(ch, pf);
	}

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

此时,文件中被存放a~z的数据


 3.2 以“字符输入函数从文件中读取数据,放到程序中

int fgetc( FILE *stream );
//如果读取成功,返回该字符的ASCII码值
//如果读取失败,返回的是EOF

 

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件 - 输入操作
	int ch = 0;
	while((ch = fgetc(pf)) != EOF)
	{
		printf("%c ", ch);
	}

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

 可以看到,我们刚刚所写的a~z数据被打印到屏幕上


3.3 流

你可以把流理解为水流,流其实是一种信息的转换

例如电脑上有各种各样的外部设备(键盘,鼠标,屏幕,网卡....),我们在操作不同的外部设备时,操作读写的方法也各不相同。如果要求程序员掌握所有接入外部设备的操作方法,学习成本是十分巨大的,流的概念应运而生。

不知道外部设备操作如何操作,C语言在外部设备上层高度封装了一个“流”,我们只需要关注流。读写数据时都放入流中,我们只需要从流拿取即可

通常我们把对象接收外界的信息输入(stdin)称为标准输入流(键盘),相应地从对象向外输出(stdout)信息为标准输出流(屏幕),标准错误流(stderr)。只要C语言程序运行,三个流(FILE*)默认开启。而我们要读写文件时打开的又是一个文件流

可以把流看作是一种数据的载体,通过它可以实现数据交换和传输。

所以,我们也可以从标准输入流(键盘)获取信息,再通过标准输出流(屏幕)打印信息

int main()
{
	int ch = fgetc(stdin);
	fputc(ch, stdout);

	return 0;
}

3.4文本行输出函数:fputs

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

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件 - 写一行
	fputs("qwertyuiop\n", pf);
	fputs("xxxxxxxxxx\n", pf);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

如果原文件中存在数据,以“w”形式写入后会销毁原数据,并按照所写的字符串格式输入进文件

3.5 文本行输入函数:fgets

char *fgets( char *string, int n, FILE *stream );
//n是读取字符的最大个数,实际读取字符个数n-1,预留空间给\0

fgets从FILE*读取n-1个字符,存放到string中,同时返回string地址,遇到错误或者文件结束返回NULL

int main()
{
	char arr[256] = "XXXXXX";
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件 - 读一行

	while(fgets(arr, 256, pf) != NULL)//判断fgets是否返回NULL
	{
		printf("%s", arr);
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}


3.6 格式化输出函数:fprintf

我们先来看printf函数的参数

int printf( const char *format [, argument]... );

 fprint函数的参数只比printf函数多了一个流(文件信息区)

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

把结构体中的数据存放到文件中:

struct S
{
	char name[20];
	int age;
	double d;
};

int main()
{
	struct S s = { "张三", 20, 95.5 };
	//打开文件
	FILE* pf = fopen("test2.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//写文件
	fprintf(pf, "%s %d %lf", s.name, s.age, s.d);
	
	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}	

3.7 格式化输入函数:fscanf

同理,要想知道fscanf,我们先查看scanf函数的参数

int scanf( const char *format [,argument]... );

 多了一个流(文件信息区)

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

fscanf的作用是从流中读取一个类型为%s %d %lf的数据,存放到struct s的每个成员中

struct S
{
	char name[20];
	int age;
	double d;
};

int main()
{
	struct S s = {0};
	//打开文件
	FILE* pf = fopen("test2.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//读文件
	fscanf(pf, "%s %d %lf", s.name, &(s.age), &(s.d));
	//printf("%s %d %lf\n", s.name, s.age, s.d);//打印出来的就是张三....带有格式的数据
	fprintf(stdout, "%s %d %lf\n", s.name, s.age, s.d);//打印到屏幕上

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}


3.8 二进制输出函数:fwrite

size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
//buffer指需要读写的数据,size指元素的大小,count指最多几个元素要被写入

数据可以按照二进制进行读写

struct S
{
	char name[20];
	int age;
	double d;
};

int main()
{
	struct S s = { "张三", 20, 95.5 };
	//写文件 -二进制的方式写
	FILE* pf = fopen("test3.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//二进制的方式写文件
	fwrite(&s, sizeof(struct S), 1, pf);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

打开文件,你会发现看不懂数据 ,因为数据以二进制方式写入,张三是因为张三是按照文本或者二进制形式放进去,结果一样。注意:只是记事本不了解数据(记事本以文本形式读取)

 3.9 二进制输入函数:fread

size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
//从流里读取数据放到buffer中,最多读count个,每个size大小
//返回类型size_t代表是返回实际读到的个数

我们以二进制方式读取文件:

struct S
{
	char name[20];
	int age;
	double d;
};

int main()
{
	struct S s = {0};
	//读文件 -二进制的方式读
	FILE* pf = fopen("test3.txt", "rb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//二进制的方式读
	fread(&s, sizeof(struct S), 2, pf);

	printf("%s %d %lf\n", s.name, s.age, s.d);

	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

所以,我们可以了解为什么当你打开文件会是一堆乱码,看都看不懂的情况发生了。按理来说,我们只需要知道对应的结构体类型,通过类型转化即可破解二进制乱码


4. 对比函数面试题

对比两组函数:

scanf/fscanf/sscanf

printf/fprintf/sprintf


sscanf:从一个字符串中转换成格式化的数据

int sscanf( const char *buffer, const char *format [, argument ] ... );
//buffer指从哪个字符串中提取,format指放到哪个数据中去

sprintf:把一个格式化的数据转换成字符串

int sprintf( char *buffer, const char *format [, argument] ... );
//buffer是存储(输出)的位置

  这个代码的意思:创建一个buf字符串,通过sprintf把格式化的结构体数据转换成字符串,放进buf中。

创建一个struct s  tmp变量,再通过sscanf把buf中的字符串提取成结构体数据类型。

struct S
{
	char name[20];
	int age;
	double d;
};

int main()
{
	char buf[256] = { 0 };
	struct S s = { "zhangsan", 20, 95.5 };
	struct S tmp = { 0 };

	sprintf(buf, "%s %d %lf", s.name, s.age, s.d);

	printf("%s\n", buf);//以字符串形式打印

	//从buf字符串中提取结构体数据
	sscanf(buf, "%s %d %lf", tmp.name, &(tmp.age), &(tmp.d));
	printf("%s %d %lf", tmp.name, tmp.age, tmp.d);//以格式化的形式打印

	return 0;
}

总结:

scanf:格式化的输入函数,针对标准输入流

fscanf:针对所有输入流的格式化输入函数

sscanf:把一个字符串转换成格式化的数据

printf:格式化的输出函数,针对标准输出流

fprintf:针对所有输出流的格式化输出函数

sprintf:把一个格式化的数据转换成字符串

sscanf的应用场景:前端网页获取的数据(字符串 )交给后端,后端C语言封装成了一个结构体,如何把字符串转换为结构体,就需要用到sscanf,后端转前端同理


5.  文件的随机读写

5.1 fseek:根据文件指针的位置和偏移量来定位文件指针

文件打开默认从第一个位置开始读写,有个文件指针指向起始位置。每当写入数据时,文件指针也会发生变化。如果我们向指定行列读写,需要用到fseek函数

int fseek( FILE *stream, long offset, int origin );
//offset指偏移量,origin指起始位置

fseek函数根据文件指针的位置和偏移量来定位文件指针

其中origin起始位置有三个选项:

1.SEEK_CUR-文件指针当前的位置

2.SEEK-END-文件末尾的位置

3.SEEK_SET-文件开始的位置

 

 通过指针的偏移量来更改所需字符的位置,同时在计算偏移时,起始位置可以更换

你可以在test.txt文件中写入数据,用fseek来测试


int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//随机读
	int ch = fgetc(pf);//a
	printf("%c\n", ch);

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

	fseek(pf, 5, SEEK_SET);
    //fseek(pf,-1,SEEK_END);

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

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}

5.2 ftell:返回文件指针相对于起始位置的偏移量

long ftell( FILE *stream );

 传递一个文件指针,返回


5.3 让文件指针的位置回到文件的起始位置:rewind        

void rewind( FILE *stream );

int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	//随机读
	fputc('a', pf);
	fputc('b', pf);
	fputc('c', pf);
	fputc('d', pf);
	//abcd
	fseek(pf, -3, SEEK_CUR);
	fputc('w', pf);
	//awcd
	long pos = ftell(pf);//long类型接收
	printf("%ld\n", pos);//现在指针指向了c,偏移量是2

	rewind(pf);//回到0偏移量

	pos = ftell(pf);
	printf("%ld\n", pos);//0

	//关闭文件
	fclose(pf);
	pf = NULL;

	return 0;
}


6. 文本文件和二进制文件 

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。一个数据在内存中是怎么存储的呢? 字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

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

 测试代码:

#include <stdio.h>
int main()
{
	int a = 10000;
	FILE* pf = fopen("test.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}

	fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
	fclose(pf);
	pf = NULL;
	return 0;
}

 虽然文件中是看不懂的,但是当我们把文件放到vs中

 

 

 


7. 文件读取结束的判定

 7.1 被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。

而是应用于已经知道文件读取结束,但是不知道因为什么而结束,结束的原因有两种情况:读取失败异常结束,还是遇到文件末尾正常结束。

应该用下列函数来判断是否结束:

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

例如: fgetc 判断是否为 EOF(读取结束) .

fgets 判断返回值是否为 NULL(读取结束) .

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

例如: 由于fread函数返回类型是size_t,如果返回的个数小于要求读取的个数,代表读取结束。fread判断返回值是否小于实际要读的个数。

文本文件的例子

#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))//ferror作用是:如果返回非0,表示文件读取发生错误,而结束
        puts("I/O error when reading");
    else if (feof(fp))//判断是否遇到文件末尾(EOF)而结束,如果feof返回非0,表示正常结束
        puts("End of file reached successfully");
    fclose(fp);
}

二进制文件的例子:

#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
    double a[SIZE] = {1.,2.,3.,4.,5.};
    FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式
    fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
    fclose(fp);
    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
    if(ret_code == SIZE) {
        puts("Array read successfully, contents: ");
        for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
        putchar('\n');
   } else { // error handling
       if (feof(fp))//是否文件末尾而结束
          printf("Error reading test.bin: unexpected end of file\n");
       else if (ferror(fp)) {//是否遇到错误而结束
           perror("Error reading test.bin");
       }
   }
    fclose(fp);
}

8. 文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。

从内存向磁盘(文件)输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到输出缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

原因:如果一个字符放一次,频繁打扰操作系统,这时候就有了缓冲区概念,当缓冲区数据放满后,再交给操作系统,效率提高

VS2013 WIN10环境测试缓冲区真实存在:

#include <windows.h>
int main()
{
	FILE* pf = fopen("test.txt", "w");
	fputs("abcdef", pf);//先将代码放在输出缓冲区

	printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
	Sleep(20000);//睡眠10秒

	printf("刷新缓冲区\n");
	fflush(pf);//主动刷新缓冲区时,将输出缓冲区的数据写到文件(磁盘)
	//注:fflush 在高版本的VS上不能使用了
	printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
	Sleep(10000);//让我们知道是fflush刷新了缓冲区而不是fclose

	fclose(pf);
	//注:fclose在关闭文件的时候,也会刷新缓冲区
	pf = NULL;
	return 0;
}

 当sleep十秒期间打开文件,你会发现并没有数据

当我们执行了fflush后,再次打开文件,会发现文件数据存在

 

所以当不正常关闭程序时,fclose有可能不会把缓冲区内容放到硬盘上,造成数据丢失

猜你喜欢

转载自blog.csdn.net/weixin_63543274/article/details/124087038
c14