十、文件操作--附代码案例

10.1 概述

问题:程序运行时产生的数据都属于临时数据,不管是堆区还是栈区产生的数据,程序一旦运行结束都会被释放,数据没有保存。
解决:通过文件把数据记录在文件中,可以将数据持久化。
所有程序(例如exe又叫可执行文件)、操作系统文件、普通文件(记事本、word、excel、mp3等)都是以文件形式存储。

有些电脑病毒是没有界面的,伪装成服务的形式存储(在任务管理器的服务)。比如在使用输入法,输入法对应的服务监听到按键后启动输入法,某些病毒会入侵输入法服务,用户在登陆账号时,通过输入法服务获取到数据。因此在未知安全的电脑输入账号密码时,建议用软键盘输入相对安全。

10.1.1 文件分类

系统盘:存放系统启动的内容。
作用:承上启下,向上提供可视化窗口,向下提供接口封装,把所有硬件内容都封装起来。
硬关机:强制给电脑断电
。如果电脑卡死动不了,按电源键5秒可以强制关机(台式机还可以直接拔电源线)。但是卡机过程中磁磁针仍然高速转动进行读写磁盘,突然断电会导致读写中的数据突然中止,数据不但没有保存,而且损伤磁盘。

文件分类定义存储位置特点
磁盘文件一组相关数据的有序集合存储在外部介质(如磁盘、U盘)上使用时才调入内存
设备文件操作系统中每一个与主机相连的输入、输出设备(设备管理器的所有硬件对应的驱动)存储在系统盘上(一般是C盘)每一个与主机相连的输入、输出设备的输入、输出等同于对磁盘文件的读和写;设备文件在Windows下是看不见的

10.1.2 磁盘文件的分类

计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储,从用户或者操作系统使用的角度(逻辑上)把磁盘文件分为:
在这里插入图片描述

磁盘文件分类定义举例特点常见编码
文本文件基于字符编码的文件记事本可用文本编辑器直接打开ASCII、UNICODE
二进制文件基于二进制编码的文件word、excel、图片、视频文本编辑器直接打开会乱码二进制

数5678以ASCII存储(8421法,每个字符占领8位)形式为:00110101 00110110 00110111 00111000,这里的5是’5’,ASCII值是53。
数5678以二进制存储形式为:0001 0110 0010 1110。
对比可知,二进制文件比文本文件占用磁盘更少。

10.2 文件的打开和关闭

10.2.1 文件指针FILE

定义:文件时基于指针操作的,在C语言中用一个指针变量指向一个文件,这个指针称为文件指针FILE。
说明:文件指针是系统封装好的一个结构体,文件指针的类型是结构体指针。

// FILE结构中含有文件名、文件状态和文件当前位置等信息
typedef struct{
	short           level;		//缓冲区"满"或者"空"的程度 
	unsigned        flags;		//文件状态标志 
	char            fd;			//文件描述符
	unsigned char   hold;		//如无缓冲区不读取字符
	short           bsize;		//缓冲区的大小
	unsigned char   *buffer;	//数据缓冲区的位置 
	unsigned        ar;			//指针,当前的指向 
	unsigned        istemp;		//临时文件,指示器
	short           token;		//用于有效性的检查 
}FILE;
// FILE是系统使用typedef定义
// 在IDE输入printf,右击转入定义,发现FILE属于stdio.h
// 在IDE输入FILE,右击转入定义,发现FILE属于corecrt_wstdio.h
// 在stdio.h上方可看到,stdio.h导入头文件corecrt_wstdio.h,因此使用文件指针FILE,导入stdio.h即可,不需要导入corecrt_wstdio.h

10.2.2 文件的打开

函数名FILE* fopen(const char* filename, const char* mode)
头文件include <stdio.h>
参数filename:需要打开的文件名,根据需要加上路径(一般写上绝对路径)
mode:打开文件的模式设置
功能打开文件
返回值成功:文件指针
失败:NULL
说明①任何文件使用之前必须打开;
②打开1个文件,会将文件放在缓冲区中。
打开文件失败原因①找不到文件;
②文件权限;
③程序打开文件超出上限pow(2,16)-1=65535。
	//用两个反斜杠"\\"或者1个正斜杠"/"表示,不要用1个反斜杠"\"表示,因为1个反斜杠跟接下来的1个字符会变成转义字符(比如"\n")
	// "\\"这样的路径形式,只能在windows使用
	// "/"这样的路径形式,windows和linux平台下都可用,建议使用这种
	//参数1的几种形式:
	//FILE* fp = fopen("open.txt", "r");			//相对路径:打开当前项目工程目录下的源文件(.c文件)的同级目录下的open.txt文件
	//FILE* fp = fopen("./open.txt", "r");			//相对路径:打开当前目录的open.txt文件(./表示当前目录)
	//FILE* fp = fopen("../Popen.txt", "r");		//相对路径:打开当前目录的上一级目录的Popen.txt文件(../表示上一级目录)
	FILE* fp = fopen("F:/open.txt", "r");			//绝对路径:打开F盘目录下open.txt文件
	//设置判断原因:文件打开失败的概率比较大,写个判断用于提示打开失败的情况,防止返回空指针,最后关闭文件时操作空指针而报错
	//堆空间的开辟不写判断原因:因为内存足够大,只要不是开辟特别大的内存空间,都可以不写判断
	if (fp == NULL) {								//返回空,说明打开失败
		printf("fail to open file\n");
		//perror("open");							//perror()是标准出错打印函数,能打印调用库函数出错原因
		return -1;
	}
	printf("文件打开成功,文件指针地址为%p\n",fp);
	fclose(fp);	//关闭后fp变成野指针,可以在释放后令fp = NULL,但是程序快结束了,写不写都没什么意义

FILE* fp = fopen("open.txt", "r");
在这里插入图片描述
FILE* fp = fopen("./open.txt", "r");
在这里插入图片描述
FILE* fp = fopen("../Popen.txt", "r");
在这里插入图片描述
FILE* fp = fopen("F:\open.txt", "r");//没有F盘,打开失败
在这里插入图片描述

参数2的打开模式含义
r或rb以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)
w或wb以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a或ab以追加方式打开文件,在末尾添加内容(若文件不存在则创建文件)
r+或rb+以可读、可写的方式打开文件(不创建新文件)
w+或wb+以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a+或ab+以添加方式打开文件,打开文件并在末尾更改文件(若文件不存在则创建文件)
注意:
	b表示二进制模式,b只是在Windows有效,在Linux用r和rb的结果是一样的;
	在Windows平台下,以“文本”方式打开文件,不加b;
	Unix和Linux所有文本文件行都是\n结尾,而Windows所有文本文件行都是\r\n结尾;
	当读取文件的时候,系统会将所有的 "\r\n" 转换成 "\n";
	当写入文件的时候,系统会将 "\n" 转换成 "\r\n" 写入 ;
	以二进制方式打开文件,则读写\n都不会进行这样的转换。

10.2.3 文件的关闭

类似堆空间的开辟和释放:
①任何文件在使用后应该养成习惯手动关闭,如果总是打开文件不关闭,会占用内存资源。
②一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败。
③如果没有调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭。
④指针fp在文件中表示光标流,文件在读取的时候光标流会自动向下移动,所以不能更改光标流位置,否则关闭文件会报错 。

函数名int fclose(FILE * stream)
头文件include <stdio.h>
参数stream:文件指针,关闭后fp变成野指针,可以在释放后令fp = NULL
功能关闭由fopen()打开的文件指针,释放缓冲区资源
返回值成功:0
失败:-1

10.3 文件的顺序读写

10.3.1 按照字符读写文件fgetc、fputc

C语言中有三个特殊的文件指针由系统默认打开,用户无需定义即可直接使用:

