1. 文件的概念

文件的分类

根据数据的组织形式,数据文件可分为ASCII文件和二进制文件:

  • ASCII文件(文本文件):每一个字节放一个ASCII代码
  • 二进制文件:把内存中的数据按照其在内存中的存储形式原样输出到磁盘上存放

文本文件和二进制文件的区别

1,文本文件

ASCII文件便于对字符进行逐个处理,也便于输出字符。但一般占存储空间较多,而且要花费转换时间。

在文本文件中数据是以字符形式呈现的,每一个字符占用一个字节,而字节在计算机中又是以ASCII码来识别。在存储文本文件时需要先讲ASCII码转为二进制的形式然后进行存储。
例如:在内存中将整数123以文本形式存放,如下
C语言文件操作详解_c语言

由于文本形式存储都是以字符形式展示的,所以整数123被拆分为三个字符‘1’,‘2’,‘3’。这三个字符所对应的ASCII码值分别为49、50、51,长方形内的数字是该ASCII码对应的二进制。

2,二进制文件

二进制文件可以节省外部存储空间和转换时间,但是一个字节并不对应一个字符,不能直接输出字符形式。

二进制文件在存储数据时是直接以二进制的方式进行的,这种存储方式与数据在牛才能中的存储方式相同,不需要进行转换,因此不仅可以提高执行效率,还能节省存储空间。
例如:在内存中将整数123以二进制形式存放(假设123为short类型),如下
C语言文件操作详解_数据_02

整数123在内存中是以二进制数“00000000 01111011”直接存储的,所以将二进制文件从硬盘读到内存是不需要进行数据转换的。

文件指针

在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。

C语言内置的文件指针类型 FILE*

在标准输入输出库库中,系统定义了三个FILE型的指针常量:

  • stdin(标准输入文件指针) 指向在内存中与键盘相应的文件信息区,因此,用它进行输入就蕴含了从键盘输入。
  • stdout(标准输出文件指针) 指向在内存中与显示器屏幕相应的文件信息区,因此,用它进行输出就蕴含了输出到显示器屏幕。
  • stderr (标准错误文件指针) 用来输出出错的信息,它也指向在内存中与显示器屏幕相应的文件信息区,因此,在程序运行时的出错的信息就输出到显示器屏幕。

文件的位置指针

位置指针用来指示当前的读写位置。

一般情况下,在对字符文件进行读写时,文件的位置指针指向文件开头,这时如果对文件进行读的操作,就读第一个字符,然后文件的位置指针顺序向后移一个位置,在下一次执行读的操作时,就将指针指向的第二个字符读入。 依此类推,直到遇文件尾

2. 文件操作

文件的打开与关闭

打开文件

FILE*  fopen(char const* _FileName,char const* _Mode);
  • fileName:需要打开的文件名,也就是准备访问的文件的名字;
  • mode:文件打开模式(“读”还是“写”等) ;
  • 返回值:指向被打开的文件。

关闭文件

int  fclose(FILE* _Stream);

文件一旦使用完毕,应该用 fclose() 函数把文件关闭,以释放相关资源,避免数据丢失。

文件的打开模式

模式 含义 说明
r 只读 文件必须存在,否则打开失败
w 只写 若文件存在,则清除原文件内容后写入;否则,创建文件后写入
a 追加只写 若文件存在,则位置指针移到文件末尾,在文件尾部追加写入,故该方式不删除原文件数据;若文件不存在,则打开失败
r+ 读写 文件必须存在。在只读 r 的基础上加 ‘+’ 表示增加可写的功能。
w+ 读写 在只写 w 的基础上加 ‘+’ 表示增加可读的功能
a+ 读写 在”a”模式的基础上,增加可读功能
rb 二进制读 功能同模式”r”,区别:b表示以二进制模式打开。下同
wb 二进制写 功能同模式“w”。二进制模式
ab 二进制追加 功能同模式”a”。二进制模式
rb+ 二进制读写 功能同模式"r+”。二进制模式
wb+ 二进制读写 功能同模式”w+”。二进制模式
ab+ 二进制读写 功能同模式”a+”。二进制模式

文件打开失败原因

在文件操作总经常会出现文件打开失败问题,总结主要有以下几点原因。

  1. 用”r”只读的方式打开一个不存在的文件

  2. 文件路径错误

  3. 文件名非法
    C语言文件操作详解_visual studio_03

  4. 权限不够:以管理员身份运行该程序即可
    C语言文件操作详解_c++_04

文件读写

在C语言中,读写文件比较灵活,既可以每次读写一个字符,也可以读写一个字符串,甚至是任意字节的数据(数据块)。

字符形式读写

字符读取函数fgetc

以字符形式读写文件时,每次可以从文件中读取一个字符,或者向文件中写入一个字符。主要使用两个函数,分别是 fgetc() 和 fputc()。

int  fgetc(FILE* _Stream);
  • 读取成功时返回读取到的字符,读取到文件末尾或读取失败时返回EOF
    • EOF 是 end of file 的缩写,表示文件末尾,是在 stdio.h 中定义的宏,它的值是一个负数,往往是 -1。

