文件的操作
只要放在磁盘上的都是文件。 程序设计中的文件有程序文件、数据文件两种。 ::: tip 程序文件:包含源程序文件(后缀为.c、.h等)、目标文件(Windows环境后缀.obj)、可执行程序(Windows环境后缀.exe),是可以执行编译的文件。 数据文件:文件的内容不一定是程序,而是程序运行时读写的数据,如程序运行需要从中 读写的数据的文件,或者输出内容的文件。 ::: 程序文件可以操作数据文件。
文件名是文件的唯一文件标识(即文件名),包含文件路径、文件名主干、文件后缀。如下:
根据数据的组织形式,数据文件称为文本文件或二进制文件。 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式存储的文件就是文本文件。即任何数据都可以以二进制或文本的形式存储。如果内存中的二进制形式不加任何转换放在外存中,就是二进制文件;如果内存中的数据转换成ASCII码的形式存放在外存中,就是文本文件。 ::: warning 数据在内存中是以二进制的形式存储的。
数值是以补码表示。 字符的存储是把字符相对应的ASCII代码放到存储单元中,这些ASCII代码在内存中以二进制形式存放。 ::: 例如一个整数10000,如果以二进制的形式存储到磁盘中,一个int占据4个字节;如果以文本文件的形式存储,每一个数字都是一个字符占用一个字节,共占用5字节。如下: ::: tip 十进制数转ASCII值再转二进制。 1的ASCII值是49,转二进制是00110001。 0的ASCII值是48,转二进制是00110000. ::: 测试代码:
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
上述代码的作用是创建一个文件test.c,将a以二进制形式放入文件。 ::: tip 代码分析: fopen打开文件,文件名为test.c,wb的含义是以二进制形式写入。 当test.c文件不存在时,fopen会创建这个文件。 fwrite写文件,fwrite(&a, 4, 1, pf);表示数据来自于a地址处,写入4个字节,写入一个这样的数据,即写1个4字节的数据,放在pf维护的文件中即test.c文件中。. fclose关闭文件,关闭后pf置为空指针。 ::: 内存中的二进制数放在磁盘中打开是以16进制呈现的,上述代码会在代码保存路径下新建文件test.txt,用VS的二进制形式打开后是“10 27 00 00”,内存中是小端存储模式,实际存储为00 00 27 10,将16进制数2710转为10进制,为10000。
文件缓冲区:
ANSIC标准采用“缓冲文件系统”处理的数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。 缓冲区的大小根据C编译系统决定的。
文件指针 文件类型指针简称文件指针,是缓冲文件系统中关键概念。 每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息保存在一个结构体变量中,该结构体类型是有系统声明的,取名FILE。即文件信息区对应的是FILE类型的变量。 ::: tip FILE类型创建一个变量,该变量向内存中申请一块空间,该变量和相应的文件进行关联,把文件的相关信息存储在该内存空间中。 ::: 每打开一个文件,系统会根据文件情况自动创建一个FILE结构的变量并填充信息。 一般使用FILE类型的指针来维护这个FILE结构变量。 上述代码中的“FILE* pf”就是创建了一个FILE类型的指针pf。 对文件所做的任何操作都会更改文件的文件信息区。
编写程序时,在打开文件的同时,都会返回一个FILE*的指针指向该文件,相当于建立了指针和文件的关系。 ANSIC规定使用fopen打开文件,fclose关闭文件。 filename为文件名,传递文件名实际是文件名的首字符地址,使用char * 类型。 mode为打开方式。 下图为不同的打开方式: ::: warning 对文件的操作是“w”时,如果要写的文件名已经在该目录下有同名文件,fopen会销毁旧的同名文件,建立一个新文件。 :::
打开文件时的两种写法:
//绝对路径:
FILE* pf = fopen("C:\\2020\\code\\test.txt", "r");
//相对路径
FILE* pf = fopen("test.txt", "r");
::: tip 当要打开的文件和代码文件在同一个路径下时,可以写成相对路径的形式。 绝对路径写法时,“\”会被识别成转义字符,需要在“\”后再加一个“\”,对每一个“\”都进行转义,就不会再识别成转义字符了。 :::
创建文件test.txt时,系统会自动创建填充test.txt的文件信息区,并将文件信息区的起始地址返回给pf。 ::: warning 当打开文件失败时,FILE结构体不会创建(不创建文件信息区),fopen返回空指针,pf中填充NULL。 ::: 读文件完毕后需要关闭文件释放空间,fclose(pf)是值传递,不改变pf的值,需要手动pf=NULL,类似于free释放空间。 读文件的全过程:
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("../test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//文件打开成功
//读文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
文件的顺序读写
相关函数: 写入: 读取: 流即文件。 向文件中写入内容(w)是文件输出流,读取内容(r)是文件输入流。 ::: tip 流的输入输出是相对于内存而言的。 当写文件时实际是从内存输出磁盘中,故用的是输出流;当读文件时实际是从磁盘输入到内存,故用的是输入流(入内存即输入,出内存即输出)。 :::
当一个程序运行时,会默认打开3个流,stdin、stdout、stderr,都是FILE * 类型的。 stdin是标准输入设备,即键盘。 stdout是标准输出设备,即屏幕。
从键盘读取,从屏幕输出:
int main()
{
int ch = fgetc(stdin);
fputc(ch, stdout);
return 0;
}
h
h
先用键盘输入一个字符,屏幕会紧接着打印出来。
参考文章: https://blog.csdn.net/wo12369874/article/details/124518922
读文件:
#include <errno.h>
#include <string.h>
int main()
{
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
//fputc('H', p);
//fputc('i', p);
//读文件
int ch = fgetc(p);
printf("%c ", ch);
ch = fgetc(p);
printf("%c ", ch);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
H i
::: tip 如上述代码,fgetc在读取时,每次读取一个字符,连续读写时可以自动向下跳。 :::
stream:流,文件。 str:数据存储位置。从文件读取的信息放入str指向的字符串中。 n:最多读取n个字符。
#include <string.h>
#include <errno.h>
int main()
{
FILE* ph = fopen("test.txt", "r");
if (ph == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
char pph[1024] = { 0 };
//char* fgets(char* str, int num, FILE * stream);
fgets(pph,1024,ph);
printf("%s", pph);
fgets(pph, 1024, ph);
printf("%s", pph);
fclose(ph);
ph = NULL;
return 0;
}
Hi
Cat
上述代码第14行打印时可以不加“\n”,原因是原文件中第一行后自带一个换行符,fgets读取第一行时会把末尾的“\n”也读到pph中打印出来。
puts:写一行到标准输出(屏幕)。
#include <string.h>
#include <errno.h>
int main()
{
FILE* ph = fopen("test.txt", "r");
if (ph == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
char pph[1024] = { 0 };
//char* fgets(char* str, int num, FILE * stream);
fgets(pph,1024,ph);
puts(pph);
fgets(pph, 1024, ph);
puts(pph);
fclose(ph);
ph = NULL;
return 0;
}
Hi
Cat
puts在打印完一行内容后会自动换行。
fputs:写一行内容到流中。
#include <string.h>
#include <errno.h>
int main()
{
FILE* ph = fopen("test.txt", "w");
if (ph == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
fputs("Hello", ph);
fputs("World", ph);
fclose(ph);
ph = NULL;
return 0;
}
fputs在写入时,如果需要换行,需要手动加上“\n”。
::: tip 文本行输入输出函数(fgets、fputs)一次操作一行内容。 :::
fgets和fputs适用于任何输入输出流:
int main()
{
char buf[100] = { 0 }; //从键盘读取一行文本信息
//fgets(buf,100,stdin); //从标准输入流读取
//fputs(buf, stdout); //输出到标准输出流
gets(buf);
puts(buf);
return 0;
}
gets、puts和fgets、fputs的区别是前者是默认是从标准输入读,输出到标准输出上,期间没有发生过传参。
格式化输入输出: 即将有格式的数据的写入读取。 FILE*stream:要操作的文件流的指针。 后面的内容为:格式、内容。 ::: tip 对比printf和fprintf: ::: printf默认针对标准输出流(stdout)。 fprintf可以针对标准输出流或者指定的文件流。
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 20,3.14f,"Hello" }; //3.14f表示是浮点型
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
return 0;
}
//格式化形式写入文件
fprintf(pf, "%d %f %s", s.n, s.score, s.arr);
//将结构体s中的信息写入到文件test.txt中
return 0;
}
上述代码第16行写入数据时可以调整写入顺序,只要格式和内容对应即可。文件中的数据顺序和写入顺序一致。 ::: tip 对比scanf和fscanf: :::
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//格式化输入数据(从文件中读取的数据放入结构体s)
fscanf(pf, "%d %f %s", &(s.n), &(s.score), s.arr);
printf("%d %f %s\n", s.n, s.score, s.arr);
return 0;
}
20 3.140000 Hello
fscanf和fprintf适用于任何输入输出流:
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 0 };
fscanf(stdin, "%d %f %s", &(s.n), &(s.score), s.arr); //标准输入
fprintf(stdout,"%d %f %s\n", s.n, s.score, s.arr); //标准输出
return 0;
}
1024 3.14 hi
1024 3.140000 hi
::: tip 上述代码中第10行,需要输入的数据内容的地址,s.arr本身就是数组s.arr[]的地址。 上述代码中第11行,如果需要控制打印的浮点数位数,比如精确控制打印出3.14,可以在%f之间加上“.2”,即%.2f可以打印两位浮点数。 :::
::: tip 一组函数的对比: scanf/printf:针对标准输入流/标准输出流的格式化输入/输出语句。 fscanf/fprintf:针对所有输入流/所有输出流的格式化输入/输出语句。 所有输入流/所有输出流包含标准输入/输出流和文件输入/输出流。
::: sscanf和sprintf
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 20,3.14f,"Hello" };
char buf[100];
sprintf(buf, "%d %f %s", s.n, s.score, s.arr);
printf("%s", buf);
return 0;
}
20 3.140000 Hello
上述代码输出的结果是结构体数据转换成字符串的形式输出的。
struct S
{
int n;
float score;
char arr[10];
};
int main()
{
struct S s = { 20,3.14f,"Hello" };
struct S tmp = { 0 };
char buf[100];
//把格式化的数据转换成字符串存储到buf中
sprintf(buf, "%d %f %s", s.n, s.score, s.arr);
//从buf中读取格式化的数据到tmp中
sscanf(buf, "%d %f %s", &(tmp.n), &(tmp.score), tmp.arr);
printf("%d %f %s\n", tmp.n, tmp.score, tmp.arr);
return 0;
}
20 3.140000 Hello
上述代码输出的结果是字符串形式转换成结构体形式输出的。
二进制输入输出: ptr:指向要写入的元素数组的指针,转换为const void * 。 size:要写入的每个元素的大小(以字节为单位)。size_t是无符号整数类型。 count:元素的数量,每个元素的大小为size字节。size_t是无符号整数类型。
二进制输入
struct S
{
char name[20];
int age;
double score;
};
int main()
{
struct S s = { "张三",26,66.8 };
FILE* pf = fopen( "test.txt","wb" );
if (pf == NULL)
{
return 0;
}
//二进制形式写入
fwrite(&s, sizeof(struct S), 1,pf);
fclose(pf);
pf = NULL;
return 0;
}
上述代码输出结果乱码的原因是以二进制形式输出的。
struct S
{
char name[20];
int age;
double score;
};
int main()
{
//二进制形式读取
struct S tmp = { 0 };
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
return 0;
}
fread(&tmp, sizeof(struct S), 1, pf);
printf("%s %d %lf\n", tmp.name, tmp.age, tmp.score);
fclose(pf);
pf = NULL;
return 0;
}
张三 26 66.800000
上述代码输出结果就是test.txt中二进制写入的内容。
::: tip 上述代码中一次可以写入读取一个数据,如果需要读取多个数据,上述代码中的“s”、“tmp”必须有能力存储多个元素,即可以是一个数组,同时后面的参数count也要改为需要读取的元素数量。 :::
fread的返回值: ::: warning 上图的意思是:假设参数count设置为10,如果返回5,则本次只读到5个元素。假设文件中有25个元素,第一次第二次都读到了10个元素,第三次读到了剩下的5个元素,即函数fread返回的值是真实读到的元素个数。 如果fread的返回值小于count参数的值,说明文件中的元素都被读完。 :::
文件的随机读写
fseek 函数fseek的功能是根据文件指针的位置和偏移量来定位文件指针。(移动一个文件指针到指定位置) stream:要调整位置的文件指针。 offset:偏移量,单位字节。 偏移量为正则向后偏移,偏移量为负则向前偏移。 origin:文件指针的当前位置。 起始位置origin的三个选项: 如果没有指定偏移,默认的文件指针位置是起始位置。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//定位文件指针
fseek(pf, 2, SEEK_SET);
//读取文件
int ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
当test.txt中内容为abcdef时,上述代码输出结果为“c”。
ftell 函数ftell的功能是返回文件指针相对于起始位置的偏移量。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
//定位文件指针
fseek(pf, -2, SEEK_END);
//输出文件指针偏移量
int pos = ftell(pf);
printf("%d\n", pos);
fclose(pf);
pf = NULL;
return 0;
}
4
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
fgetc(pf);
int pos = ftell(pf);
printf("%d\n", pos);
fclose(pf);
fclose(pf);
pf = NULL;
return 0;
}
上述代码中,第8、9行代码,fgetc读取一个字符后,指针向后跳一个字符,所以ftell输出指针当前位置和起始位置的偏移量时输出值为2。
rewind 函数rewind的功能是让文件指针回到文件的起始位置。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
printf("%c\n", fgetc(pf));
printf("%c\n", fgetc(pf));
rewind(pf);
printf("%c\n", fgetc(pf));
fclose(pf);
pf = NULL;
return 0;
}
a
b
a
上述代码中,第8、9行代码执行后,指针向后跳2个字符,指向“c”。第10行代码将文件指针回到文件的起始位置,第11行代码读取到的一个字符又变为起始位置的字符a。
文件结束判定
feof ::: warning feof不是EOF。
在文件读取过程中,不能用feof函数的返回值直接判断文件是否结束。 而是应该应用于当文件读取结束时判断是读取失败还是遇到文件末尾结束。 ::: 判断读取结束的原因:
应用场景如下: 当文本文件返回值EOF(读取结束)时,需要使用feof判断结束原因。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
return 0;
}
int ch = fgetc(pf);
printf("%d\n", ch);
return 0;
}
-1
::: tip 涉及到perror perror的功能类似于strerror,但是更加简便。如下打开一个不存在的文件:
int main()
{
FILE* ph = fopen("test1.txt", "r");
if (ph == NULL)
{
perror("hehe");
return 0;
}
fclose(ph);
ph = NULL;
return 0;
}
hehe: No such file or directory
如上,代码输出时会先输出perror后括号内的内容,加上冒号和空格,然后输出错误码对应的错误信息。 :::
feof使用场景:
int main()
{
FILE* ph = fopen("test.txt", "r");
if (ph == NULL)
{
perror("hehe");
return 0;
}
//读取文件
int ch = 0;
while ((ch = fgetc(ph) != EOF))
{
putchar(ch);
}
if (ferror(ph))
{
printf("error\n");
}
else if (feof(ph))
{
printf("end of file\n");
}
fclose(ph);
ph = NULL;
return 0;
}
二进制文件的判断时使用fread读取文件。