文件指针说明使用
stdin标准输入,默认为当前终端(键盘)scanf、fgetc函数默认从此终端获得数据
stdout标准输出,默认为当前终端(屏幕)printf、fputc函数默认输出信息到此终端
stderr标准出错,默认为当前终端(屏幕)perror函数默认输出报错信息到此终端

10.3.1.1 读文件fgetc

函数名int fgetc(FILE* stream)
头文件include <stdio.h>
参数stream:文件指针
功能从stream指定的文件中读取一个字符
返回值成功:读取到的字符
失败:-1
	FILE* fp = fopen("F:/open.txt", "r");	//F盘没有该文件
	if (!fp) {
		perror("FileNotFound");				//标准出错,perror函数默认输出报错信息到此屏幕
		return -1;
	}
	printf("文件打开成功");
	fclose(fp);

在这里插入图片描述

	FILE* fp = fopen("D:/open.txt", "r");
	if (!fp) {
		perror("FileNotFound");	// 标准出错,perror函数默认输出报错信息到此屏幕
		return -1;
	}
	printf("%c", fgetc(fp));	//不管是读取还是写入,文件指针都会自动往下走
	//fp++;						//error,不能修改文件指针,文件在读取时候光标流会自动向下移动
	printf("%c", fgetc(fp));	//1个中文都是由2个负数的ASCII值组合
	fclose(fp);

在这里插入图片描述

10.3.1.2 文件结尾

C语言的文件结尾全称使用范围说明
EOF或者-1End Of File文本文件在文本文件中,数据都是以字符的ASCII码值的形式存放,字符的ASCII值的范围是0~127,不可能出现-1(汉字的ASCII是负数,但不是-1),且在stdio.h中有宏定义#define EOF (-1)
feof函数二进制文件、文本文件当把数据以二进制形式存放到文件中时,就会有-1值的出现,因此不能采用EOF作为二进制文件的结束标志。为解决这一个问题,ANSI C提供一个feof函数,用来判断文件是否结束
函数名int feof(FILE * stream)
头文件include <stdio.h>
参数stream:文件指针
功能检测是否读取到了文件结尾。判断的是最后一次“读操作的内容”,不是当前位置内容(上一个内容)
返回值非0值:已经到文件结尾
0:没有到文件结尾

问题:新建并打开txt记事本文件,查看文件右下角,会显示编码格式。如下图,一般是UTF-8的编码格式。用循环和fgetc函数去读取UTF-8编码的记事本文件时,如果里面有中文,中文在屏幕终端显示为乱码。
在这里插入图片描述
解决:点击左上角文件→另存为。
在这里插入图片描述
修改名字→点击编码→选择ANSI→保存。
在这里插入图片描述
打开文件,可看到右下角的编码格式变为ANSI。此时用循环和fgetc函数去读取ANSI编码的记事本文件时,无论是否为中文字符,都能原样显示在屏幕终端
在这里插入图片描述
利用fgetc函数把文件的内容一次性读取(对比后面的fgets函数的写法):

	FILE* fp = fopen("D:/openANSI.txt", "r");	//ANSI编码保存文本文件打开不乱码
	//FILE* fp = fopen("D:/openUTF-8.txt","r");	//UTF-8编码保存文本文件打开会乱码
	if (!fp) {
		perror("FileNotFound");	// 标准出错,perror函数默认输出报错信息到此屏幕
		return -1;
	}
	//char ch = fgetc(fp);
	//while((ch != -1)			//不能写成这样,否则光标流保持不变,陷入死循环
	char ch;
	while ((ch = fgetc(fp)) != -1)
		//printf("%c\n", ch)	//error,中文占两个字符,因为fgetc函数一次只能输出1个字符,如果换行输出会把两个相连的字符拆开导致乱码
		printf("%c", ch);
	//printf("%c", fgetc(fp));	//error,光标流向下移动,少输出一个字符
	fclose(fp);

FILE* fp = fopen("D:/openANSI.txt", "r"); //ANSI编码保存文本文件打开不乱码
在这里插入图片描述
//FILE* fp = fopen("D:/openUTF-8.txt","r"); //UTF-8编码保存文本文件打开会乱码
在这里插入图片描述

10.3.1.3 写文件fputc