【示例】在屏幕上显示demo.txt文件的内容

#include<stdio.h>

int main(int argc,char*argv[])
{
    FILE* fp = fopen("demo.txt","r");
    if(fp == NULL)
    {
        perror("文件打开失败:");
        return -1;
    }

    char ch = '\0';
    while ((ch = fgetc(fp)) != EOF)	//逐字符读取,直到读取完毕
    {
       putchar(ch);
    }
    
    //判断文件是否出错,其实文件很少出错,可以不加
    if(ferror(fp)){
        puts("读取出错");
    }else{
        puts("读取成功");
    }
    
    fclose(fp);
    
    return 0;
}

EOF 本来表示文件末尾,意味着读取结束,但是很多函数在读取出错时也返回 EOF,那么当返回 EOF 时,到底是文件读取完毕了还是读取出错了?我们可以借助 stdio.h 中的两个函数来判断,分别是 feof() 和 ferror()。

feof() 函数用来判断文件内部指针是否指向了文件末尾,当指向文件末尾时返回非零值,否则返回零值。

int feof(FILE* _Stream);

ferror() 函数用来判断文件操作是否出错,出错时返回非零值,否则返回零值。

int ferror ( FILE *fp );
if (feof(fp))
{
	break;
}
字符写入函数fputc
int  fputc(int   ch, FILE* _Stream);
  • ch 为要写入的字符,Stream为文件指针。fputc() 写入成功时返回写入的字符,失败时返回 EOF,返回值类型为 int 也是为了容纳这个负数

字符串形式读写

fgetc() 和 fputc() 函数每次只能读写一个字符,速度较慢;实际开发中往往是每次读写一个字符串或者一个数据块,这样能明显提高效率。

读字符串函数 fgets

fgets() 函数用来从指定的文件中读取一个字符串,并保存到字符数组中,它的用法为:

char*  fgets(char* _Buffer,int  _MaxCount,FILE* _Stream);
  • buffer为字符数组
  • maxcount为要读取的字符数量(不要超过buffer字符数组的大小)
  • stream 文件指针
  • 返回值:读取成功时返回字符数组首地址,也就是buffer;读取失败时返回 NULL;如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL。
  • 需要重点说明的是,在读取到 MaxCount-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 MaxCount的值多大,fgets() 最多只能读取一行数据,不能跨行。在C语言中,没有按行读取文件的函数,我们可以借助 fgets(),将 MaxCount的值设置地足够大,每次就可以读取到一行数据。
写字符串函数 fputs

fputs() 函数用来向指定的文件写入一个字符串,它的用法为:

int  fputs(char const* _Buffer,FILE*  _Stream);

格式化读写

fscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。

int fscanf ( FILE *_Stream, char * format, ... );
int fprintf ( FILE *_Stream, char * format, ... );

_Stream为文件指针,format 为格式控制字符串,… 表示参数列表。与 scanf() 和 printf() 相比,它们仅仅多了一个 fp 参数。

二进制读写

fgets() 有局限性,每次最多只能从文件中读取一行内容,因为 fgets() 遇到换行符就结束读取。如果希望读取多行内容,需要使用 fread() 函数;相应地写入函数为 fwrite()。

fread() 函数用来从指定文件中读取块数据。所谓块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。fread() 的原型为:

size_t fread(void*  Buffer,size_t ElementSize,size_t ElementCount,FILE*  Stream);

fwrite() 函数用来向文件中写入块数据,它的原型为:

size_t  fwrite(void const* Buffer,size_t ElementSize,size_t ElementCount,FILE* Stream);

对参数的说明:

  • Buffer为内存区块的指针,它可以是数组、变量、结构体等。fread() 中的 Buffer用来存放读取到的数据,fwrite() 中的 Buffer用来存放要写入的数据。
  • ElementSize:表示每个数据块的字节数。
  • ElementCount:表示要读写的数据块的块数。
  • Stream:表示文件指针。
  • 理论上,每次读写 size*count 个字节的数据。

返回值:返回成功读写的块数,也即 count。如果返回值小于 count:

  • 对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
  • 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。

【示例】从键盘输入一个数组,将数组写入文件再读取出来。

#include<stdio.h>
#define N 5
int main(){
    //从键盘输入的数据放入a,从文件读取的数据放入b
    int a[N], b[N];
    int i, size = sizeof(int);
    FILE *fp;

    if( (fp=fopen("D:\\demo.txt", "rb+")) == NULL ){  //以二进制方式打开
        puts("Fail to open file!");
        exit(0);
    }
  
    //从键盘输入数据 并保存到数组a
    for(i=0; i<N; i++){
        scanf("%d", &a[i]);
    }
    //将数组a的内容写入到文件
    fwrite(a, size, N, fp);
    //将文件中的位置指针重新定位到文件开头
    rewind(fp);
    //从文件读取内容并保存到数组b
    fread(b, size, N, fp);
    //在屏幕上显示数组b的内容
    for(i=0; i<N; i++){
        printf("%d ", b[i]);
    }
    printf("\n");

    fclose(fp);
    return 0;
}

