C语言-文件操作
于高山之巅,方见大河奔涌
于群峰之上,更觉长风浩荡
目录
scanf/fscanf/sscanf printf/fprintf/sprintf
接下来我将带领大家一起去学习文件使用的方法~
?在进入本章知识讲解之前,大家在以往的编程中是不是都有这样的困惑
这里用比较简单的代码进行展示:
#include<stdio.h>
int main()
{
int n = 0;
printf("%d\n", n);
scanf("%d", &n);
printf("%d", n);
return 0;
}
n的初始值为0,我给n赋值为5
但我下一次打开程序,n的值仍然是0
因为n是内存中的数据,一旦程序结束空间就还给操作系统了
数据不能持久性的保存
这就是我们使用文件的原因:
持久化的保存数据
文件的简单介绍:
使用文件的原因:
我们写的程序的数据是 存储在电脑的内存 中,如果 程序退出,内存回收,数据就丢失 了,等再次运行程序,是看不到上次程序的数据的,如果要将 数据进行持久化的保存 ,我们可以使文件
文件的类型:
1.程序文件源程序文件(后缀为.c)目标文件(windows环境后缀为.obj)可执行程(windows环境后缀为.exe)2.数据文件文件的内容不?定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
注意:本篇讨论的都是数据文件
文件名 ?
一个文件要有一个唯一的文件标识,以便用户识别和引用文件名包含3部分: 文件路径+文件名主干+文件后缀例如: c:\code\test.txt为了方便起见, 文件标识常被称为文件名
文本文件与二进制文件
二进制文件:数据在内存中以二进制的形式存储,不加转换的输出到外存
以下是记事本是以文本文件的形式打开是读不懂二进制的所以产生乱码
文本文件:外存上以ASCII码的形式存储,硬盘上以ASCII字符的形式存储的文件
以下是文本文件把二进制信息转换成对应的ASCII码值的形式
一个数据在内存中存储的方式:
这里看不懂没有关系我们用代码来实现以下:
整数10000:如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符占一个字节),而二进制形式输出,则在磁盘上只占4个字节
我们这里用二进制的形式打开一个文件
"wb"就是以二进制的形式打开一个文件
#include <stdio.h> int main() { int a = 10000; FILE* pf; pf = fopen("test_12_15.txt", "wb"); fwrite(&a, 4, 1, pf); fclose(pf); pf = NULL; return 0; }
我们发现这个文件以记事本的形式打开是一推乱码
所以它是一个二进制文件
我们在编译器上以二进制的形式读取
我们发现10000在二进制的存储的形式是10 27 00 00
我们知道在VS的编译器下内存是以小端存储的
所以要倒过来看:10 27 00 00
总结:
我们发现二进制文件不加转换的输出到外存
以ASCII字符的形式存储的文件就是文本文件
以上内容大家以了解的形式看就行,我们实际编程中用到不多
接下来来到我们的重点
文件的打开和关闭
流和标准流
流的概念:
流是一个系统与一个程序之间形成的一个通道:C程序针对文件、画面、键盘等的数据输?输出操作都是通过流操作的。 一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
?标准流的概念:
C语言程序在启动的时候,默认打开了3个流:? stdin - 标准输入流,在大多数的环境中从键盘输?, scanf函数就是从标准输入流中读取数据? stdout - 标准输出流,大多数的环境中输出至显示器界面, printf函数就是将信息输出到标准输出流中? stderr - 标准错误流,大多数环境中输出到显示器界面
哪有什么岁月静好,不过是有人替你负重前行
流就是这样,因为有了流scanf、printf等函数就可以直接进行输入输出操作的
stdin、stdout、stderr 三个流的类型是: FILE* ,通常称为文件指针FILE* 的文件指针是用来维护流的各种操作的
文件指针 ?
文件指针?FILE*?的概念:
这个指针是用来存放文件的首地址的
每个被使用的文件都在内存中开辟了?个相应的文件信息区,用来存放文件的相关信息。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE
我们一般都是?个FILE的指针来维护这个FILE结构的变量?,例如:
FILE* pf; //文件指针变量
文件的打开和关闭 ?
1.文件在读写之前应该先打开文件,在使用结束之后应该关闭文件
2.在打开文件的同时,都会返回?个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系3. fopen 函数来打开文件, fclose 来关闭文件
fopen打开文件函数:
FILE *fopen(const char *filename, const char *mode)
- filename?-- 字符串,表示要打开的文件名称
mode--表示文件的打开模式,下面都是文件的打开模式:?
fclose关闭函数:
int fclose(FILE *stream)
stream?-- 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流
#include <stdio.h>
int main()
{
//打开文件
FILE* fp = fopen("data.txt", "w");
//文件操作
if (fp == NULL)
{
perror("fopen");
exit(0); //输入错误,直接退出
}
//关闭文件
fclose(fp);
fp = NULL;
return 0;
}
文件的路径问题
相对路径:相对于当前工作目录的文件路径
例如:这样我们就在编译器的下创建了一个data.txt文件
因为我们还没有开始写数据,所以大小为0kb
例如:我们要创建当前路径的上一级目录中的data.txt的文件
运用点路径
"?. "表示当前路径
" .. "表示当前路径的上一个路径
这样我们的文件就创建在了当前路径下的上一个路径
绝对路径:从根目录开始的完整文件路径
在使用绝对路径时,需要提供完整的路径信息,包括根目录、路径分隔符以及文件名
例如:我们要创建桌面上的名为data.txt的文件
我们将桌面一个应用的地址拷贝到我们的代码里面:
我们这里加斜杠的原因是为了防止某些字符的转义
这样文件就在我们桌面上创建了
文件输入和输出的概念:
文件输入:?文件输入是指将外部文件中的数据读取到程序中进行处理的过程
文件输出: 是指将程序中的数据写入到外部文件中的过程
?文件的顺序读写
?顺序读写函数介绍:
?
?字符串:
int fgetc ( FILE * stream );
作用:从指定的流 stream 获取下一个字符,并把位置标识符往前移动
- stream?-- 这是指向 FILE 对象的指针,该 FILE 对象标识了要在上面执行操作的流
int fputc ( int character, FILE * stream );
作用:把参数?character?指定的字符写入到指定的流 stream 中,并把位置标识符往前移动
- character?-- 这是要被写入的字符,该字符以其对应的 int 值进行传递
- stream?-- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流
fputc写数据
代码展示:把26个小写字符写到我的文件里面
#include <stdio.h> int main() { //打开文件 FILE* fp = fopen(" data.txt", "w"); //文件操作 if (fp == NULL) { perror("fopen"); exit(0); //输入错误,直接退出 } for (int i = 0; i < 26; i++) { fputc('a' + i, fp); } //关闭文件 fclose(fp); fp = NULL; return 0; }
到这里肯定有小伙伴会有些困惑,顺序表现在哪里呢?
我们画图来说明:
fputc一旦读写一个字符,光标就会自动往后移动一位
fgetc读数据
代码展示:将文本文件的数据读到屏幕上#include <stdio.h> int main() { //打开文件 FILE* fp = fopen("data.txt", "r"); //文件操作 if (fp == NULL) { perror("fopen"); exit(0); //输入错误,直接退出 } int ch = fgetc(fp); for (int i = 0; i < 26; i++) { printf("%c", ch+i); } //关闭文件 fclose(fp); fp = NULL; return 0; }
这样我们就把文本文件的信息读取到屏幕上啦
代码练习:写一个代码完成将data1.txt文件内容,拷贝一份到生成的data2.txt文件里面?
代码展示:
#include <stdio.h> int main() { //这里要先创建一个data1.txt的文件 FILE* fp = fopen("data1.txt", "r"); //打开data1.txt负责读取数据 if (fp == NULL) { perror("fopen->data1.txt"); //专门指出data1.txt的错误 return 1; } FILE* pf = fopen("data2.txt", "w"); if (pf == NULL) { fclose(fp); //如果打开失败,就将data1.txt文件关闭 fp = NULL; perror("fopen->data2.txt"); return 1; } int ch = 0; while ((ch = fgetc(fp)) != EOF) //将在data1.txt的文件信息读取 { fputc(ch, pf); //并写到data2.txt中 } fclose(fp); //关闭文件 fclose(pf); return 0; }
文本行:?
char * fgets ( char * str, int num, FILE * stream );
作用:从指定的流 stream 读取一行,并把它存储在?str?所指向的字符串内。当读取?(num-1)?个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定
- str?-- 这是指向一个字符数组的指针,该数组存储了要读取的字符串
- num?-- 这是要读取的最大字符数。通常是使用以 str 传递的数组长度
- stream?-- 这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流
int fputs ( const char * str, FILE * stream );
作用:把字符串写入到指定的流 stream 中
- str?-- 这是一个数组,包含了要写入的以空字符终止的字符序列
- stream?-- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流
fputs写数据
代码展示:
#include <stdio.h> int main() { FILE* fp = fopen("data.txt", "w"); if (fp == NULL) { perror("fopen"); return 1; } fputs("abcdef", fp); fclose(fp); fp = NULL; return 0; }
fgets读数据
代码展示:
#include <stdio.h> int main() { FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } char arr[20] = "*******************"; fgets(arr,10,fp); for (int i = 0; i < 20; i++) { printf("%c", arr[i]); } fclose(fp); fp = NULL; return 0; }
我们在data.txt文件中保存了10个字符
我们发现它是读取了num-1个也就是9个这是为什么呢?
我们在监视的环境看看
我们发现fgets在读取的时候会给' \0 '预留一个位置
我们以后的编程中要注意:
fgets读数据的时候读取的是num-1个
格式化:
int fscanf ( FILE * stream, const char * format, ... );
作用:从流 stream 读取格式化输入
- stream?-- 这是指向 FILE 对象的指针,该 FILE 对象标识了流
- format?-- 这是 C 字符串,包含了以下各项中的一个或多个:空格字符、非空格字符?和?format 说明符
- format 说明符形式为?[=%[*][width][modifiers]type=],具体讲解如下:
int fprintf ( FILE * stream, const char * format, ... );
作用:函数把格式化的字符串写入到指定的输出流
fprintf写数据
代码展示:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { Node s = { "张三",18,99.9f }; FILE* fp = fopen("data.txt", "w"); if (fp == NULL) { perror("fopen"); return 1; } fprintf(fp,"%s %d %.2f", s.name, s.age, s.score); fclose(fp); fp = NULL; return 0; }
我要将一个结构体类型的数据写入文件就可以使用fprintf
fscanf读数据
代码展示:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { Node s = { 0 }; FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } fscanf(fp,"%s %d %f", s.name, &(s.age), &(s.score));//不是数组就要& printf("%s %d %.2f", s.name, s.age, s.score); fclose(fp); fp = NULL; return 0; }
读取一个结构体类型的文本数据
二进制:?
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
fread() 函数读取打开的文件
函数会在到达指定长度或读到文件末尾(EOF)时(以先到者为准),停止运行
该函数返回读取的字符串,如果失败则返回 FALSE
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
fwrite() 函数将内容写入一个打开的文件中
函数会在到达指定长度或读到文件末尾(EOF)时(以先到者为准),停止运行
如果函数成功执行,则返回写入的字节数。如果失败,则返回 FALSE
fwrite写数据
代码展示:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { Node s = { "张三",18,99.9f }; FILE* fp = fopen("data.txt", "wb"); if (fp == NULL) { perror("fopen"); return 1; } fwrite(&s, sizeof(s), 1, fp); fclose(fp); fp = NULL; return 0; }
注意:因为这里是以二进制的形式进行写入的,所以文本文件是读不懂的,显示的就是一堆乱码
肯定有小伙伴想问,既然看不懂那有什么用呢?
别着急,我们可以用 fread?去读文件
fread读数据
代码展示:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { Node s = { 0 }; FILE* fp = fopen("data.txt", "rb"); if (fp == NULL) { perror("fopen"); return 1; } fread(&s, sizeof(s), 1, fp); printf("%s %d %.2f", s.name, s.age, s.score); fclose(fp); fp = NULL; return 0; }
?
对比一组函数的区别
scanf/fscanf/sscanf printf/fprintf/sprintf
大家看到以上成双成对的函数,是不是会很头疼,它们究竟有什么区别呢?
scanf-针对标准输入(键盘)的格式化输入函数
printf-针对标准输出(屏幕)的格式化的输出函
fscanf-针对所有输入流的格式化输入函数fprintf-针对所有输出流的格式化输出
sscanf-从字符串中读取格式化数据
sprintf-把格式化的数据转换成字符串
sscanf/sprintf前面没讲过,我们现在重点讲
sscanf:
int sscanf ( const char * s, const char * format, ...);
sprintf:
int sprintf ( char * str, const char * format, ... );
sprintf的讲解:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { Node s = { "zhangsan",18,100}; char arr[100] = { 0 }; sprintf(arr, "%s %d %.2f", s.name, s.age, s.score); printf("%s\n", arr); return 0; }
将结构体格式化数据转换成字符串的形式
这里重点代码:
? ? char arr[100] = { 0 };
? ? sprintf(arr, "%s %d %.2f", s.name, s.age, s.score);
? ? printf("%s\n", arr);以字符串的形式输出结构体数据
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { Node temp = { 0 }; char arr[100] = "zhangsan 18 100"; sscanf(arr, "%s %d %f", temp.name, &(temp.age), &(temp.score)); printf("%s %d %.2f", temp.name, temp.age, temp.score); return 0; }
将字符串的数据转换成格式化数据形式
这里重点代码:
? ? char arr[100] = "zhangsan 18 100";
? ? sscanf(arr, "%s %d %f", temp.name, &(temp.age), &(temp.score));
? ? printf("%s %d %.2f", temp.name, temp.age, temp.score);以结构体的形式输出字符串的数据
文件的随机读写?
?首先来介绍以下,这里的随机并不是随机数的随机,而我想在哪里读写文件就可以在哪里
fseek
作用:
该函数把文件指针从当前位置向前或向后移动到新的位置
新位置从文件头开始以字节数度量
(文件指针就是我们所谓的光标)
int fseek ( FILE * stream, long int offset, int origin );
stream | 必需。规定要在其中定位的文件。 |
offset | 必需。规定新的位置(从文件头开始以字节数度量)。 |
origin | SEEK_SET:基准位置为文件开头,即offset表示距离文件开头的偏移量 SEEK_CUR:基准位置为文件当前位置,即offset表示距离文件当前位置的偏移量? SEEK_END:基准位置为文件末尾,即offset表示距离文件末尾的偏移量 |
现在看不懂没有关系,接下来我将带大家去理解:
我们拿顺序读写来作比较:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } int ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); fclose(fp); fp = NULL; return 0; }
我们在data.txt中保存了这些字符
我们读写的时候发现它是顺序读写的
每一次读入数据光标就会往后移动
那么我想重新读写我的a怎么办呢?
这个时候就要用到我们的fseek函数
代码展示:#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } int ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); fseek(fp, -3, SEEK_CUR); ch = fgetc(fp); printf("%c\n", ch); fclose(fp); fp = NULL; return 0; }
这种随机读写的方法有多种,这里在介绍几种:
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } int ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); fseek(fp, 0, SEEK_SET); ch = fgetc(fp); printf("%c\n", ch); fclose(fp); fp = NULL; return 0; }
#include <stdio.h> typedef struct Stu { char name[20]; int age; float score; }Node; int main() { FILE* fp = fopen("data.txt", "r"); if (fp == NULL) { perror("fopen"); return 1; } int ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); ch = fgetc(fp); printf("%c\n", ch); fseek(fp, -11, SEEK_END); ch = fgetc(fp); printf("%c\n", ch); fclose(fp); fp = NULL; return 0; }
以上的代码第四位显示在屏幕上的依然是a
希望大家可以理解,这里就不画图了
ftell
作用:返回文件指针相对于起始位置的偏移量
简单来说就是返回当前光标的位置
long int ftell ( FILE * stream );
#include <stdio.h>
typedef struct Stu
{
char name[20];
int age;
float score;
}Node;
int main()
{
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int ch = fgetc(fp);
printf("%c\n", ch);
ch = fgetc(fp);
printf("%c\n", ch);
ch = fgetc(fp);
printf("%c\n", ch);
int n = ftell(fp);
printf("%d\n", n);
fclose(fp);
fp = NULL;
return 0;
}
此时的光标位置就是3,显示出来的字符就是c
但你偏移量(光标)不知道在哪时,就可以用这种方法来找到
rewind
作用:让文件的指针(光标)回到文件的起始位置
void rewind ( FILE * stream );
#include <stdio.h>
typedef struct Stu
{
char name[20];
int age;
float score;
}Node;
int main()
{
FILE* fp = fopen("data.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int ch = fgetc(fp);
printf("%c\n", ch);
ch = fgetc(fp);
printf("%c\n", ch);
ch = fgetc(fp);
printf("%c\n", ch);
rewind(fp);//这个时候光标已经回到起始位置了
ch = fgetc(fp);//读取的字符就是a
printf("%c\n", ch);
fclose(fp);
fp = NULL;
return 0;
}
文件读取结束的判定?
feof - 在文件读取结束后,判断是否是因为遇到文件末尾而结束
ferror- 在文件读取结束后,判断是否是因为遇到错误而结束
例如? fgetc 判断是否为 EOF?? fgets 判断返回值是否为 NULL?
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp) {
perror("File opening failed");
return EXIT_FAILURE;//EXIT_FAILURE就是1的意思,这里相当于枚举常量
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF)
{
putchar(c);
}
//判断程序是什么原因结束的
if (ferror(fp))//如果遇到程序错误而结束,fp返回EOF(-1)为真执行语句
puts("I/O error when reading");
else if (feof(fp))//判断程序是否因为到达程序末尾而结束
puts("End of file reached successfully");
fclose(fp);
fp = NULL
}
2.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数?
例如
fread判断返回值是否小于实际要读的个数
#include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb"); // 必须用二进制模式
fwrite(a, sizeof * a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof * b, SIZE, fp); // 读 double 的数组
if (ret_code == SIZE) {
puts("Array read successfully, contents: ");
for (int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
}
else {
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
fp = NULL;
}
?文件缓冲区
系统自动地在内存中为程序中每一个正在使用的文件开辟一块文件缓冲区
介绍:
每当我们想从内存向硬盘中输出数据,都会先将数据输送到缓冲区中,然后装满缓冲区后才一起输送到硬盘上。 如果想从硬盘向计算机内读入数据,则会先将读到的数据输送到缓冲区中,装满缓冲区后再逐个将数据输送到程序数据区(内存中的变量)
相当于我们朋友圈的定时发布
我们用以下方法来模拟我们的文件缓存区?
#include <stdio.h>
#include <windows.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开data.txt文件,发现文件没有内容\n");
Sleep(10000);//延迟函数,单位是毫秒
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
printf("再睡眠10秒-此时,再次打开data.txt文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
一开始我们发现我们的文件是没有放数据的
过了10秒过后
既然我们在写数据的过程中,会让操作系统调用接口来替我们做一些事情,那么写数据这个操作就必然会打断操作系统,如果频繁的写数据,那么操作系统必然会被频繁的打断
为了不会因为频繁的操作而打断操作系统
我们会在内存中另外开辟一块空间
用于存放需要传输的数据,直到缓冲区被放满
再由操作系统一次性全部输送到硬盘中去
可以这么理解:
文件缓冲区在写文件的时候提高整个操作系统的效率,在读文件的时候提高了程序的效率
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!