函数名int fputc(int ch, FILE* stream)
头文件include <stdio.h>
参数ch:需要写入文件的字符
stream:文件指针
功能将ch转换为unsigned char后写入stream指定的文件中
返回值成功:成功写入文件的字符
失败:-1
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fileGetChar() {
	FILE* fp = fopen("D:/fputc.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char ch;
	while ((ch = fgetc(fp)) != -1)
		printf("%c", ch);
	fclose(fp);
	return 0;
}

int main() {
	FILE* fp = fopen("D:/fputc.txt", "w");			//以写的方式打开的的文件默认编码是ANSI
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char str[] = "通过循环和fputc将内容写入文本中,再用封装好的函数,调用fgetc读取";
	for(int i =0; str[i] != 0; i++){
		if (fputc(str[i], fp) == -1) {	//写入失败
			perror("CreateFileFail");
			return -1;
		}
	}
	//fileGetChar()						//error,因为前面已经通过循环+fetc函数将文件指针指向文件末尾,如果在关闭文件之前执行该函数,屏幕终端无法显示内容
	fclose(fp);
	fileGetChar();						//该步骤必须放在关闭文件之后,文件指针重新回到文件开始
	return 0;
}

运行代码前,D盘下没有fputc.txt文件。
在这里插入图片描述
运行代码后,可以发现D盘下产生fputc.txt文件。打开fputc.txt文件,文件内容写入成功,文件编码格式为ANSI,也再次证明ANSI编码格式下在终端屏幕显示中文不乱码。
在这里插入图片描述
文件编码格式默认为UTF-8的情况:

	①右键新建记事本文件,此时新建的记事本文件编码格式默认为UTF-8;
	②已存在编码格式为UTF-8记事本文件,将里面的内容读取出来。接着以"w"模式打开新的记事本文件,并写入到新文件,此时新的记事本文件编码格式默认为UTF-8。

文件编码格式默认为ANSI的情况:

	①以"w"模式打开的记事本文件,此时新建的记事本文件编码格式默认为ANSI;
	②已存在编码格式为ANSI记事本文件,将里面的内容读取出来。接着以"w"模式打开新的记事本文件,并写入到新文件,此时新的记事本文件编码格式默认为ANSI。

案例: 利用scanf函数在终端输入内容,再使用fgetc函数将内容保存在文件中。
将fputc.txt编码格式修改为UTF-8,并另存为fputc.txt,覆盖原来格式为ANSI的文件。运行如下代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fileGetChar() {
	FILE* fp = fopen("D:/fputc.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char ch;
	while ((ch = fgetc(fp)) != -1)
		printf("%c", ch);
	fclose(fp);
}

int main() {
	FILE* fp = fopen("D:/fputc.txt", "w");			//此时编码格式是ANSI
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char str;
	//while(ch != '@'){								//error,未初始化的局部变量
	while (1) {
		scanf("%c", &str);
		if (str == '@')								//选个写代码不常用字符,且在ASCII码有的,也就是'@',作为文件结束符退出循环
			break;
		if (fputc(str, fp) == -1) {					//写入失败
			perror("WriteFileFail");
			return -1;
		}
	}
	fclose(fp);
	fileGetChar();
	return 0;
}

发现原来格式为UTF-8的fputc.txt,用以写的方式打开,原来的内容被清空,并且编码格式修改为ANSI。
在这里插入图片描述
加密方式:①利用ASCII表加密;②密钥加密;③md5加密:任何东西都可以加密成一个md5码(32位大小写英文+数字),一般用于密码传输;
普通加密与原理:因为字符的本质是1个字节的整形,利用ASCII对照表,对原来文本文件的内容的每个字符有规律增加或者减去1个任意整数,再次对照ASCII生成新的文本内容。
注意:不能搭配随机数使用,否则没有统一的标准,后面无法解密。
适用范围:对于英文字符,其每个字符ASCII值加1,把字母往后挪一位,比如abcd123变成bcde234,但是这种加密方式太简单,容易破解。对于中文字符,它由两个字符(ASCII)组成,每个ASCII加1后变成乱码,不容易识别。因此中文的普通加密效果会更好。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fileGetChar() {
	FILE* fp = fopen("D:/lock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char ch;
	while ((ch = fgetc(fp)) != -1)
		printf("%c", ch);
	fclose(fp);
}

int main() {
	FILE* fr = fopen("D:/openUTF-8.txt", "r");
	//FILE* fr = fopen("D:/openANSI.txt", "r");	//后面用ANSI文件打开对比一下。发现有一个奇怪的现象,打开编码格式为ANSI的记事本文件,如果文件内容含有“件”字,后面直接无法写入全部内容??
	FILE* fw = fopen("D:/lock.txt", "w");		//此时编码格式是ANSI
	if (!fr || !fw) {							//等价于if (!(fr && fw))
		perror("FileNotFound");
		return -1;
	}
	char ch;
	while ((ch = fgetc(fr)) != -1){
		ch++;//英文字符的ASCII加1,只是把字母往后挪一位(比如abcd123变成bcde234),这种加密方式太简单,但是中文由两个ASCII组成,ASCII加1后变成乱码,加密效果更好。
		if (fputc(ch, fw) == -1) {		//写入失败
			perror("WriteFileFail");
			return -1;
		}
	}
	fclose(fr);
	fclose(fw);
	fileGetChar();
	return 0;
}

FILE* fr = fopen("D:/openUTF-8.txt", "r");加密效果如下:
在这里插入图片描述
//FILE* fr = fopen("D:/openANSI.txt", "r"); 加密效果如下:
在这里插入图片描述
FILE* fr = fopen("D:/openANSI.txt", "r");有一个奇怪的现象,打开编码格式为ANSI的记事本文件,如果文件内容含有“件”字,后面直接无法写入全部内容??
在这里插入图片描述
普通加密的解密原理:首先要知道原来的加密的方式是变化了几个ASCII值,然后反向推理即可。
举例:原来每个字符通过加1进行加密,解密就每个字符减1即可。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void fileGetChar() {
	FILE* fp = fopen("D:/unlock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char ch;
	while ((ch = fgetc(fp)) != -1)
		printf("%c", ch);
	fclose(fp);
}

int main() {
	FILE* fr = fopen("D:/lock.txt", "r");
	FILE* fw = fopen("D:/unlock.txt", "w");	//此时编码格式是UTF-8
	if (!fr || !fw) {
		perror("FileNotFound");
		return -1;
	}
	char ch;
	while ((ch = fgetc(fr)) != -1){
		ch--;							
		if (fputc(ch, fw) == -1) {
			perror("WriteFileFail");
			return -1;
		}
	}
	fclose(fr);
	fclose(fw);
	fileGetChar();
	return 0;
}

如果以写的方式打开文件,写入的内容是其他文件读取出来的字符进行写入,如果原来的字符是UTF-8或者ANSI编码,那么写入到新文件后也是UTF-8或者ANSI编码,UTF-8编码的在屏幕终端显示会乱码,但文本文件内容不乱码。FILE* fr = fopen("D:/openUTF-8.txt", "r");加密效果如下:
在这里插入图片描述
//FILE* fr = fopen("D:/openANSI.txt", "r"); 加密效果如下:
在这里插入图片描述
FILE* fr = fopen("D:/openANSI.txt", "r");有一个奇怪的现象,打开编码格式为ANSI的记事本文件,如果文件内容含有“件”字,后面直接无法写入全部内容??
在这里插入图片描述

10.3.2 按照行读写文件fgets、fputs

参照《五、数组和字符串》的5.4.3.3,如果stream是stdin或者stdout,是基于界面操作。如果stream是文件指针,是基于文件操作。

10.3.2.1 读文件fgets

unlock.txt文件内容如下:
在这里插入图片描述
调试如下代码:

	FILE* fp = fopen("D:/unlock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char str[10];
	for (size_t i = 1; i < 7; i++){
		fgets(str, 10, fp);	//从fp文件中,读取内容并保存到str指定的内存空间,读到换行、0、或者10-1个字符时为止
		printf("%s", str);
	}
	fclose(fp);

当第4次循环时,“例”是第4×10-1=39个字符和第40个字符的组合,但是第只能读到39个字符,也就是“例”只能读一半,所以会乱码。
在这里插入图片描述
当第5次循环时,文件指针位置在“例”字位置,再次调用fgets函数光标流会从原来的位置继续往下走,由于第一行字符数不足5×10-1=49个,所以读到换行符自动停止。
在这里插入图片描述
当第6次循环时,光标流走到下一行,从下一行开始继续按行读文件。
在这里插入图片描述
利用fgets函数把文件的内容一次性读取(对比前面的fgetc函数的写法):

#include<stdio.h>
#include<string.h>
int main(){
	FILE* fp = fopen("D:/unlock.txt", "r");
	if(!fp){
		perror("FileNotFound");
		return -1;
	}
	char p[10];
	//char* p = malloc(sizeof(char)*10);	//不管是开辟栈区还是堆区,只要涉及到文件结尾,都要调用memset函数进行内存重置为0
	while(!feof(fp)){						//文件末尾函数,不到文件末尾就一直循环fgets(str, 10, fp),也可以用前面的fgetc函数并结合EOF来替代这一段写法
		memset(p, 0, sizeof(char) * 10);	//重置内存空间,防止接下来受到旧内容(比如结尾处有空行)影响,使得文件指针没有到文件达结尾而导致运行结果异常
		fgets(p, 10, fp); 
		printf("%s", p);
	}
	fclose(fp);
	return 0;
}

在这里插入图片描述

10.3.2.2 写文件fputs

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
void fileGetString() {
	FILE* fp = fopen("D:/unlock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char p[10];
	while (!feof(fp)) {
		memset(p, 0, 10);
		fgets(p, 10, fp);
		printf("%s", p);
	}
	fclose(fp);
}

int main() {
	FILE* fp = fopen("D:/unlock.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char* str[] = { "定义字符指针数组\n", "遍历获得fputs函数的返回值\n", "打印返回值并通过开辟对空间和fgets函数打印内容\0", "不输出\0后面的内容" };	//'\n'直接换行,'\0'后面内容不输出,行读写是基于字符串读写的
	for (int i = 0; i < 4; i++)
		fputs(str[i], fp);	//最好用if判断一下返回值
	fclose(fp);
	fileGetString();
	return 0;
}

在这里插入图片描述

10.3.2.3 案例1:利用scanf函数在终端输入内容,再使用fgets函数将内容保存在文件中

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
void fileGetString() {
	FILE* fp = fopen("D:/unlock.txt", "r");
	if(!fp){
		perror("FileNotFound");
		return -1;
	}
	char p[10];
	while(!feof(fp)){
		memset(p, 0, sizeof(char) * 10);
		fgets(p, 10, fp); 
		printf("%s", p);
	}
	fclose(fp);
}

int main(){
	FILE* fp = fopen("D:/unlock.txt","w");
	if(!fp){
		perror("FileNotFound");
		return -1;
	}
	char p[1024];
	//while(p != "command==exit"){		//error,未初始化的局部变量
	while(1){
		memset(p, 0, 1024);
		scanf("%s", p);
		if(!strncmp(p, "command==exit",13))	//手动输入一个字符串退出循环
			break;
		fputs(p, fp);
	}
	fclose(fp);
	fileGetString();
	return 0;
}

问题:运行代码,复制粘贴HelloWorld代码到终端,发现文件内容没有空格、没有水平制表、没有换行。
原因:scanf函数不接收空格、水平制表、换行,输入空格、水平制表、换行后,scanf函数就停止输入。再下一次循环就会跳过空格、水平制表、换行才继续输入。
在这里插入图片描述
解决:①使用正则表达式;②fgtes函数,将主函数代码修改为:

	FILE* fp = fopen("D:/unlock.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char p[1024];
	//while(p != "command==exit"){				//error,未初始化的局部变量
	while (1) {
		memset(p, 0, 1024);
		//scanf("%s", p);						//scanf函数不接收空格、水平制表、换行,输入空格、水平制表、换行后,scanf函数就停止输入。再下一次循环就会跳过空格、水平制表、换行才继续输入
		scanf("%[^\n]", p);
		if (!strncmp(p, "command==exit", 13))	//手动输入一个字符串退出循环
			break;
		fputs(p, fp);
	}
	fclose(fp);
	fileGetString();

问题:运行代码,手动输入内容,发现输入回车后直接卡住,无法继续输入。
原因:%[^\n] 接收所有非换行符的字符,输入回车后换行符一直在缓存中,无法执行下一步代码,也就无法继续输入。
在这里插入图片描述
解决:使用getchar()接收缓存的字符。将主函数代码修改为:

	FILE* fp = fopen("D:/unlock.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char p[1024];
	//while(p != "command==exit"){				//error,未初始化的局部变量
	while (1) {
		memset(p, 0, 1024);						//重置内存空间,防止接下来旧内容影响新内容
		//scanf("%s", p);						//scanf函数不接收空格、水平制表、换行,输入空格、水平制表、换行后,scanf函数就停止输入。再下一次循环就会跳过空格、水平制表、换行才继续输入
		scanf("%[^\n]", p);						//方法1:正则表达式,接收所有非换行符的字符

		getchar();								//使用getchar()接收缓存的换行符,防止代码无法继续执行
		strcat(p, "\n");						//因为换行符被getchar()接收,所以要追加换行符
		if (!strncmp(p, "command==exit", 13))	//手动输入一个字符串退出循环,也可以写成 if(!strcmp(p, "command==exit\n"))
			break;
		fputs(p, fp);
	}
	printf("-------------------------------\n");
	fclose(fp);
	fileGetString();

问题:如果判断条件修改为if(!strcmp(p, "command==exit")),因为在终端输入command==exit后还要追加换行符,实际上变成command==exit\n,导致strcmp函数认为command==exit不等于command==exit\n,无法正常退出程序。
解决:要么使用if(!strncmp(p, "command==exit",13))排除追加的换行符作为判断,要么使用if(!strcmp(p, "command==exit\n"))手动添加换行符作为判断。要么先执行if(!strcmp(p, "command==exit")) break;先不追加换行符,再执行strcat(p, "\n");追加换行符。
或者用fgets(p, 1024, stdin)将主函数代码修改为:

	FILE* fp = fopen("D:/unlock.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char p[1024];
	char* p = (char*)malloc(sizeof(char) * 1024);
	//while(p != "command==exit"){				//error,未初始化的局部变量
	while (1) {
		memset(p, 0, 1024);						//重置内存空间,防止接下来旧内容影响新内容
		//scanf("%s", p);						//scanf函数不接收空格、水平制表、换行,输入空格、水平制表、换行后,scanf函数就停止输入。再下一次循环就会跳过空格、水平制表、换行才继续输入
		//scanf("%[^\n]", p);						//方法1:正则表达式,接收所有非换行符的字符
		//getchar();								//使用getchar()接收缓存的换行符,防止代码无法继续执行
		fgets(p, 1024, stdin);						//方法2:fgets函数
		if (!strcmp(p, "command==exit\n"))		//手动输入一个字符串退出循环,也可以写成 if(!strcmp(p, "command==exit\n"))
			break;								//不能写成return 0,否则不执行fileGetString函数
		//strcat(p, "\n");						//因为换行符被getchar()接收,所以要追加换行符
		fputs(p, fp);
	}
	printf("-------------------------------\n");
	fclose(fp);
	fileGetString();

在这里插入图片描述

10.3.2.4 案例2:生成四则运算

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char p[20];
int a, b, sum;	//通过随机数产生
char c;			//通过枚举产生
enum operator{
	add, substract, multi, divide
};
void fileGetString() {
	FILE* fp = fopen("D:/result.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	while (!feof(fp)) {
		memset(p, 0, 20);
		fgets(p, 20, fp);
		printf("%s", p);
	}
	fclose(fp);
}

void filePutString() {		//生成四则运算的算式(无结果)
	FILE* fp = fopen("D:/operate.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	srand((size_t)time(NULL));
	for (int i = 0; i < 5; i++) {
		memset(p, 0, 20);
		a = rand() % 100 + 1;
		b = rand() % 10 + 1; //防止除数为0导致运算错误
		switch (rand() % 4) {
		case add:		c = '+';	break;
		case substract:	c = '-';	break;
		case multi:		c = '*';	break;
		case divide:	c = '/';	break;
		}
		sprintf(p, "%d%c%d=\n", a, c, b);
		fputs(p, fp);
	}
	fclose(fp);
}

int main() {				//生成四则运算的算式(有结果)
	filePutString();
	FILE* fr = fopen("D:/operate.txt", "r");
	FILE* fw = fopen("D:/result.txt", "w");
	if (!fr || !fw) {
		perror("FileNotFound");
		return -1;
	}
	//while (!feof(fp1)) {	//如果使用该代码,由于最后一行是空行,但是文件指针还没到文件结尾,导致最后一行算式会重复写入
	for (int i = 0; i < 5; i++) {
		memset(p, 0, 20);
		fgets(p, 20, fr);
		sscanf(p, "%d%c%d=\n", &a, &c, &b);
		switch (c) {
		case '+':	sum = a + b;	break;
		case '-':	sum = a - b;	break;
		case '*':	sum = a * b;	break;
		case '/':	sum = a / b;	break;
		}
		memset(p, 0, 20);	//因为原来存放a、b、c的值,需要再次初始化
		sprintf(p, "%d%c%d=%d\n", a, c, b, sum);
		fputs(p, fw);
	}
	fclose(fr);
	fclose(fw);
	fileGetString();
	return 0;
}

在这里插入图片描述

10.3.3 按照格式化文件fprintf、fscanf

参照《七、C/C++指针》的7.6.5.7。sprintf、sscanf的用法是基于字符串操作。fprintf、fscanf的用法是基于文件操作。

10.3.3.1 读文件fscanf

函数名int fscanf(FILE* stream, const char* format, …)
头文件include <stdio.h>
参数stream:已经打开的文件
format:字符串格式,用法和scanf()一样
功能从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据,跟scanf函数一样,遇到回车、空格就结束读取(注意不包括字符串结束标志位,否则10的整数岂不是无法输出)
返回值成功:参数数目,成功转换的值的个数
失败:-1
	FILE* fp = fopen("D:/unlock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a;
	char* p = (char*)malloc(sizeof(char) * 100);
	memset(p, 0, 100);
	fscanf(fp, "%4d", &a);	//格式化输出,一般默认是%s,在此是%d并保留四位宽度
	printf("%d\n", a);		//7836
	memset(p, 0, 100);
	fscanf(fp, "%s", p);	//只读取到一行数据
	printf("%s", p);		//32
	memset(p, 0, 100);
	fscanf(fp, "%s", p);	//换行符也没有打印出来,因为fscanf函数跟scanf函数、sprintf函数一样,遇到回车、空格就结束读取
	printf("%s", p);		//定义字符指针数组
	memset(p, 0, 100);
	fscanf(fp, "%s", p);
	printf("%s", p);		//开辟堆空间和fgets函数打印内容
	free(p);
	fclose(fp);

在这里插入图片描述

10.3.3.2 写文件fprintf

函数名int fprintf(FILE* stream, const char* format, …)
头文件include <stdio.h>
参数stream:已经打开的文件
format:字符串格式,用法和printf()一样
功能根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,直到出现字符串结束符 ‘\0’ 为止
返回值成功:实际写入文件的字符个数
失败:-1
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void filePutString() {
	FILE* fp = fopen("D:/unlock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char* p = (char*)malloc(sizeof(char) * 10);
	printf("文件里面的内容是:\n");
	while (!feof(fp)) {
		memset(p, 0, 10);
		fgets(p, 10, fp);
		printf("\t%s", p);
	}
	fclose(fp);
}

int main() {
	FILE* fp = fopen("D:/unlock.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a = 15;
	fprintf(fp, "%05d\n", a);	// 00015(十进制)
	fprintf(fp, "%05X\n", a);	// 0000F(十六进制)
	fprintf(fp, "%05X\n", a);	// 0000F(十六进制)
	fclose(fp);

	fp = fopen("D:/unlock.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a1, a2;
	fscanf(fp, "%d\n", &a1);	// 将文件中的数据格式化为十进制,保存在a1中
	printf("%d\n", a1);			// 15
	fscanf(fp, "%X\n", &a2);	// 将文件中的数据格式化为十六进制,保存在a2中   fscanf也能读出十六进制数
	printf("%d\n", a2);			// 15,以十进制的方式显示
	fscanf(fp, "%d\n", &a2);	// 将文件中的数据格式化为十进制,保存在a2中
	printf("%d\n", a2);			// 0,因为读取的数据F不在十进制范围内,如果第一个数据在十进制范围内,就从第一个数读到最后一个十进制范围内的有效数据,因此0000F只是读取到0000。但是第一个数据不在十进制范围内,printf会乱码。
	fclose(fp);
	filePutString();
	return 0;
}

在这里插入图片描述

10.3.3.3 案例1:优化四则运算

将 10.3.2.3 四则运算案例用fscanf函数和fprintf函数优化:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int a, b, sum;
char c;
void fileScanf() {
	FILE* fp = fopen("D:/result.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char p[20];
	while (!feof(fp)) {
		memset(p, 0, 20);
		fgets(p, 20, fp);
		printf("%s", p);
	}
	fclose(fp);
}

void filePrintf() {
	FILE* fp = fopen("D:/operate.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	srand((size_t)time(NULL));

	for (size_t i = 0; i < 5; i++) {
		a = rand() % 100 + 1;
		b = rand() % 10 + 1;
		switch (rand() % 4) {	//也可以不用枚举
		case 0:	c = '+';	break;
		case 1:	c = '-';	break;
		case 2:	c = '*';	break;
		case 3:	c = '/';	break;
		}
		fprintf(fp, "%d%c%d=\n", a, c, b);
	}
	fclose(fp);
}

int main() {
	filePrintf();
	FILE* fr = fopen("D:/operate.txt", "r");
	FILE* fw = fopen("D:/result.txt", "w");
	if (!fr || !fw) {
		perror("FileNotFound");
		return -1;
	}
	for (size_t i = 0; i < 5; i++) {
		fscanf(fr, "%d%c%d=", &a, &c, &b);
		switch (c) {
		case '+':	sum = a + b;	break;
		case '-':	sum = a - b;	break;
		case '*':	sum = a * b;	break;
		case '/':	sum = a / b;	break;
		}
		fprintf(fw, "%d%c%d=%d\n", a, c, b, sum);
	}
	fclose(fr);
	fclose(fw);
	fileScanf();
	return 0;
}

优点:fscanf函数和fprintf函数可以不需要像sscanfsprintf操作字符串,因此也不需要创建、释放堆区。

10.3.3.4 案例2:大数据排序

①利用随机数创建n个数据,fprintf写入到文件mix.txt中(这里令N=10);
②读取mix.txt,给里面的数据冒泡排序并fprintf写入到文件mix.txt中。

#pragma warning(disable:4996)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 10
void fileScanf() {
	FILE* fp = fopen("D:/bubbleSortRank.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a;
	while (!feof(fp)) {
		fscanf(fp, "%d\t", &a);		//如果不加\n,第N次循环就还没到结尾,还要再循环1次
		printf("%d\t", a);
	}
	fclose(fp);
}

int bubbleSort(int array[], int length) {
	int bubbleSortCount = 0;
	for (int i = 0; i < length - 1; i++) {
		for (int j = 0; j < length - 1 - i; j++) {
			bubbleSortCount++;			//right
			if (array[j] > array[j + 1]) {
				int swap = array[j];
				array[j] = array[j + 1];
				array[j + 1] = swap;
				//bubbleSortCount++;	//error,计数是写在判断之前,而不是写在判断内部,否则条件不成立时,无法自增
			}
		}
	}
	return bubbleSortCount;
}

int filePrintf() {
	FILE* fp = fopen("D:/mix.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	srand((size_t)time(NULL));
	int a, filePrintfCount = 0;
	for (size_t i = 0; i < N; i++) {
		filePrintfCount++;
		a = rand() % 5;
		fprintf(fp, "%d\t", a);//这里必须换行分开,否则生产的随机数连在一块,后面输出数据时会将N个数一次全部输出
		printf("%d\t", a);
	}
	fclose(fp);
	return filePrintfCount;
}

int main() {
	printf("排序前数据:");
	int filePrintfCount = filePrintf();			//执行n次
	FILE* fr = fopen("D:/mix.txt", "r");
	FILE* fw = fopen("D:/bubbleSortRank.txt", "w");
	if (!fr || !fw) {
		perror("FileNotFound");
		return -1;
	}

	int* arr = (int*)malloc(sizeof(int) * N);	//因为要对很多数据进行排序,因此开辟堆空间
	int fscanfCount = 0, fprintfCount = 0;
	memset(arr, 0, sizeof(int) * N);
	for (size_t i = 0; i < N; i++) {
		fscanfCount++;							//执行n次
		fscanf(fr, "%d\n", &arr[i]);
	}

	int bubbleSortCount = bubbleSort(arr, N);	//执行n×(n-1)/2(次)
	printf("\n排序后数据:");
	for (size_t i = 0; i < N; i++) {
		fprintfCount++;							//执行n次
		fprintf(fw, "%d\t", arr[i]);
	}
	free(arr);
	fclose(fr);
	fclose(fw);

	fileScanf();								//在文件中获取arr的排序

	int sum = fscanfCount + fprintfCount + bubbleSortCount + filePrintfCount;
	printf("\nfilePrintf执行%d次", filePrintfCount);
	printf("\nfscanf执行%d次", fscanfCount);
	printf("\nbubbleSort执行%d次", bubbleSortCount);
	printf("\nfprintf执行%d次", fprintfCount);
	printf("\n该程序一共执行%d次\n", sum);
	return 0;
}

在这里插入图片描述
问题:该代码除去fileGetString函数,对n个数进行冒泡排序并写入文件,总共要执行n+n+n×(n-1)/2+n (次),如果n越大,程序的工作量越大。
解决:要比较的数的数量多于某个范围内的数的数量,利用直接插入法(注意不是插入排序),因为所有处理的数据在rand函数下都是在一定范围的。因此定义一个整型数组,长度是5,初始化每个下标为0。如果读取一个数是i,就会存入到下标为i的位置,对下标为i的元素值自增1。

#include<stdio.h>
#pragma warning(disable:4996)
#define N 10
void fileScanf() {
	FILE* fp = fopen("D:/insert.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a;
	while (!feof(fp)) {
		fscanf(fp, "%d\n", &a);		//如果不加\n,第N次循环就还没到结尾,还要再循环1次
		printf("%d\n", a);
	}
	fclose(fp);
}

int filePrintf() {
	FILE* fp = fopen("D:/mix.txt", "w");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	srand((size_t)time(NULL));
	int a, filePrintfCount = 0;
	for (size_t i = 0; i < N; i++) {
		filePrintfCount++;			//执行n次
		a = rand() % 5;
		fprintf(fp, "%d\n", a);		//最后一行为空行
		printf("%d\n", a);
	}
	fclose(fp);
	return filePrintfCount;
}

int main() {
	printf("排序前数据:\n");
	int filePrintfCount = filePrintf();
	//打开文件
	FILE* fr = fopen("D:/mix.txt", "r");
	FILE* fw = fopen("D:/insert.txt", "w");
	if (!fr || !fw) {
		perror("FileNotFound");
		return -1;
	}
	int value, arr[5] = {0}, fscanfCount = 0, fprintfCount = 0;
	//读数据
	for (size_t i = 0; i < N; i++) {
		fscanfCount++;							//执行n次
		fscanf(fr, "%d\n", &value);				//从文件中读取的值放到value中,由随机数可知value的值在0-4之间
		//直接插入法
		arr[value]++;							//将数据的个数放在对应的下标里,读取到1个value,就给arr[value]自增
		//printf("%d\n",arr[i]);				//不能直接输出数据,因为i可以大于4,value只能在0-4,arr[5]及其以后的因为找不到相应的值,必定是是乱码
	}

	printf("排序后数据:\n");
	for (size_t i = 0; i < 5; i++) {				//外循环是循环数组的下标
		for (size_t j = 0; j < arr[i]; j++) {	//内循环是对每个下标元素进行计数
			fprintfCount++;						//因此外循环+内循环一共执行n次
			fprintf(fw, "%d\n", i);				//通过内循环得到该下标每个元素的个数,并通过内循环的临时变量一个个写到文件中
		}
	}
	fclose(fr);
	fclose(fw);

	fileScanf();

	int sum = fscanfCount + fprintfCount + filePrintfCount;
	printf("\nfilePrintf执行%d次", filePrintfCount);
	printf("\nfscanf执行%d次", fscanfCount);
	printf("\nfprintf执行%d次", fprintfCount);
	printf("\n该程序一共执行%d次\n", sum);
	return 0;
}

该代码除去fileScanf函数,对n个数进行直接插入法并写入文件,直接插入法执行次数为n+n+n(次)。如果n越大,程序的工作量相比较冒泡排序会更少。
适用范围:当数据都在一定范围内且数据量很大的时候,可以考虑直接插入法。
在这里插入图片描述

10.3.4 按照块读写文件fread、fwrite

按照块读写文件是基于二进制文件操作的。但是打开的文件可以是以txt(记事文本文件),也可以是bat(bat是批处理文件)。
在这里插入图片描述
其中批处理文件可以写入相关命令,比如右击→编辑:
在这里插入图片描述
输入命令osk→保存并退出。
在这里插入图片描述
双击运行批处理文件.bat,即可打开软键盘。
在这里插入图片描述

10.3.4.1 写文件fwrite

函数名size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
头文件include <stdio.h>
参数ptr:写入文件数据的地址
size: 写入文件内容的块数据大小
nmemb:写入文件的块数
stream:文件指针
功能以数据块的方式给文件写入内容,写入文件数据总大小为size * nmemb
返回值成功:实际成功写入文件数据的块数目,此值和 nmemb 相等
失败:0
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
void fileGetString() {
	FILE* fp = fopen("D:/fwrite.txt", "r");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	char p[20];
	while (!feof(fp)) {
		memset(p, 0, 20);
		fgets(p, 20, fp);
		printf("%s", p);
	}
	fclose(fp);
}

int main() {
	FILE* fp = fopen("D:/fwrite.txt", "wb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a = 5678;
	//int b[] = { 5,6,7,8 };		//如果是数组的块写入
	fwrite(&a, sizeof(int), 1, fp);
	//fwrite(b, sizeof(int), 4, fp);//写入的块数就是数组的长度sizeof(数组)
	//fwrite(b, sizeof(b), 1, fp);
	fclose(fp);
	return 0;
}

发现fgets函数在终端无法显示二进制内容,打开记事本也无法显示fwrite写入的具体内容。
在这里插入图片描述
右击fwrite.txt→属性,得知5678在二进制中占4字节大小。
在这里插入图片描述

10.3.4.2 读文件fread

函数名size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
头文件include <stdio.h>
参数ptr:存放读取出来数据的内存空间
size: 读取文件内容的块数据大小
nmemb:读取文件的块数
stream:已经打开的文件指针
功能以数据块的方式从文件中读取内容,读取文件数据总大小为size * nmemb
返回值成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾
失败:0
10.3.4.2.1 单个变量的块读写

要想把10.3.4.1 fwrite写入的二进制在终端读取出来,可以使用fread函数。将10.3.4.1代码修改为:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	FILE* fp = fopen("D:/fwrite.txt", "wb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a = 5678;
	fwrite(&a, sizeof(int), 1, fp);
	fclose(fp);

	fp = fopen("D:/fwrite.txt", "rb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int value;
	fread(&value, sizeof(int), 1, fp);
	printf("%d", value);
	fclose(fp);
	return 0;
}

在这里插入图片描述

10.3.4.2.2 数组的块读写
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	FILE* fp = fopen("D:/fwrite.txt", "wb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int a[] = { 5,6,7,8 };
	printf("fwrite的返回值:%d\n", fwrite(a, sizeof(int), 4, fp));	//fwrite的返回值:4
	fclose(fp);

	fp = fopen("D:/fwrite.txt", "rb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	int array[4];
	printf("fread的返回值:%d\n", fread(array, sizeof(int), 4, fp));//fread的返回值:4
	//fread(array, sizeof(int), 3, fp);	//5 6 7 -858993460		error,总体数据块大小不同,不可以正确读取出来
	//fread(array, 2, 8, fp);			//5 6 7 8				right,总体数据块大小相同,也可以正确读取出来
	for (size_t i = 0; i < 4; i++) {
		printf("%d\t", array[i]);
	}
	fclose(fp);
	return 0;
}
10.3.4.2.3 结构体的块读写
#pragma warning(disable:4996)
#include<stdio.h>
#include<string.h>
struct student {
	char name[21];
	int age;
};
int main() {
	FILE* fp = fopen("D:/fwrite.txt", "wb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	struct student stu[2] = {
		{"曹操",62},
		{"刘备",68}
	};
	printf("结构体整体大小:sizeof(struct student)=%d\n", sizeof(struct student));	//28
	printf("结构体变量大小:sizeof(stu)=%d\n", sizeof(stu));							//56

	fread(stu, sizeof(struct student), 2, fp);			//等价于下面这段for循环
	//for (size_t i = 0; i < 2; i++) {					//每次写入一个结构体数据&stu[i]
	//	fwrite(&stu[i], sizeof(struct student), 1, fp);	//fwrite写入,中文还是用中文显示,英文则无法正常显示
	//}
	fclose(fp);

	fp = fopen("D:/fwrite.txt", "rb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	fread(stu, sizeof(struct student), 2, fp);			//等价于下面这段while循环
	//int i = 0;
	//while (!feof(fp)) {
	//	memset(&stu[i], 0, 50);		//right
	//	//memset(stu, 0, 50);		//error,类型不一致
	//	//memset(stu, 0, 60);		//error,stu周围的栈区被损坏
	//	fread(&stu[i], sizeof(struct student), 1, fp);
	//	i++;
	//}
	for (int i = 0; i < 2; i++)
		printf("stu[%d].name:%s\tstu[%d].age:%d\n", i, stu[i].name, i, stu[i].age);
	fclose(fp);
	return 0;
}

memset(&stu[i], 0, 50)
在这里插入图片描述
memset(stu, 0, 50); //error,类型不一致
在这里插入图片描述
//memset(stu, 0, 60);//stu周围的栈区被损坏,原因是sizeof(stu)=56,60>56,没有开辟开辟堆空间或者栈区大于56的内存,直接memset(stu,0,60)会导致栈区周围被破坏。
在这里插入图片描述

10.3.4.3 案例:大文件拷贝

问题:如果拷贝大文件,例如有个60MB的MP4视频,无法直接创建60MB的堆空间去操作内容。

#pragma warning(disable:4996)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define SIZE 1024
int main(int argc, char* argv[]) {
	if (argc < 3) {
		printf("input parameter no enough");
		return -2;
	}
	//argv[0]是程序名		argv[1]表示需要打开的文件	argv[2]表示拷贝文件的位置
	//D:/copy.exe			D:/a.mp4					D:/b.mp4
	//假设文件大小是50MB,不可能直接开辟堆空间去操作内容。
	FILE* fr = fopen(argv[1], "rb");
	FILE* fw = fopen(argv[2], "wb");
	if (!fr || !fw) {
		perror("CopyFileError");
		return -1;	//跟上面输入参数不够的返回值做区分,以便于如果出现问题,可以在主程序中根据不同的返回值确定是哪个问题
	}
	char* p = (char*)malloc(sizeof(char) * SIZE);//创建1KB空间
	while (!feof(fr)) {
		memset(p, 0, sizeof(char) * SIZE);
		fread(p, sizeof(char), SIZE, fr);
		fwrite(p, sizeof(char), SIZE, fw);
	}
	free(p);
	fclose(fr);
	fclose(fw);
	return 0;
}

打开CMD,输入命令gcc -o D:\copy.exe D:\project\VS2022\Project1\Project1\project.c(命令模板是:gcc -o 生成文件 源文件),回车后生成D:\copy.exe。
在这里插入图片描述
接着输入命令D:\copy.exe D:\opencv模块1.mp4 D:\opencv模块2.mp4,发现生成opencv模块2.mp4文件。
在这里插入图片描述
分别右击opencv模块1.mp4和opencv模块2.mp4查看属性:
在这里插入图片描述
问题:复制的文件大小比源文件大。为什么复制文件内存大小会不一致?
原因:开辟1KB的空间,在文件结尾前的最后一次while循环,最后一个1KB堆空间不一定全部内存占满,可能有多余的空位,读取的时候只读被占领的内存,但写的时候是把整个1KB内存写进去的,导致复制文件比原始文件大。
解决:由fread函数和fwrite函数的返回值可得,返回fread函数的值,再把值传递给fwrite函数作为参数
。将主函数代码修改如下:

	if (argc < 3) {
		printf("input parameter no enough");
		return -2;
	}
	//argv[0]是程序名		argv[1]表示需要打开的文件	argv[2]表示拷贝文件的位置
	//D:/copy.exe			D:/a.mp4					D:/b.mp4
	//假设文件大小是50MB,不可能直接开辟堆空间去操作内容。
	FILE* fr = fopen(argv[1], "rb");
	FILE* fw = fopen(argv[2], "wb");
	if (!fr || !fw) {
		perror("CopyFileError");
		return -1;	//跟上面输入参数不够的返回值做区分,以便于如果出现问题,可以在主程序中根据不同的返回值确定是哪个问题
	}
	char* p = (char*)malloc(sizeof(char) * SIZE);//创建1KB空间
	int count;
	while (!feof(fr)) {
		memset(p, 0, sizeof(char) * SIZE);
		count = fread(p, sizeof(char), SIZE, fr);	//实际成功读取到内容的块数
		fwrite(p, sizeof(char), SIZE, fw);			//count作为fwrite的参数,就可以原样读出原样写入
	}
	free(p);
	fclose(fr);
	fclose(fw);

在这里插入图片描述
以上代码每次都按照1KB的堆空间去块读写,如果增加一倍堆空间大小,程序的while循环次数会减少一半左右,运行速度变快,要想程序快速运行,只需更改宏定义的SIZE大小即可。

10.4 文件的随机读写

函数名int fseek(FILE *stream, long offset, int whence)
头文件include <stdio.h>
参数stream:文件指针
offset:根据whence来移动的位移数(偏移量),可以是正数,也可以负数,如果正数,则相对于whence往右移动,如果是负数,则相对于whence往左移动。如果向前移动的字节数超过了文件开头则出错返回,如果向后移动的字节数超过了文件末尾,再次写入时将增大文件尺寸
whence取值如下:①SEEK_SET:从文件开头移动offset个字节;②SEEK_CUR:从当前位置移动offset个字节;③SEEK_END:从文件末尾移动offset个字节
功能移动文件流(文件光标)的读写位置
返回值成功:0
失败:1
函数名long ftell(FILE *stream)
头文件include <stdio.h>
参数stream:已经打开的文件指针
功能获取文件流(文件光标)的读写位置
返回值成功:当前文件流(文件光标)的读写位置
失败:1
函数名void rewind(FILE *stream)
头文件include <stdio.h>
参数stream:已经打开的文件指针
功能把文件流(文件光标)的读写位置移动到文件开头
返回值
#include<stdio.h>
#include<string.h>
int main() {
	FILE* fp = fopen("D:/openANSI.txt", "r");
	if (!fp) {
		perror("OpenFileFail");
		return -1;
	}
	char str[100];
	memset(str, 0, sizeof(char) * 100);
	fgets(str, 100, fp);
	printf("%s", str);			//7.1 概述

	memset(str, 0, sizeof(char) * 100);
	fgets(str, 100, fp);
	printf("%s", str);			//        7.1.1 内存

	fseek(fp, -19, SEEK_CUR);	//从当前位置返回17个字符,因为上一行已结束,windows文本文件换行是\r\n,因此文件指针移动到“概”的前面
	//fseek(fp, -19, 1);		//等价于上一句话,因为SEEK_CUR的宏定义是1
	memset(str, 0, sizeof(char) * 100);
	fgets(str, 100, fp);
	printf("%s", str);			//概述

	fseek(fp, 12, SEEK_SET);
	memset(str, 0, sizeof(char) * 100);
	fgets(str, 100, fp);
	printf("%s", str);			//.1.1 内存

	fseek(fp, -17, SEEK_END);	//从文件末尾返回17个字符,因为文件结尾是-1不是\r\n,因此文件指针移动到最后一行的开始
	memset(str, 0, sizeof(char) * 100);
	fgets(str, 100, fp);
	printf("%s", str);			//                7.6.5.12 atoi()

	printf("\n光标流位置:%ld", ftell(fp));
	rewind(fp);					//等价于fseek(fp, 0, SEEK_SET);
	printf("\n光标流位置:%ld", ftell(fp));
	
	fclose(fp);
	return 0;
}

在这里插入图片描述

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
struct student {
	char name[21];
	int age;
};
int main() {
	FILE* fp = fopen("D:/fwrite.txt", "wb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	struct student stu[3] = {
		{"曹操",62},
		{"刘备",68},
		{"孙权",48},
	};

	fwrite(stu, sizeof(struct student), 3, fp);
	fclose(fp);

	fp = fopen("D:/fwrite.txt", "rb");
	if (!fp) {
		perror("FileNotFound");
		return -1;
	}
	struct student temp;
	int ret = 0;
	fseek(fp, 2 * sizeof(struct student), SEEK_SET);	//文件光标读写位置从开头往右移动2个结构体变量的位置
	ret = fread(&temp, sizeof(struct student), 1, fp);	//读第3个结构体
	if (ret == 1)
		printf("[temp]%s, %d\n", temp.name, temp.age);
	rewind(fp);											//把文件光标移动到文件开头
	ret = fread(stu, sizeof(struct student), 3, fp);
	printf("ret = %d\n", ret);
	for (int i = 0; i < 3; i++)
		printf("stu[%d].name:%s\tstu[%d].age:%d\n",i, stu[i].name, i, stu[i].age);
	fclose(fp);
	return 0;
}

在这里插入图片描述

10.5 Windows和Linux文本文件区别

判断文本文件是Linux格式还是Windows格式:

#include<stdio.h>
int main(int argc, char **args){
	if (argc < 2){
		printf("输入参数不够");
		return 0;
	}
	FILE *fp = fopen(args[1], "rb");
	if (!fp)
		return -1;
	char a[1024] = { 0 };
	fgets(a, sizeof(a), p);
	int len = 0;
	while (a[len]){
		if (a[len] == '\n'){
			if (a[len - 1] == '\r')	{	printf("windows file\n"); 	}
			else					{	printf("linux file\n");		}
		}
		len++;
	}
	fclose(p);
	return 0;
}

在这里插入图片描述

10.6 获取文件状态

函数名int stat(const char *path, struct stat *buf)
头文件include <sys/types.h>
include <sys/stat.h>
参数path:文件名
buf:结构体指针,保存文件信息的结构体
功能获取文件状态信息
返回值成功:0
失败:1
优点不用手动打开文件查询属性,通过stat函数知道文件的大小
struct stat {
	dev_t         st_dev;		//文件的设备编号
	ino_t         st_ino;		//节点
	mode_t        st_mode;		//文件的类型和存取的权限
	nlink_t       st_nlink;		//连到该文件的硬连接数目,刚建立的文件值为1
	uid_t         st_uid;		//用户ID
	gid_t         st_gid;		//组ID
	dev_t         st_rdev;		//(设备类型)若此文件为设备文件,则为其设备编号
	off_t         st_size;		//文件字节数(文件大小)
	unsigned long st_blksize;	//块大小(文件系统的I/O 缓冲区大小)
	unsigned long st_blocks;	//块数
	time_t        st_atime;		//最后一次访问时间
	time_t        st_mtime;		//最后一次修改时间
	time_t        st_ctime;		//最后一次改变时间(指属性)
};

问题:在 10.3.4.3 大文件拷贝案例中,如果要拷贝的文件很小(比如.c文件),开辟过大的堆空间会浪费内存,如何可以不手动打开文件属性知道文件的大小,再去开辟合适的堆空间?
解决:通过stat函数获取文件的大小。

#pragma warning(disable:4996)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#define SIZE (1024*1024*8)
int main(int argc, char* argv[]) {
	if (argc < 3) {
		printf("input parameter no enough");
		return -2;
	}
	FILE* fr = fopen(argv[1], "rb");
	FILE* fw = fopen(argv[2], "wb");
	if (!fr || !fw) {
		perror("CopyFileError");
		return -1;
	}
	struct stat st;										//定义文件状态结构体变量,stat是系统提供好的结构体
	stat(argv[1], &st);
	char* p;
	int realSize, count;								//记录实际开辟的堆空间大小和成功读取的块数
	if (st.st_size > SIZE)								//通过结构体变量.成员获得相关属性,根据实际大小开辟堆空间
		realSize = SIZE;								//如果文件超过8MB,开辟堆空间8MB
	else
		realSize = st.st_size + 10;						//如果文件小于8MB,文件有多大就开辟多大的堆空间(最好加个偏移量略微大一点,防止'\0'干扰,直接一次性读写完数据)
	
	p = (char*)malloc(sizeof(char) * realSize);
	while(!feof(fr)){
		memset(p, 0, sizeof(char) * realSize);
		count = fread(p, sizeof(char), realSize, fr);	//如果不记录实际开辟的堆空间大小,如果是按文件大小加大10字节内存的形式开辟堆空间,块读写的总数会不一致导致报错
		fwrite(p, sizeof(char), count, fw);
	}
	free(p);
	fclose(fr);
	fclose(fw);
	return 0;
}

10.7 删除文件、重命名文件名

函数名int remove(const char *pathname)
头文件include <stdio.h>
参数pathname:文件名
功能永久删除文件,不会放到回收站
返回值成功:0
失败:1
删除失败原因①文件不存在
②文件正在使用中
③文件权限(被锁定)
④文件夹(不管是不是空文件夹,都无法删除)
说明很多杀毒软件清理temp缓存用的就是利用remove函数
	//首先在D盘目录下新建lock1.txt
	if (remove("D:/lock1.txt") == 0)
		printf("remove file succeed");
	else
		perror("RemoveFileFail");

先执行一次程序,删除文件成功,D盘下没有lock1.txt文件:
在这里插入图片描述
再执行一次程序,删除文件失败,提示文件不存在(上一次运行已经把文件删除):
在这里插入图片描述

	//首先在D盘目录下新建文件夹,命名为1(不管1是不是空文件夹)
	if (remove("D:/1") == 0)
		printf("remove file succeed");
	else
		perror("RemoveFileFail");

运行代码,删除文件夹失败,提示文件权限不够(文件夹不可被删除):
在这里插入图片描述

	//首先在D盘目录下新建1.zip,并双击打开解压包
	if (remove("D:/1.zip") == 0)
		printf("remove file succeed");
	else
		perror("RemoveFileFail");
	return 0;

运行代码,删除文件失败,提示文件权限不够(文件正在使用中):
在这里插入图片描述

函数名int rename(const char *oldpath, const char *newpath)
头文件include <stdio.h>
参数oldpath:旧文件名
newpath:新文件名
功能把oldpath的文件名改为newpath
返回值成功:0
失败:1
	if (rename("D:/opencv模块1.mp4","opencv模块.mp4") == 0)
		printf("rename file succeed");
	else
		perror("RenameFileFail");
	//移动(剪切)文件
	if (rename("D:/opencv模块2", "D:/path/opencv模块.mp4") == 0)
		printf("rename file succeed");
	else
		perror("RenameFileFail");

原始磁盘目录如下:
在这里插入图片描述
运行程序后如下:
在这里插入图片描述
注意:无法重命名相同的名字或者剪切到同名文件的上级目录下,如下图。
在这里插入图片描述
运行程序后如下,重命名文件失败,提示文件已存在。
在这里插入图片描述

10.8 文件缓冲区

10.8.1 文件缓冲区

定义:ANSI C标准采用“缓冲文件系统”处理数据文件,缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区。
流程:如果从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去;如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个将数据送到程序数据区(给程序变量)。
作用:①临时存储数据;②如果没有文件缓冲区,磁盘直接频繁读写数据,容易损坏磁盘,并且速度较慢。
特点:缓冲区也是内存的一部分,内存读取速度快,内存小,断电丢失数据;磁盘读取速度慢,内存大,断电不丢失数据。

10.8.2 磁盘文件的存取

请添加图片描述
①磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存;
②程序与磁盘交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率。

10.8.3 更新缓冲区

函数名int fflush(FILE *stream)
头文件include <stdio.h>
参数stream:文件指针
功能更新缓冲区,让缓冲区的数据立马写到文件中
返回值成功:0
失败:1
说明①Word文档每隔一段时间或者充满缓冲区就会自动保存,其实就是利用fflush函数;
②对于非常重要数据,需要实时保存;否则不建议实时保存(比如写代码),因为会损伤磁盘,建议写个条件判断,如果写入字符超过多少个或者设置记录当前系统时间后每隔一段时间更新缓冲区。
③如果不更新缓冲区,在fclose关闭文件或者程序运行结束后会自动更新缓冲区写入到磁盘中。如果是突然断电,则缓冲区数据丢失,没有保存在磁盘中。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main() {
	char str;
	FILE* fp = fopen("D:/fputc.txt", "w");
	if (!fp) {
		perror("CreateFileFail");
		return -1;
	}
	while (1) {
		scanf("%c", &str);
		if (str == '@')
			break;
		fflush(fp);									//每写完1行就更新缓冲区,频繁数据交互,但不建议这么写并且去运行该代码,会损伤磁盘
		if (fputc(str, fp) == -1) {
			perror("CreateFileFail");
			return -1;
		}
	}
	fclose(fp);
	return 0;
}

在这里插入图片描述

码字不易,如果大家觉得有用,请高抬贵手给一个赞让文章上推荐让更多的人看到吧,也可以评论提出意见让后面的文章内容越来越生动丰富。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值