文件操作
文件是什么?
文件是数据源的一种,最主要的作用是保存数据。
在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。例如:
- 通常把显示器称为标准输出文件,printf 就是向这个文件输出数据;
- 通常把键盘称为标准输入文件,scanf 就是从这个文件读取数据。
常见硬件设备所对应的文件
- stdin 标准输入文件,一般指键盘;scanf()、getchar() 等函数默认从 stdin 获取输入。
- stdout 标准输出文件,一般指显示器;printf()、putchar() 等函数默认向 stdout 输出数据。
- stderr 标准错误文件,一般指显示器;perror() 等函数默认向 stderr 输出数据(后续会讲到)。
- stdprn 标准打印文件,一般指打印机。
我们不去探讨硬件设备是如何被映射成文件的,大家只需要记住,在C语言中硬件设备可以看成文件,有些输入输出函数不需要你指明到底读写哪个文件,系统已经为它们设置了默认的文件,当然你也可以更改,例如让printf 向磁盘上的文件输出数据。
操作文件的正确流程为:打开文件 --> 读写文件 --> 关闭文件。文件在进行读写操作之前要先打开,使用完毕要关闭。
-
所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 FILE 类型的结构体变量中。
-
关闭文件就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。
-
在C语言中,文件有多种读写方式,可以一个字符一个字符地读取,也可以读取一整行,还可以读取若干个字节。文件的读写位置也非常灵活,可以从文件开头读取,也可以从中间位置读取。
流是什么?
文件(保存在磁盘中)只有加载到内存中才可以进行处理,内存中的数据只有保存到文件(磁盘)中才可以不丢失。在这期间我们把数据在文件到内存中的传递叫做文件流。
文件是数据源的一种,除了文件,还有数据库、网络、键盘等,数据传递到内存也就是保存到C语言的变量(例如整数、字符串、数组、缓冲区等)。我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。
文本文件与二进制文件的区别?
(在C语言中)我们根据数据在文件存储形式的不同把文件文件和二进制文件。
- 文本文件:把要存储的数据当成是一系列的字符组成,把每个字符的ASCII码值存入文件中。每个ASCII码值占一个字节(8bit)每个字节表示一个字符。因此文本文件也称为字符文件或ASCII文件,是字符序列文件。
- 二进制文件:把数据对应的二进制形式存储到文件,是字节序列文件。
C语言的文件操作
打开与关闭文件
打开文件
在C语言中,操作文件之前必须先打开文件;所谓“打开文件”,就是让程序和文件建立连接的过程。
打开文件之后,程序可以得到文件的相关信息,例如大小、类型、权限、创建者、更新时间等。在后续读写文件的过程中,程序还可以记录当前读写到了哪个位置,下次可以在此基础上继续操作。
FILE *fopen( const char *filename, const char *mode );(C99 前)
- filename - 关联到文件系统的文件名
- mode - 打开方式
- 返回值 FILE(定义与stdio.h中的一种结构体类型后面会讲到) 打开文件失败返回NULL
文件打开方式
调用 fopen() 函数时必须指明读写权限,但是可以不指明读写方式(此时默认为"t")。
读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)。例如:
将读写方式放在读写权限的末尾:“rb”、“wt”、“ab”、“r+b”、“w+t”、“a+t”
将读写方式放在读写权限的中间:“rb+”、“wt+”、“ab+”
整体来说,文件打开方式由 r、w、a、t、b、+ 六个字符拼成,各字符的含义是:
- r(read):读
- w(write):写
- a(append):追加
- t(text):文本文件
- b(binary):二进制文件
- +:读和写
关闭文件
文件一旦使用完毕,应该用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。fclose() 的用法为:
int fclose( FILE stream )
- stream - 需要关闭的文件流
- 返回值 成功时为 0 ,否则为 EOF 。
关闭给定的文件流。冲入任何未写入的缓冲数据到 OS 。舍弃任何未读取的缓冲数据。无论操作是否成功,流都不再关联到文件。若在 fclose 返回后使用指针 stream 的值则行为未定义。
示例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define N 100
int main() {
FILE* fp;
char str[N + 1];
if ((fp = fopen("text.txt", "r")) == NULL) {
puts("文件打开失败");
exit(0);
}
while (fgets(str,N,fp)!=NULL)
{
printf("%s", str);
}
fclose(fp);
return 0;
}
文件的读写
以字符形式读写文件
int fgetc( FILE *stream );
- stream - 读取字符的来源
- 返回值 成功时为获得的字符,失败时为 EOF 。
int fputc( int ch, FILE *stream );
- ch - 要被写入的字符
- stream - 输出流
- 返回值 成功时,返回被写入字符。失败时,返回 EOF 并设置 stream 上的错误指示器(见 ferror() )。
示例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define N 100
int main() {
FILE* fp;
char ch;
if ((fp = fopen("text.txt", "r+")) == NULL) {
puts("文件打开失败");
exit(0);
}
while ((ch = fgetc(fp))!=EOF)
{
// Sleep(100);
putchar(ch);
}
while ((ch = getchar()) != '\n') {
fputc(ch, fp);
}
fclose(fp);
return 0;
}
以字符串形式读写文件
char *fgets ( char *str, int n, FILE *fp );
- str 为字符数组
- n 为要读取的字符数目
- fp 为文件指针。
- 返回值:读取成功时返回字符数组首地址,也即 str;读取失败时返回 NULL;如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL。
关于fgets()几点要注意的地方
- fgets() 遇到换行时,会将换行符一并读取到当前字符串。而 gets() 不一样,它会忽略换行符。
- 读取到的字符串会在末尾自动添加 ‘\0’,n 个字符也包括 ‘\0’。也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。
- 需要重点说明的是,在读取到 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。在C语言中,没有按行读取文件的函数,我们可以借助 fgets(),将 n 的值设置地足够大,每次就可以读取到一行数据。
int fputs( char *str, FILE *fp );
- str 为要写入的字符串
- fp 为文件指针
- 返回值 写入成功返回非负数,失败返回 EOF
注意:puts()往stdin输出时自带换行
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define N 100
int main() {
FILE* fp;
char str[N+1];
if ((fp = fopen("text.txt", "r+")) == NULL) {
puts("文件打开失败");
exit(0);
}
while (fgets(str,N,fp)!=NULL)
{
//Sleep(100);
printf("%s", str);
}
fclose(fp);
return 0;
}
二进制方式读取文件
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );(C99 前)
size_t fread( void *restrict buffer, size_t size, size_t count,FILE *restrict stream );(C99 起)
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );(C99 前)
size_t fwrite( const void *restrict buffer, size_t size, size_t count, FILE *restrict stream );(C99 起)
- buffer为内存区块的指针,它可以是数组、变量、结构体等。fread() 中的 buffer 用来存放读取到的数据,fwrite() 中的 buffer 用来存放要写入的数据。
- size:表示每个数据块的字节数。
- count:表示要读写的数据块的块数。
- stream:表示文件指针(文件流)。
返回值:返回成功读写的块数(对象数),也即 count。如果返回值小于 count:
- 对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
- 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。
- 若出现错误,则流的文件位置指示器的结果值不确定。若读入部分的元素,则元素值不确定。
示例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
enum { SIZE = 5 };
int main(void)
{
int a[SIZE] = { 1, 2, 3, 4, 5 };
FILE* fp1 = fopen("text.txt", "wb");
assert(fp1);
int tem = fwrite(a, sizeof(a[0]), SIZE, fp1);
printf("成功将%d个数据写入文件\n", tem);
fclose(fp1);
int b[SIZE];
FILE* fp2 = fopen("text.txt", "rb");
assert(fp2);
tem = fread(b, sizeof b[0], SIZE, fp2);
fclose(fp2);
printf("成功读入%d个数据\n", tem);
for (int i = 0; i < SIZE; i++) {
printf("%d ", b[i]);
}
return 0;
}
格式化读写文件
int fscanf ( FILE *fp, char * format, … );
int fprintf ( FILE *fp, char * format, … );
- fp 为文件指针
- format 为格式控制字符串
- … 表示参数列表
- 返回值 fprintf() 返回成功写入的字符的个数,失败则返回负数。
- 返回值 fscanf() 返回参数列表中被成功赋值的参数个数。
与 scanf() 和 printf() 相比,它们仅仅多了一个 fp 参数。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define N 2
struct stu {
char name[10];
int num;
int age;
float score;
} boya[N], boyb[N], * pa, * pb;
int main() {
FILE* fp;
fp = fopen("test.txt", "wt+");
assert(fp);
pa = boya;
for (int i = 0; i < N; i++) {
scanf("%s %d %d %f", pa->name, &(pa->num), &(pa->age), &(pa->score));
pa++;
}
pa = boya;
for (int i = 0; i < N; i++) {
fprintf(fp, "%s %d %d %f\n", pa->name, pa->num, pa->age, pa->score);
pa++;
}
rewind(fp);//移动文件位置指示器到给定文件流的起始。
pb = boyb;
for (int i = 0; i < N; i++) {
fscanf(fp, "%s %d %d %f\n", pb->name, &(pb->num), &(pb->age), &(pb->score));
pb++;
}
pb = boyb;
for (int i = 0; i < N; i++) {
printf("%s %d %d %f\n", pb->name, pb->num, pb->age, pb->score);
pb++;
}
fclose(fp);
return 0;
}
随机读写文件
前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据。但在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写。这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。
实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。
void rewind ( FILE *fp );用来将位置指针移动到文件开头
int fseek ( FILE *fp, long offset, int origin );用来将位置指针移动到任意位置
- fp 为文件指针,也就是被移动的文件。
- offset 为偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动。
- origin 为起始位置,也就是从何处开始计算偏移量。C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾
每个位置都用对应的常量来表示:
- 起始点——常量名 ——常量值
- 文件开头 SEEK_SET 0
- 当前位置 SEEK_CUR 1
- 文件末尾 SEEK_END 2
注意: fseek() 一般用于二进制文件,在文本文件中由于要进行转换,计算的位置有时会出错。
示例:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//ftell 返回当前的文件位置指示值
//
//fgetpos 获取文件位置指示器
//
//fseek 将文件位置指示符移动到文件中的指定位置
//
//fsetpos 将文件位置指示器移动到文件中的指定位置
//
//rewind 将文件位置指示器移动到文件首
#define N 3
struct stu {
char name[10]; //姓名
int num; //学号
int age; //年龄
float score; //成绩
}boys[N], boy, * pboys;
int main() {
FILE* fp;
pboys = boys;
fp = fopen("test.txt", "w+");
puts("Input data:\n");
for (int i = 0; i < N; i++) {
scanf("%s %d %d %f", pboys->name, &pboys->num, &pboys->age, &pboys->score);
pboys++;
}
pboys = boys;
fwrite(pboys, sizeof(struct stu), N, fp);
rewind(fp);
fseek(fp, sizeof(struct stu), SEEK_SET);
fread(&boy, sizeof(struct stu), 1, fp);
printf("%s %d %d %f", boy.name, boy.num, boy.age, boy.score);
fclose(fp);
return 0;
}
C语言实现文件拷贝
#include "stdio.h"
#include "stdlib.h"
int main(int arg,char* argv[]){
if(arg!=3){
printf("input error!");
exit(0);
}
FILE* fpr = fopen(argv[1],"rb");
FILE* fpw = fopen(argv[2],"wb");
if(fpr==NULL||fpw==NULL) return 1;
fseek(fpr,0,SEEK_END);
int len = ftell(fpr);
rewind(fpr);
for(int i = 0;i<len;i++){
char ch = fgetc(fpr);
fputc(ch,fpw);
}
fclose(fpr);
fpr = NULL;
fclose(fpw);
fpw = NULL;
return 0;
}
获取文件长度
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main(){
FILE* fp = fopen("test.txt", "r");
fseek(fp, 0, SEEK_END);
int len = ftell(fp);
rewind(fp);
printf("文件的长度为:%d", len);//文件中有 0D 0A
return 0;
}