打开 demo.txt,发现文件内容根本无法阅读。这是因为我们使用"rb+"方式打开文件,数组会原封不动地以二进制形式写入文件,一般无法阅读。

数据写入完毕后,位置指针在文件的末尾,要想读取数据,必须将文件指针移动到文件开头,这就是rewind(fp);的作用。

随机读写

前面介绍的文件读写函数都是顺序读写,即读写文件只能从头开始,依次读写各个数据。但在实际开发中经常需要读写文件的中间部分,要解决这个问题,就得先移动文件内部的位置指针,再进行读写。这种读写方式称为随机读写,也就是说从文件的任意位置开始读写。

实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。

文件定位函数rewind和fseek

rewind() 用来将位置指针移动到文件开头,前面已经多次使用过,它的原型为:

void rewind ( FILE *Stream );

fseek() 用来将位置指针移动到任意位置,它的原型为:

int fseek ( FILE *Stream, long offset, int origin );

参数说明:

  • Stream为文件指针,也就是被移动的文件。
  • offset 为偏移量,也就是要移动的字节数。之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。offset 为正时,向后移动;offset 为负时,向前移动。
  • origin 为起始位置,也就是从何处开始计算偏移量。C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,每个位置都用对应的常量来表示:
起始点 常量名 常量值
文件开头 SEEK_SET 0
当前位置 SEEK_CUR 1
文件末尾 SEEK_END 2

例如,把位置指针移动到离文件开头100个字节处:

fseek(fp, 100, 0);

值得说明的是,fseek() 一般用于二进制文件,在文本文件中由于要进行转换,计算的位置有时会出错。

3. 其他文件操作函数

ftell 用于得到文件位置指针当前位置相对于文件首的偏移字节数,ftell一般用于读取文件的长度。

long  ftell(FILE* Stream);

remove 用于删除指定的文件,如果文件被成功删除,则返回零值。

int  remove(char const* FileName);

rename 用于重名/移动文件,成功返回0,失败返回非零

int  rename(char const* OldFileName,char const* NewFileName);
  • 将oldname 指定的文件或目录的名称更改为newname

  • 如果oldnamenewname指定不同的路径并且系统支持,则文件将移动到新位置。

  • 如果newname命名现有文件,则该函数可能会失败或覆盖现有文件,具体取决于特定的系统和库实现

4.文件重定向

  • 重定向输出到指定文件
freopen("output.txt","w",stdout);
  • 取消重定向(重新重定向到控制台)
freopen("CON","w",stdout)

示例:

FILE* fp = freopen("output.txt","w",stdout);
if(!fp)
{
    perror("重定向失败!\n");
}

printf("hello world!\n");	//会输出到文件里
puts("maye");				//同上
putchar('A');				//同上

fprintf(stderr,"has error:");	//标准错误,不能重定向,只能输出到黑窗口

fp = freopen("CON","w",stdout);
if(!fp)
{
    perror("重定向恢复失败!\n");
}

5. 代码测试

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define N 5
int main()
{
	FILE* fp = fopen("demo.txt", "w+");
	if (!fp)
	{
		perror("文件打开失败!\n");
		exit(-1);
	}
	FILE* fp2 = fopen("result.txt", "w+");
	if (!fp2)
	{
		perror("文件打开失败!\n");
		exit(-1);
	}
#if 0
	char ch = '\0';
	for (int i = 0; i < 26; i++)
	{
		fputc('A' + i, fp);
		if (i % 5 == 0)
		{
			fputc('\n', fp);
		}
	}
	rewind(fp);	//移动到文件开头

	while (ch = fgetc(fp) )
	{
		if (feof(fp))
		{
			break;
		}
		putchar(ch);
	}
#elif 0
	const char ch[20] = "woainihhdhahwdd";
	fputs(ch, fp);	//写入文件
	rewind(fp);

	char result[20];
	//读取文件
	fgets(result, sizeof(result) / sizeof(result[0]), fp);
	printf("%s", result);

#elif 0
	fputs("25 woaini 666\n", fp);
	rewind(fp);
	char ch;
	int num = 0;
	char s[10];
	fscanf(fp, "%d %s", &num, s);		//从文件读取
	fprintf(fp2, "%s\t%d\n", s, num);	//往文件写入
	rewind(fp2);
	while ((ch = fgetc(fp2)) != EOF)
	{
		putchar(ch);
	}
#elif 1
	size_t num = 0;
	int a[N], b[N];
	for (int i = 0; i < N; i++)
	{
		scanf("%d", &a[i]);
	}
	//二进制读写
	fwrite(a, sizeof(int), N, fp2);
	fseek(fp2, 0, SEEK_SET);//文件开头
	fread(b, sizeof(int), N, fp2);
	for (int i = 0; i < N; i++)
	{
		printf("%d", b[i]);
	}
	//ftell获取当前位置到文件开头的字节数
	long num1 = 0;
	fseek(fp2, 0, SEEK_END);
	num1 = ftell(fp2);
	printf("\n%ld\n", num1);

	rename("./result.txt", "./hhh.txt");

#endif
	fclose(fp);
	fclose(fp2);
	system("pause");
	return 0;
}