十、文件操作(易懂易学习,附代码案例)
十、文件操作
- 10.1 概述
- 10.1.1 文件分类
- 10.1.2 磁盘文件的分类
- 10.2 文件的打开和关闭
- 10.2.1 文件指针FILE
- 10.2.2 文件的打开
- 10.2.3 文件的关闭
- 10.3 文件的顺序读写
- 10.3.1 按照字符读写文件fgetc、fputc
- 10.3.1.1 读文件fgetc
- 10.3.1.2 文件结尾
- 10.3.1.3 写文件fputc
- 10.3.2 按照行读写文件fgets、fputs
- 10.3.2.1 读文件fgets
- 10.3.2.2 写文件fputs
- 10.3.2.3 案例1:利用scanf函数在终端输入内容,再使用fgets函数将内容保存在文件中
- 10.3.2.4 案例2:生成四则运算
- 10.3.3 按照格式化文件fprintf、fscanf
- 10.3.3.1 读文件fscanf
- 10.3.3.2 写文件fprintf
- 10.3.3.3 案例1:优化四则运算
- 10.3.3.4 案例2:大数据排序
- 10.3.4 按照块读写文件fread、fwrite
- 10.3.4.1 写文件fwrite
- 10.3.4.2 读文件fread
- 10.3.4.2.1 单个变量的块读写
- 10.3.4.2.2 数组的块读写
- 10.3.4.2.3 结构体的块读写
- 10.3.4.3 案例:大文件拷贝
- 10.4 文件的随机读写
- 10.5 Windows和Linux文本文件区别
- 10.6 获取文件状态
- 10.7 删除文件、重命名文件名
- 10.8 文件缓冲区
- 10.8.1 文件缓冲区
- 10.8.2 磁盘文件的存取
- 10.8.3 更新缓冲区
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或者-1 | End 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"); //以写的方式打开的的文件默认编码是ANSIif (!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"); //此时编码格式是ANSIif (!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"); //此时编码格式是ANSIif (!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-8if (!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函数进行内存重置为0while(!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); //7836memset(p, 0, 100);fscanf(fp, "%s", p); //只读取到一行数据printf("%s", p); //32memset(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); // 15fscanf(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++; //rightif (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的返回值:4fclose(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)); //28printf("结构体变量大小:sizeof(stu)=%d\n", sizeof(stu)); //56fread(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的宏定义是1memset(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; //连到该文件的硬连接数目,刚建立的文件值为1uid_t st_uid; //用户IDgid_t st_gid; //组IDdev_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,开辟堆空间8MBelserealSize = 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.txtif (remove("D:/lock1.txt") == 0)printf("remove file succeed");elseperror("RemoveFileFail");
先执行一次程序,删除文件成功,D盘下没有lock1.txt文件:
再执行一次程序,删除文件失败,提示文件不存在(上一次运行已经把文件删除):
//首先在D盘目录下新建文件夹,命名为1(不管1是不是空文件夹)if (remove("D:/1") == 0)printf("remove file succeed");elseperror("RemoveFileFail");
运行代码,删除文件夹失败,提示文件权限不够(文件夹不可被删除):
//首先在D盘目录下新建1.zip,并双击打开解压包if (remove("D:/1.zip") == 0)printf("remove file succeed");elseperror("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");elseperror("RenameFileFail");//移动(剪切)文件if (rename("D:/opencv模块2", "D:/path/opencv模块.mp4") == 0)printf("rename file succeed");elseperror("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;
}
码字不易,如果大家觉得有用,请高抬贵手给一个赞让文章上推荐让更多的人看到吧,也可以评论提出意见让后面的文章内容越来越生动丰富。
十、文件操作(易懂易学习,附代码案例)相关推荐
- file 选择的文件胖多有多大_「HTML5 进阶」FileAPI 文件操作实战,内附详细案例,建议收藏...
FileAPI 介绍 HTML5 为我们提供了 File API 相关规范.主要涉及 File 接口 和 FileReader 对象 . 本文整理了兼容性检测.文件选择.属性读取.文件读取.进度监控. ...
- python常用代码入门-入门十大Python机器学习算法(附代码)
入门十大Python机器学习算法(附代码) 今天,给大家推荐最常用的10种机器学习算法,它们几乎可以用在所有的数据问题上: 1.线性回归 线性回归通常用于根据连续变量估计实际数值(房价.呼叫次数.总销 ...
- 一、C语言概述(易懂易学习,附代码案例)
一.C语言概述 1.1 什么是C语言 1.2 为什么要学C语言 1.2.1 C语言的特点 1.2.2 C语言应用领域 1.2.3 C语言的简洁 1.3 第一个C语言程序:HelloWorld 1.3. ...
- python学习笔记十-文件操作
对文件操作流程 1.打开文件,得到文件句柄并赋值给一个变量 2.通过句柄对文件进行操作 3.关闭文件 操作如下: 花间一壶酒,独酌无相亲. 举杯邀明月,对影成三人. 月既不解饮,影徒随我身. 暂伴月将 ...
- jquery和php怎么链接地址,jQuery操作url地址(附代码)
这次给大家带来jQuery操作url地址(附代码),jQuery操作url地址的注意事项有哪些,下面就是实战案例,一起来看一下. path.makeUrlAbsolute() 把相对URL转化为绝对U ...
- python 文件函数_python文件操作及函数学习
文件操作 文件读 f = open('a.txt', encoding='utf-8', mode='r') #只读方式打开文件 data = f.read() #read函数读取所有文件内容,光 ...
- python pandas csv 写文件_Pandas读写CSV文件的方法介绍(附代码)
本篇文章给大家带来的内容是关于Pandas读写CSV文件的方法介绍(附代码),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 读csv:使用pandas读取import pandas ...
- 五、数组、字符串以及冒泡排序--附代码案例
五.数组和字符串 5.1 一维数组 5.1.1 一维数组的定义和使用 5.1.2 一维数组的初始化 5.1.3 数组名 5.1.4 强化训练 5.1.4.1 一维数组的最值 5.1.4.2 一维数组的 ...
- “装箱”和“拆箱”操作(有详细的代码案例和说明);
目录 C# 的"装箱"和"拆箱": 1.装箱 2.拆箱 使用拆箱操作时出现的错误 C# 的"装箱"和"拆箱": C#语言 ...
最新文章
- 关于刘冬大侠Spring.NET系列学习笔记3的一点勘正
- 一些有用的Python库
- 【原】iOS学习之Quartz2D(1)
- NullPointerException异常的原因及java异常??
- 《微服务设计》读书笔记
- ngx_http_lua_inject_socket_tcp_api函数代码注释
- 第 19 课时:调度器的调度流程和算法介绍(木苏)
- kali linux fuzz工具集简述
- Angular @Hostbinding工作原理
- java调用reader的nextInt_Java中如何从键盘输入内容: import java.util.Scanner; .nextLine(); .hasNextInt();...
- 在计算机硬件中mo是指,计算机导论 - [课件]第2章 计算机系统的硬件.ppt
- caioj 1081 动态规划入门(非常规DP5:观光游览)
- 网上讨论“电商平台打败了实体店”?
- Stanford机器学习---第一讲. Linear Regression with one variable
- php 所有国家时区,php输出全球各个时区列表
- 数字证书及CA的详细理解
- 微信王者服务器怎么删掉,微信王者荣耀账号怎么注销 王者注销微信账号方法一览...
- 短视频平台原创检测规则 伪原创制作
- hadoop面试题 5 ---有用
- 前端 css实现文字竖向排列
热门文章
- 生物药公司“普米斯”获1.8亿元融资,华金资本、珠海高科创投、弘晖资本联合投资...
- 很容易理解的C语言快速排序算法(完整注释+完整输出)
- m3u8流的视频播放与销毁
- Linux 驱动库曝光 AMD 新一代显卡:Vega 和 Polaris
- AVL 树高度和结点数的关系
- 前端项目启动流程及常见问题
- 手把手教你制作查题微信公众号
- Ranged-Based for loop vs. iterator
- Excel的简单Word报告批量生成 vba笔记 (一)
- PCL点云处理之车载建筑物点云立面提取(三十二)