概述
磁盘文件和设备文件
磁盘文件
指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存。
设备文件
在操作系统中把每一个与主机相连的输入输出设备看作是一个文件,把他们的输入,输出等同于对磁盘文件的读和写
磁盘文件的分类
计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的,以字节为单位进行顺序存储。
从用户或操作系统使用的角度把文件分为
- 文本文件:基于字符编码的文件
- 二进制文件:基于值编码的文件
文本文件和二进制文件
文本文件
- 基于字符编码,常见编码有ASCII UNICODE等
- 一般可以使用文本文档直接打开
- 数5678的存储形式(ASCII)为
00110101 00110110 00110111 00111000
00110101 00110110 00110111 00111000 二进制
53 54 55 56 ASCII
‘5’ ‘6’ ‘7’ ‘8’ 字符
二进制文件
- 基于值编码,自己根据具体应用,指定某个值是什么意思
- 把内存中的数据按其在内存中的存储形式原样输出到磁盘上
- 5678的存储形式(二进制)为
00010110 00101110
从5678不同的编码方式存储在磁盘的空间来看,还是使用二进制存储更节省磁盘空间。很多操作情况下都趋于二进制进行编码
文件的打开和关闭
文件的指针
在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。
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定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。
声明FILE结构体类型的信息包含在头文件“stdio.h”中,
一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。通过文件指针就可对它所指的文件进行各种操作。
C语言中有三个特殊的文件指针由系统默认打开,用户无需定义即可直接使用:
- stdin:标准输入,默认为当前终端(键盘),我们使用的scanf、getchar函数默认从此终端获得数据。
- stdout:标准输出,默认为当前终端(屏幕),我们使用的printf、puts函数默认输出信息到此终端。
- stderr:标准出错,默认为当前终端(屏幕),我们使用的perror函数默认输出信息到此终端。
文件的打开
任何文件使用之前必须打开:
#include <stdio.h>
FILE * fopen(const char * filename, const char * mode);
功能:打开文件
参数:
filename:需要打开的文件名,根据需要加上路径
mode:打开文件的模式设置
返回值:
成功:文件指针
失败:NULL
第一个参数的几种形式:
FILE *fp_passwd = NULL;
//相对路径:
//打开当前目录passdw文件:源文件(源程序)所在目录
FILE *fp_passwd = fopen("passwd.txt", "r");
//打开当前目录(test)下passwd.txt文件
fp_passwd = fopen(". / test / passwd.txt", "r");
//打开当前目录上一级目录(相对当前目录)passwd.txt文件
fp_passwd = fopen(".. / passwd.txt", "r");
//绝对路径:
//打开C盘test目录下一个叫passwd.txt文件
fp_passwd = fopen("c:/test/passwd.txt","r");
第二个参数的几种形式(打开文件的方式):
b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的
Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾
在Windows平台下,以“文本”方式打开文件,不加b,当读取文件的时候,系统会将所有的 “\r\n” 转换成 “\n”,当写入文件的时候,系统会将 “\n” 转换成 “\r\n” 写入 ,以"二进制"方式打开文件,则读和写都不会进行这样的转换
在Unix/Linux平台下,“文本”与“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出
文件的关闭
任何文件在使用后应该关闭
打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存
一个进程同时打开的文件数是有限制的,超过最大同时打开文件数65535,再次调用fopen打开文件会失败
如果没有明确的调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭。
#include <stdio.h>
int fclose(FILE * stream);
功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
stream:文件指针
返回值:
成功:0
失败:-1
打开关闭文件资源的代码示例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r"); 以只读方式打开
if(fp == NULL){
printf("打开失败\n");
return -1;
}
printf("打开成功 %p\n",fp);
fclose(fp); 关闭资源
printf("关闭文件\n");
return 0;
}
文件的顺序读写
按照字符读写文件fgetc、fputc
文件读取
fgetc(FILE* stream) 读取文件中的一个字符
#include <stdio.h>
int fgetc(FILE * stream);
功能:从stream指定的文件中读取一个字符
参数:
stream:文件指针
返回值:
成功:返回读取到的字符
失败:-1
简单的使用一下
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r"); 打开一个文件
if(!fp){ 判断是否打开成功
printf("打开文件失败\n");
return -1;
}
printf("打开文件成功 %p\n",fp);
char ch;
ch = fgetc(fp); 读取一个字符
printf("%c\n",ch);
return 0; 这里没有关闭资源,其实没有关系,
因为程序运行结束,他会自己关闭
}
运行结果
可以看到,读取到了一个字符 ‘h’
那么,读取下一个字符该怎么操作呢?是不是需要给文件指针fp+1呢?
其实不用,因为fgetc()调用的时候,文件中相当于有个光标,会随着函数的调用,自己移动一格,所以如果想要获取后面的字符,直接再调用fgetc()即可
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp){
printf("打开文件失败\n");
return -1;
}
printf("打开文件成功 %p\n",fp);
char ch;
ch = fgetc(fp);
printf("%c\n",ch);
ch = fgetc(fp);
printf("%c\n",ch);
ch = fgetc(fp);
printf("%c\n",ch);
ch = fgetc(fp);
printf("%c\n",ch);
ch = fgetc(fp);
printf("%c\n",ch);
fclose(fp);
printf("关闭资源\n");
return 0;
}
运行结果
如果想要把这个文件读取完,应该以什么作为条件判断呢?也就是说怎么就知道我读取到了文件末尾呢?
文件默认结尾为-1
文件结尾
在C语言中,EOF表示文件结束符(end of file),在while循环中,以EOF作为文件结束标志,
这种以EOF作为文件结束标志的文件,必须是文本文件,
在文本文件中,数据都是以字符的ASCII码值的形式存放,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束的标志
#define EOF (-1)
二进制文件不能以EOF作为文件结束标志,因为二进制存储过程中,会有-1值出现,为了解决这个问题,ANSI C提供一个feof函数,用来判断文件是否结束,feof函数既可以用于判断文本文件又可以判断二进制文件
#include <stdio.h>
int feof(FILE * stream);
功能:检测是否读取到了文件结尾。判断的是最后一次“读操作的内容”,不是当前位置内容(上一个内容)。
参数:
stream:文件指针
返回值:
非0值:已经到文件结尾
0:没有到文件结尾
使用EOF作为文件结尾判断来读取一整个文件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp){
printf("打开文件失败\n");
return -1;
}
char ch;
while((ch=fgetc(fp))!=EOF){
printf("%c",ch);
}
fclose(fp);
return 0;
}
运行结果:hello world
文件写入
写文件,使用fopen的时候,就不能传r了,需要传w
上面表里有说到,w表示的意义是以写方式打开文件,如果文件存在,则清空文件,如果文件不存在,则创建文件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp){
printf("打开文件失败\n");
return -1;
}
char ch = 'a';
fputc(ch,fp);
fclose(fp);
return 0;
}
运行结果:可以看到hello.txt中,之前的hello world已经被清空了,只有一个a
如果以上代码代开文件一行,换成
FILE* fp = fopen("nihao.txt","w");
运行结果就是会新创建一个nihao.txt,里面有一个a
写一个类似于记事本的程序,键盘上输入字符,写入到文件中,知道键盘输入@,程序结束
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp){
printf("打开文件失败\n");
return -1;
}
char ch;
while(1){
scanf("%c",&ch);
if(ch=='@'){
break;
}
fputc(ch,fp);
}
fclose(fp);
return 0;
}
键盘输入
文件内容
练习:文件简单的加密解密
原文件
加密代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp1 = fopen("原文件.txt","r");
FILE* fp2 = fopen("加密.txt","w");
if(!fp1 || !fp2){
return -1;
}
char ch;
while((ch = fgetc(fp1))!=EOF){
ch++;
fputc(ch,fp2);
}
fclose(fp1);
fclose(fp2);
return 0;
}
加密文件:
解密代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp1 = fopen("加密.txt","r");
FILE* fp2 = fopen("解密.txt","w");
if(!fp1 || !fp2){
return -1;
}
char ch;
while((ch = fgetc(fp1))!=EOF){
ch--;
fputc(ch,fp2);
}
fclose(fp1);
fclose(fp2);
return 0;
}
解密文件
按照行读写文件fgets、fputs
读文件
#include <stdio.h>
char * fgets(char * str, int size, FILE * stream);
功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,
最后会自动加上字符 '\0' 作为字符串结束。
参数:
str:字符串
size:指定最大读取字符串的长度(size - 1)
stream:文件指针
返回值:
成功:成功读取的字符串
读到文件尾或出错: NULL
简单的一个示例,演示一下该函数的使用方法
目标文件:hello.txt
代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r"); 打开文件
char* str = (char*)malloc(sizeof(char)*100); 分配缓冲区
memset(str,0,100); 初始化缓冲区
fgets(str,100,fp); 读取一行
printf("%s",str); 打印 :春天来了,把你手拉上
return 0;
}
代码中分配了100个字节的缓冲区,但是实际可以使用到的是99个字节,因为最后一个字节要留着存放\0
如果我们把分配的100个字节缓冲区换成5个字节的
char* str = (char*)malloc(sizeof(char)*5);
memset(str,0,5);
fgets(str,5,fp);
printf("%s",str);
运行结果为“春天”,一个汉字两个字节,剩下一个字节是\0
如果将缓冲区大小改成4,运行结果就是“春”,最后一个字节是\0,前面只剩下3个字节,只够一个半汉字的,所以只显示了一个汉字
思考:上面5个字节大小缓冲区的情况下,我如果再读取一次,是继续读第一行后面的内容呢?还是从下一行开始读?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r");
char* str = (char*)malloc(sizeof(char)*5);
memset(str,0,5);
fgets(str,5,fp);
printf("%s",str);
//第二次读取
memset(str,0,5); 把之前读取的结果缓存清理了,防止影响读取结果
fgets(str,5,fp);
printf("%s",str);
return 0;
}
运行结果为“春天来了”
所以在读取的过程中,如果因为缓冲区大小限制,导致一行没读完,没有遇到\n,他会保留光标位置,下次读取的时候,继续从光标位置开始读,直到遇到\n
将一个文件中的所有内容全部读出来
这里涉及到一个问题,怎么判断是否读取到了文件的结尾。
前面《文件结尾》说过,有一个feof函数,用来判断是否到了文件结尾处,
如果没有到文件结尾返回0,如果到了文件结尾返回非0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp)
return -1;
char* str = (char*)malloc(sizeof(char)*100);
while(!feof(fp)){
memset(str,0,100);
fgets(str,100,fp);
printf("%s",str);
}
free(str);
fclose(fp);
return 0;
}
运行结果:
写文件
#include <stdio.h>
int fputs(const char * str, FILE * stream);
功能:将str所指定的字符串写入到stream指定的文件中,字符串结束符 '\0' 不写入文件。
参数:
str:字符串
stream:文件指针
返回值:6
成功:0
失败:-1
简单的使用一下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
char ch[] = "春天来啦把你手拉上";
fputs(ch,fp);
fclose(fp);
return 0;
}
如果将字符串改为:
char ch[] = "春天来啦\n把你手拉上";
运行结果就会是
“春天来啦
把你手拉上”
如果将字符串改为:
char ch[] = "春天来啦\0把你手拉上";
运行结果就会是
“春天来啦”
所以fputs函数对字符串的操作还是基于字符串的,跟字符串是一样的
通过键盘输入,将内容写入到文件中
思路,通过scanf接收键盘输入,直接使用fputs函数写入到文件中,先简单写一下,看看效果,有问题再修改
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
char* str = (char*)malloc(sizeof(char)*1024);
while(1){
memset(str,0,1024);
scanf("%s",str);
if(!strcmp(str,"comm=exit")){
break;
}
fputs(str,fp);
}
fclose(fp);
return 0;
}
使用键盘输入
文件中的内容为
发现没有空格,也没有换行,
分析原因,是scanf接收到空格和换行 就认为结束了这一次接收过程,,可以理解为是scanf把空格和换行吞噬了,
于是想到了,以前用的scanf通过正则表达式接收非\n,来接收空格
scanf("%{^\n}",str);
再次运行
发现才按了第一行,回车之后就动不了了,这是为什么呢?
分析原因,是因为scanf的处理机制造成的,scanf默认遇到空格和回车,就将键盘输入的内容赋值到scanf的第二个参数字符串中,
scanf加上正则表达式%[^\n],他可以遇到空格也不提交,等遇到回车的时候才提交
但是scanf提交归提交,他并不接收或者处理回车,回车事件还在缓存中
从代码流程来看,在while循环中,当输入nihao shijie一按回车,scanf将字符串提交了,但是回车事件还在缓存中,之后,代码继续往下走,经过判断和文件写入,又一次循环来到了scanf阻塞函数,等待键盘输入。
这个时候,因为回车还在缓存中,没有被处理掉,scanf就又遇到了这个回车,直接认为提交了,依然不处理回车事件,继续往下走,如此在while中循环,就造成了动不了的结果。
解决办法之一是,在scanf之后,加一个
getchar();
scanf不处理的回车,这里用一个getchar接收了,就没有缓存的回车了,程序就可以正常运行了
除了getchar()也可以使用fflush(stdio)清空键盘缓存,如果这里还没看明白为什么,可以参考这篇博客scanf后面为啥加getchar
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
char* str = (char*)malloc(sizeof(char)*1024);
while(1){
memset(str,0,1024);
scanf("%[^\n]",str); 除了\n啥都能接
getchar(); 专接\n
if(!strcmp(str,"comm=exit")){
break;
}
fputs(str,fp);
}
fclose(fp);
return 0;
}
运行结果为
可以看到,没有回车,
是因为回车被getchar()接收了,所以这里需要单独加一个回车符号
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
char* str = (char*)malloc(sizeof(char)*1024);
while(1){
memset(str,0,1024);
scanf("%[^\n]",str);
getchar();
if(!strcmp(str,"comm=exit")){
break;
}
strcat(str,"\n"); 在str后面增加一个回车符号
fputs(str,fp);
}
fclose(fp);
return 0;
}
运行结果
强化训练 文件版四则运算
生成一个文件,100行,每行内容都是一个四则运算表达式,还没有算出结果,写一个程序,自动算出其结果后修改文件。
生成文件:
思路:首先需要在一个for循环中生成随机数a和b,
然后生成加减乘除也可以使用随机数的方式,利用枚举
最后利用sprintf的方式,生成字符串,写入到文件中
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
enum opt{ 利用枚举随机生成加减乘除运算符
add,sub,mul,dive
};
int main(void){
srand((size_t)time(NULL)); 随机数种子
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
int a,b;
char ch;
char* str = (char*)malloc(sizeof(char)*20);
for(int i = 0;i<100;i++){
a = rand()%10+1; 随机生成两个数
b = rand()%10+1;
switch(rand()%4){ 随机生成4个数 0 1 2 3分别代表加减乘除
case add:
ch = '+';
break;
case sub:
ch = '-';
break;
case mul:
ch = '*';
break;
case dive:
ch = '/';
break;
}
memset(str,0,20);
sprintf(str,"%d%c%d=\n",a,ch,b); 组拼成字符串,写入到文件中
fputs(str,fp);
}
free(str);
fclose(fp);
return 0;
}
运行结果
计算结果文件生成:
思路:读取算术题的文件,一行一行读出来,因为每个格式都一样,所以使用sscanf函数,
获取到a b 以及是属于哪种运算方式,switch使用运算方式作为判断条件,计算每个题的结果,
再次利用sprintf函数生成字符串,写入到另一个文件中
代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int main(void){
int a,b,value;
char ch;
FILE* fp1 = fopen("hello.txt","r"); 读旧文件
FILE* fp2 = fopen("world.txt","w"); 写新文件
if(!fp1 || !fp2){
return -1;
}
char* str = (char*)malloc(sizeof(char)*20);
while(!feof(fp1)){ 判断是否是文件结尾
memset(str,0,20);
fgets(str,20,fp1);
sscanf(str,"%d%c%d=\n",&a,&ch,&b); 获取到题中的两个数和运算方式
switch(ch){ 根据运算方式计算结果
case '+':value = a + b;break;
case '-':value = a - b;break;
case '*':value = a * b;break;
case '/':value = a / b;break;
}
memset(str,0,20);
sprintf(str,"%d%c%d=%d\n",a,ch,b,value); 将结果合成新的字符串
fputs(str,fp2); 写入到文件中
}
free(str); 回收资源
fclose(fp1);
fclose(fp2);
return 0;
}
运行结果:
按照格式化读写文件 fprintf fscanf
读文件
#include <stdio.h>
int fscanf(FILE * stream, const char * format, ...);
功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
参数:
stream:已经打开的文件
format:字符串格式,用法和scanf()一样
返回值:
成功:参数数目,成功转换的值的个数
失败: - 1
简单的使用一下:
hello.txt
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp)
return -1;
char str[100];
fscanf(fp,"%s",str); 读取fp文件,然后以字符串的形式保存到str中
printf("%s\n",str); 运行结果:春天来啦
fclose(fp);
return 0;
}
发现只读取了一行,同理我们继续读取文件剩下的部分
FILE* fp = fopen("hello.txt","r");
if(!fp)
return -1;
char str[100];
fscanf(fp,"%s",str);
printf("%s\n",str);
fscanf(fp,"%s",str);
printf("%s\n",str);
fscanf(fp,"%s",str);
printf("%s\n",str);
fclose(fp);
return 0;
运行结果为:
发现最后少三个字,为啥呢?因为这三个字前面有个空格,fscanf遇到空格停下来了
结合上一个例子,fscanf和scanf是一样的,都是遇到空格和回车就停止
再加一个读取操作,剩下的3个字就读出来了。
如果给文件加上一个数字,同样的方式,也是能读出来的
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp)
return -1;
char str[100];
int a;
fscanf(fp,"%d",&a);
printf("%d\n",a);
fscanf(fp,"%s",str);
printf("%s\n",str);
fscanf(fp,"%s",str);
printf("%s\n",str);
fscanf(fp,"%s",str);
printf("%s\n",str);
fclose(fp);
return 0;
}
运行结果:
假如前面的四则运算题里面,我要把里面的所有int类型数据都读出来
假如hello.txt中有一行 5+8=13,将这3个数取出并打印
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp)
return -1;
int a,b,c;
char ch;
fscanf(fp,"%d%c%d=%d",&a,&ch,&b,&c);
printf("%d\n",a);
printf("%d\n",b);
printf("%d\n",c);
fclose(fp);
return 0;
}
运行结果:
写文件
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0' 为止。
参数:
stream:已经打开的文件
format:字符串格式,用法和printf()一样
返回值:
成功:实际写入文件的字符个数
失败:-1
简单的使用一下:将一串字符串通过fprintf写入到文件中
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
char ch[] = "hello world";
fprintf(fp,"%s",ch);
fclose(fp);
return 0;
}
如果将上述写入的内容再读出来:
int main(void){
FILE* fp = fopen("hello.txt","r");
if(!fp)
return -1;
char ch[20] ;
fscanf(fp,"%s",ch);
printf("%s\n",ch);
fclose(fp);
return 0;
}
运行结果只有hello,因为fscanf和scanf一样,遇到空格就停下来了
如果想让fscanf可以遇到空格不停下来,也可以,还是一样的方式:
fscanf(fp,"%[^\n]",ch);
如果用这种方式读取:
fscanf(fp,"%12s",ch);
读取出来的还是hello 因为不管s前面加什么数组,始终迈不过fscanf遇到空格就停的坎
改成用:
fscanf(fp,"%12c",ch);
就同样可以遇到空格也不停,因为他是按照字符读取的,只当空格也是一个字符 ,但是这种操作需要将ch初始化为0,因为有一个字符结束标志的原因
如果要读取hel 3个字符
fscanf(fp,"%3c",ch);
fscanf(fp,"%3s",ch);
都可以 ,3s是读取的时候 他已经给初始化好\0了,3c不会初始化\0,需要自己初始化
把一个int类型的数写入到文件中
int main(void){
FILE* fp = fopen("hello.txt","w");
if(!fp)
return -1;
int a = 10;
fprintf(fp,"%d",a);
fclose(fp);
return 0;
}
运行结果 10
修改代码:
fprintf(fp,"%05d",a);
运行结果 00010
结论就是,fsanf fprintf 沿袭了scanf和printf的全部特点
学了这两个函数之后,前面的四则运算就可以写的更便捷了。
练习:随机生成1000个0-256以内的数,将他写在一个文件中,再在另一个文件中给他排好序 写进去
生成文件:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
int main(void){
srand((size_t)time(NULL));
FILE* fp = fopen("数据.txt","w");
if(!fp)
return -1;
for(int i = 0;i<1000;i++){
fprintf(fp,"%d\n",rand()%257);
}
fclose(fp);
return 0;
}
生成排序后的文件:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "util.h"
#define SIZE 1000
int main(void){
FILE* fp1 = fopen("数据.txt","r");
FILE* fp2 = fopen("排序.txt","w");
if(!fp1 || !fp2)
return -1;
int* arr = malloc(sizeof(int)*SIZE);
memset(arr,0,sizeof(int)*SIZE); 因为memset函数是以字节进行初始化的,所以这里不能直接写1000
for(int i=0;i<SIZE;i++){
fscanf(fp1,"%d\n",&arr[i]);
}
BubbleSort(arr,SIZE);
for(int i = 0;i<SIZE;i++){
fprintf(fp2,"%d\n",arr[i]);
}
fclose(fp1);
fclose(fp2);
return 0;
}
运行结果:
运行结果没有问题,但是思考这样的写法会不会有什么不太好的地方,比如说,生成1000个随机数 循环1000次,将这个数读到arr中,又循环1000次,写入文件 1000次 排序 499500次 这也太多了,虽然计算机运算速度快,但是也不能这么造!想想有没有别的思路,可以计算次数少一些的
思路:因为这1000个数,大小在0-256范围,所以是不是可以定义一个257大小的数组arr,arr[3]= 5 表示3这个数出现了5次 这样,就不用写排序算法了
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "util.h"
#define SIZE 1000
int main(void){
FILE* fp1 = fopen("数据.txt","r");
FILE* fp2 = fopen("排序.txt","w");
if(!fp1 || !fp2)
return -1;
int* arr = malloc(sizeof(int)*257); 创建一个257大小的数组
int value;
memset(arr,0,sizeof(int)*257); 初始化数组
for(int i=0;i<SIZE;i++){
fscanf(fp1,"%d\n",&value); 从文件中读取数据,赋值给value
arr[value]++; value当做角标,对应的arr的值自增
}
for(int i = 0;i<257;i++){
for(int j = 0;j<arr[i];j++){
fprintf(fp2,"%d\n",i); 这里注意,写入的是i的值,
}
}
free(arr);
fclose(fp1);
fclose(fp2);
return 0;
}
运行结果 和冒泡排序的结果一样,但是循环次数明显少了好几个量级
按照块读写文件 fread fwrite
块读写是一个基于二进制的操作,和文本操作不一样
前面的内容学习到,在Windows平台下 操作二进制文件的打开方式是 wb和rb,Linux平台下 r和rb效果一样
写文件
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容
参数:
ptr:准备写入文件数据的地址
size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小
nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功写入文件数据的块数目,此值和 nmemb 相等
失败:0
简单的写个例子将5678以二进制的方式写入到hello.txt中
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","wb");
int a = 5678;
fwrite(&a,sizeof(int),1,fp); 如果要写一个数组进去的话 这里的参数1要改成数组个数
fclose(fp);
return 0;
}
运行结果
读文件
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
ptr:存放读取出来数据的内存空间
size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小
nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
失败:0
简单的试验一下将上面写入文件的5678二进制码读出来
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","rb");
int value;
fread(&value,sizeof(int),1,fp);
printf("%d\n",value);
fclose(fp);
return 0;
}
运行结果:
有了上面的例子,我们可以试试把一个数组写入到文件中 并读出来
写文件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","wb");
int arr[] = {1,2,3,4,5,6,7,8,9,10};
fwrite(arr,sizeof(int),10,fp);
fclose(fp);
return 0;
}
写好的文件:
读文件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main(void){
FILE* fp = fopen("hello.txt","rb");
int arr[10] = {0};
fread(arr,sizeof(int),10,fp);
for(int i=0;i<10;i++){
printf("%d\n",arr[i]);
}
fclose(fp);
return 0;
}
运行结果:
关于fread的第二个和第三个参数,不必很遵循第二个参数是元素的大小,第三个参数是元素个数,只要保证他俩的乘积相同,也就是说保证告诉函数总共有多少个字节,运行结果不受影响,但是为了便于阅读代码,还是要按规定写
fread(arr,sizeof(int),10,fp);
fread(arr,4,10,fp);
fread(arr,10,4,fp);
fread(arr,5,8,fp); 写这几种方式都行
运行结果都不受影响
写一个将结构体写入二进制文件中
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct student{
char name[21];
int age;
int score;
char addr[51];
}student;
int main(void){
FILE* fp = fopen("hello.txt","wb");
if(!fp)
return -1;
student stu[3]={
{"张三",21,99,"张家庄"},
{"李四",21,97,"李家屯"},
{"王五",21,90,"王家坳"}
};
for(int i=0;i<3;i++){
fwrite(&stu[i],sizeof(student),1,fp);
}
fclose(fp);
return 0;
}
再将结构体从文件中读取出来
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct student{
char name[21];
int age;
int score;
char addr[51];
}student;
int main(void){
FILE* fp = fopen("hello.txt","rb");
if(!fp)
return -1;
student* pstu = (student*)malloc(sizeof(student)*3);
int i = 0;
while(!feof(fp)){ feof函数可以判断文本文件和二进制文件是否到达文件结束标志
fread(pstu+i,sizeof(student),1,fp);
i++;
}
for(int i=0;i<3;i++){
printf("%s ",pstu[i].name);
printf("%d ",pstu[i].age);
printf("%d ",pstu[i].score);
printf("%s \n",pstu[i].addr);
}
free(pstu);
fclose(fp);
return 0;
}
强化练习:大文件读写
写一个程序,可以通过命令行的方式将一个大概20M的文件,拷贝出来一份
首先需要主函数的参数做出改变
int main(int argc,char* argv[]){
for(int i=0;i<argc;i++){ 打印参数
printf("%s\n",argv[i]);
}
return 0;
}
在代码目录中使用gcc将代码编译成.exe格式,然后在命令行中执行以下代码
helloworld.exe C:\test.avi D:\test.avi
运行结果:
可以看到,main函数中argv数组的第一个是程序名称,第二个第三个一次是后面的文件名称所以开始写代码 进行拷贝
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define SIZE 1024
int main(int argc,char* argv[]){
FILE* fp1 = fopen(argv[1],"rb");
FILE* fp2 = fopen(argv[2],"wb");
if(!fp1 || !fp2){
printf("操作失败\n");
return -1;
}
if(argc<3){
printf("缺少参数\n");
return -2;
}
//开始读写
char* ptemp = (char*)malloc(sizeof(char)*SIZE);
while(!feof(fp1)){
memset(ptemp,0,SIZE);
fread(ptemp,1,SIZE,fp1);
fwrite(ptemp,1,SIZE,fp2);
}
free(ptemp);
fclose(fp1);
fclose(fp2);
printf("拷贝成功\n");
return 0;
}
执行拷贝命令
两个文件比对结果
发现有点不对,总体上,大小是差不多,但是具体的多少字节不一致,分析一下原因
问题可能出现在拷贝的操作上,拷贝一次大小是SIZE 也就是1024个字节,但是源文件不一定是1024字节的整数倍,
假如说,最后一次拷贝,剩下100个字节,我们用1024个字节读了,ptemp中只有100个有效字节,其他都是无效字节,但是最后写的过程是按照1024个字节写进去了,所以目标文件会比源文件多出来不到1024个字节,
解决办法:我们注意到fread有返回值,返回值是成功读取到的块数,那么我们可以拿这个返回值去给了fwrite的参数,fread读了多少,fwrite就写多少
//开始读写
char* ptemp = (char*)malloc(sizeof(char)*SIZE);
int count = 0;
while(!feof(fp1)){
memset(ptemp,0,SIZE);
count = fread(ptemp,1,SIZE,fp1);
fwrite(ptemp,1,count,fp2);
}
继续执行命令,查看结果:
可以看到,拷贝成功