C语言——文件操作(超全超详细)

C语言——文件操作

1. 什么是文件

磁盘上的文件是文件

但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)

1.1 程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)

1.2 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件

数据文件又分为文本文件和二进制文件

1.2.1 文本文件和二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件

如果要求在外存上以ASCII码形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

例如对于整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符占一个字节),而二进制形式输出,则在磁盘上只占4个字节,如图所示

C语言——文件操作(超全超详细)_第1张图片

1.2.2 为什么要有文本文件

可能有小伙伴会问:文本文件只是二进制二进制文件的特殊形式,为什么要特殊照顾文本文件呢?

  1. 文本文件符合人类的阅读习惯、应用场景多,可以让人类简化操作
  2. 保障数据读写的真实意图,这样能在不同的系统里有较好的兼容性
  3. 可以有换行的概念‘\n’,字符有合法范围因此文件结束符可以特殊

1.3 文件名

一个文件对应一个唯一的文件标识(即文件名)

同一路径下不可能出现同名文件

文件名包含3部分:文件路径 + 文件名主干 + 文件后缀,例如:
注:文件名中不一定包含后缀名

C语言——文件操作(超全超详细)_第2张图片

文件路径又分为绝对路径和相对路径:

1.3.4 绝对路径和相对路径

文件的绝对路径是指从根目录开始到文件的完整路径,包括所有的目录层级。例如,Windows系统中的绝对路径可能是:“C:\Users\username\Documents\file.txt”

**相对路径是指相对于当前工作目录或者其他已知目录的路径。相对路径不包含根目录,而是使用特定的标识符来表示路径的位置关系。**例如,如果当前工作目录是"/home/username/Documents",那么相对路径"file.txt"表示文件位于当前工作目录下的文件"file.txt"。

在相对路径中,还可以使用特殊的标识符来表示位置关系。例如,"…“表示父级目录,”."表示当前目录。因此,如果当前工作目录是"/home/username/Documents",那么相对路径"…/file.txt"表示文件位于父级目录下的文件"file.txt"。

需要注意的是,相对路径是相对于当前工作目录或其他已知目录的路径,所以在不同的环境中可能会有不同的结果。因此,在编写代码或指定文件路径时,最好使用绝对路径来确保准确性和可移植性


2. C语言中的流

什么是流?

在C语言中,“流”(stream)是一种用于输入和输出数据的抽象概念。它是一种数据的传输方式,可以将数据从一个地方传送到另一个地方。在C语言中,输入流和输出流是通过一组标准库函数来实现的,这些函数允许程序从键盘或文件中读取数据,或者将数据写入到屏幕或文件中

C语言中的流可以分为标准流(standard streams)文件流(file streams)

注:C语言中操作流的主要函数是标准I/O库中的stdio.h头文件中定义的函数。

2.1 标准流

我们需要清楚,C语言程序,只要运行起来,就会默认打开3个流(标准流)

标准输入流(stdin):用于读取输入数据,默认情况下是键盘输入。
标准输出流(stdout):用于向终端或命令行窗口输出数据。
标准错误流(stderr):用于输出错误信息。

2.2 文件流

C语言中的文件流是一种用于在程序中读取和写入文件的流。通过文件流,可以在C程序中打开文件,从文件中读取数据或将数据写入文件中。这样可以有效地处理大量数据、持久性存储以及与文件系统的交互。


本次,我们重点讨论文件流

3. 文件指针

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE

我们一般都是用过一个FILE的指针来维护这个FILE结构的变量,例如:

FILE* pf;	//文件指针变量

可以通过这个FILE指针pf来找到与这个文件对应的文件信息区


4. 文件的打开和关闭

在使用文件之前应该打开文件,使用完之后应该关闭文件

ANSIC规定用fopen来打开文件,用fclose来关闭文件

FILE * fopen ( const char * filename, const char * mode );	//打开文件
int fclose ( FILE * stream );	//关闭文件

打开方式mode如下:

文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件(清空原有数据) 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件(清空原有数据) 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 建立一个新的文件
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,创建一个新的文件(清空原有数据) 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写新建一个二进制文件(清空原有数据) 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件

注1:当文件打开失败出错时,会返回一个空指针,因此我们一定要在打开文件之后,对文件指针进行有效性检查
注2:对于打开进行更新的文件(包含“+”号的文件),允许输入和输出操作,在写入操作之后的读取操作之前,应刷新(fflush)或重新定位流(fseek,fsetpos,rewind)流应在读取操作之后的写入操作之前重新定位(fseek、fsetpos、rewind)(只要该操作未到达文件末尾)

例如:

#include

int main()
{
    //此时该路径下没有名为data.txt的文件,因此会打开失败
	FILE* fp = fopen("data.txt", "r");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	fclose(fp);
	fp = NULL;

	return;
}

C语言——文件操作(超全超详细)_第3张图片

#include

int main()
{
    //用写的方式打开文件,如果文件不存在,会在该路径底下创建一个新的名为data.txt的文件
	FILE* fp = fopen("data.txt", "w");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	fclose(fp);
	fp = NULL;

	return;
}

C语言——文件操作(超全超详细)_第4张图片

在上面我们使用的都是与执行文件相同路径底下的文件,如果是其他路径的文件又该如何打开呢?

有两种方法:

方法一:使用相对路径

例如我们要打开Work_7.18.c上一级目录中,文件夹Debug中的名为data.txt的文件:

#include

int main()
{
	FILE* fp = fopen("..\\Debug\\data.txt", "r");
    /*
    	..表示Work_7.18.c文件的上一级目录
    	Debug为该目录底下的文件夹
    */
    
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		printf("SUCCESS\n");
	}

	fclose(fp);
	fp = NULL;

	return;
}

C语言——文件操作(超全超详细)_第5张图片

方法二:绝对路径

例如,我们要打开桌面上的名为data.txt的文件

#include

int main()
{
	FILE* fp = fopen("C:\\Users\\HUASHUO\\Desktop\\data.txt", "r");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}
	else
	{
		printf("SUCCESS\n");
	}

	fclose(fp);
	fp = NULL;

	return;
}

注:本来,文件地址为"C:\Users\HUASHUO\Desktop\data.txt",但为了和转义字符做区分,应该在每一个反斜杠后面再加一个反斜杠:“C:\\Users\\HUASHUO\\Desktop\\data.txt”


5. 文件输入和文件输出的概念

可能有许多小伙伴会认为,文件的输入就是将数据写到文件里,文件的输出就是将文件的内容读取出来,然而事实却恰恰相反

文件输入(File Input)文件输入是指将外部文件中的数据读取到程序中进行处理的过程。

文件输出(File Output):是指将程序中的数据写入到外部文件中的过程。

我们也可以画一个图来表示这个关系:

C语言——文件操作(超全超详细)_第6张图片


6. 文件的顺序读写

6.1 顺序读写的函数

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本输入函数 fgets 所有输入流
文本输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件
6.1.1 fgetc
int fgetc ( FILE * stream );
  • 返回文件指针当前指向的字符,然后文件指针向后移动一位
  • 如果文件指针位于文件末尾,那么就返回EOF,并为流设置 (feof) 的文件结束指示器
  • 如果文件读取错误,同样返回EOF,但改为设置其错误指示器 (ferror)

示例:

若在.c文件的路径下有一名为data.txt的文件,这个文件有字符串“abcdef”

#include

int main()
{
	FILE* fp = fopen("data.txt", "r");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}
	
	char ch;
	while ((ch = fgetc(fp)) != EOF)
		printf("%c", ch);
	printf("\n");

	fclose(fp);
	fp = NULL;

	return;
}

output:

abcdef
6.1.2 fputc
int fputc ( int character, FILE * stream );
  • 将一个字符写入文件,然后文件指针向后移动一位
  • 如果写入成功,那么返回这个字符的ASCII值
  • 如果发生错误,则返回EOF

示例:

向同一文件逐个写入26个小写英文字符

#include

int main()
{
	FILE* fp = fopen("data.txt", "w");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	char ch = 'a';
	char ret;
	for (int i = 0; i < 26; i++)
	{
		ret = fputc(ch + i, fp);
		printf("%c", ret);
	}

	fclose(fp);
	fp = NULL;

	return;
}

output:

abcdefghijklmnopqrstuvwxyz
6.1.3 fgets
char * fgets ( char * str, int num, FILE * stream );
  • 从流(stream)中读取字符,并以字符串的形式存储到str中,直到读够(num - 1)个字符,或到达换行符,或读到文件尾

  • 换行符‘\n’会使fgets停止读取,但换行符会被函数认为是有效字符,并存入str中

  • 束符‘\0’会成为第num个字符,添加到str末尾

  • 如果读取成功,则返回str

  • 如果在读取的过程中遇到文件尾,那么就设置 eof 指示器 (feof)

  • 如果没有读到任何字符就遇到文件尾,那么就返回空指针,设置 eof 指示器 (feof),str的内容不会改变

  • 如果读取错误,则设置错误指示器(ferror),同样返回空指针,但str的内容可能会改变

示例1:

从文件中读取26个小写字母

#include

int main()
{
	FILE* fp = fopen("data.txt", "r");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	char ch[30] = {0};

	fgets(ch, 26 + 1, fp);

	puts(ch);

	fclose(fp);
	fp = NULL;

	return;
}

output:

abcdefghijklmnopqrstuvwxyz

示例2:

从文件读取以下内容:

123
456

output:

123
    
6.1.3.1 fgets和gets的区别

fgets

char * fgets ( char * str, int num, FILE * stream );

gets

char * gets ( char * str );
  • fgets接受流参数(标准输入流,文件流都可以),而gets只接受标准输入流
  • fets可指定读取的最大字符数
  • fgets会将换行符读入到str再结束读取,而gets碰到换行符就会停止读取,不会读入换行符

例如:

#include

int main()
{
	char str1[20] = { 0 };
	char str2[20] = { 0 };

	gets(str1);
	fgets(str2, 5 + 1, stdin);

	return 0;
}

同时在标准输入流读入“abcd\n”,利用调试,可以发现二者区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bL2MbhyH-1689748214046)(C:/Users/HUASHUO/AppData/Roaming/Typora/typora-user-images/image-20230718195146834.png)]

6.1.4 fputs
int fputs ( const char * str, FILE * stream );
  • 将str中的字符串输出到流(stream)中,结束符‘\0’不会被写入
  • 如果输出成功,则返回非负值
  • 如果失败,则返回EOF,并设置错误指示器(ferror)

例如:

将字符串“abcdef”写入文件

#include

int main()
{
	FILE* fp = fopen("data.txt", "w");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	char ch[] = "abcdef";

	int ret = fputs(ch, fp);
	printf("%d", ret);
    
    fclose(fp);
	fp = NULL;

	return;
}

output:

0
6.1.4.1 fputs和puts的区别

fputs:

int fputs ( const char * str, FILE * stream );

puts:

int puts ( const char * str );
  • fputs的输出对象可以被指定,而puts只能是标准输出流stdout
  • puts输出会自带一个换行符‘\n’,而fputs不会

例如:

#include

int main()
{
	char str[] = "abcdef";

	puts(str);
	printf("HELLO");

	printf("\n");

	fputs(str, stdout);
	printf("HELLO");

	printf("\n");

	return 0;
}

output

abcdef
HELLO
abcdefHELLO
6.1.5 fscanf
int fscanf ( FILE * stream, const char * format, ... );
  • 以格式化的形式从流(stream)中读取数据

  • 成功后,该函数返回成功填充的参数列表的项数。此计数可以与预期的项目数匹配,也可以由于匹配失败、读取错误或文件末尾的到达而减少(甚至为零)。

  • 如果发生读取错误或在读取时到达文件末尾,则会设置正确的指示器(feof 或 ferror)。并且,如果在成功读取任何数据之前发生任一情况,则返回 EOF。

例如:

从文件中读取数字123和字符串“abcdef”

#include

int main()
{
	FILE* fp = fopen("data.txt", "r");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	char str[10] = { 0 };
	int num = 0;

	fscanf(fp, "%d %s", &num, str);

	printf("%d\n%s\n", num,str);
    
    fclose(fp);
	fp = NULL;

	return;
}

output:

123
abcdef
6.1.5.1 scanf/sscanf/fscanf

scanf:

int scanf ( const char * format, ... );

sscanf:

int sscanf ( const char * s, const char * format, ...);

fscanf:

int fscanf ( FILE * stream, const char * format, ... );
  • 三者都是以格式化的形式读取数据
  • 三者在**遇到空格时会停止读取,且不会读取空格字符。**这是因为默认情况下,这些函数以空白字符(包括空格、制表符和换行符)作为输入字段的分隔符
  • scanf只能从标注输入流(stdin)读取,sscanf只能从字符串中读取,fscanf可以从标准输入流或文件流读取

例如:

文件内容为: 123 abcdef

#include

int main()
{
	FILE* fp = fopen("data.txt", "r");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	int num1 = 0;
	int num2 = 0;
	int num3 = 0;

	char str1[20] = "123 abcdef";
	char str2[10] = { 0 };
	char str3[10] = { 0 };

	scanf("%d", &num1);

	sscanf(str1, "%d%s", &num2, str2);

	fscanf(fp, "%d%s", &num3, str3);

	printf("num1 = %d, str1 = %s\n", num1, str1);
	printf("num2 = %d, str2 = %s\n", num2, str2);
	printf("num3 = %d, str3 = %s\n", num3, str3);

	fclose(fp);
	fp = NULL;

	return 0;
}

input:

123

output:

num1 = 123, str1 = 123 abcdef
num2 = 123, str2 = abcdef
num3 = 123, str3 = abcdef
6.1.6 fprintf
int fprintf ( FILE * stream, const char * format, ... );
  • 以格式化的形式向流(stream)中输出数据

  • 成功后,将返回写入的字符总数

  • 如果发生写入错误,则设置错误指示器(ferror)并返回负数。

例如:

将数字123和字符串“abcdef”写入文件

#include

int main()
{
	FILE* fp = fopen("data.txt", "w");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	char str[10] = "abcdef";
	int num = 123;

	int ret = fprintf(fp, "%d%s", num, str);

	printf("ret = %d\n", ret);
    
    fclose(fp);
	fp = NULL;

	return;
}

output:

ret = 9

注:如果在%d%f中加入一个空格,那么返回值就会加一

6.1.6.1 printf/sprintf/fprintf

printf:

int printf ( const char * format, ... );

sprintf:

int sprintf ( char * str, const char * format, ... );

fprintf:

int fprintf ( FILE * stream, const char * format, ... );
  • 三者都是以格式化的形式输出
  • print用于将格式化的数据输出到标准输出流(屏幕);sprintf用于将格式化的数据输出到字符串中;fprintf用于将格式化的数据输出到指定流。

例如:

#include

int main()
{
	FILE* fp = fopen("data.txt", "w");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	int num1 = 10;
	int num2 = 10;
	int num3 = 0;

	char str1[10] = "abcdef";
	char str2[10] = { 0 };

	printf("num1 = %d\n", num1);

	sprintf(str2, "%s", str1);
	puts(str2);

	fprintf(fp, "%d %s", num1, str1);

	fclose(fp);
	fp = NULL;

	return 0;
}

outputs:

num1 = 10
abcdef
6.1.7 fread
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
  • 以二进制的形式从流中读取count个元素,每个元素的大小为size字节,并将它们存储在 ptr 指定的内存块中。

  • 返回成功读取的元素总数。

  • 如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都会设置正确的指标,可以分别用 ferror 和 feof 进行检查。

  • 如果sizecount为零,则该函数返回零,并且流状态和 ptr 指向的内容保持不变。

例如:

读取文件中的字符串“1200”

int main()
{
	FILE* fp = fopen("data.txt", "rb");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	int str[10] = {0};

	int ret = fread(str, sizeof(int), 1, fp);
	for (int i = 0; i < 10; i++)
		printf("%d ", str[i]);

	printf("\nret = %d\n", ret);
    
    fclose(fp);
	fp = NULL;

	return;
}

output:

808464945 0 0 0 0 0 0 0 0 0
ret = 1
  • 可能有小伙伴不理解为什么是这个结果,我们一起来分析:
  • 我们知道,fread是以二进制的形式读取文件中的信息,而文件中的1200是文本信息,每个字符对应的ASCII码为49,50,48,48,转换为二进制码:0011 0001 0011 0010 0011 0000 0011 0000
  • 再看看808464945的二进制形式:

  • 可以发现,数据竟然是倒着读取的,这里就又涉及到了小端字节序存储这一概念,文件前面的数据是低地址,后面的数据是高地址,而低位数据存储在低地址,高位数据存储在高地址,因此最终读取到的数据就要将文本信息倒着读

注:

对小端字节序存储该不太了解的同学请看这里传送门

在C语言中,文件的数据存储方式取决于计算机的体系结构和操作系统。通常情况下,对于大多数计算机系统,文件的数据是按照字节顺序存储的,这意味着在内存中,文件前面的数据存储在较低的地址,后面的数据存储在较高的地址

6.1.8 fwrite
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
  • 以二进制的形式将ptr存储的数据写入流中,一共写入count个元素,每个元素的大小为size字节。

  • 返回成功写入的元素总数

  • 如果此数字与 count 参数不同,则写入错误阻止函数完成。在这种情况下,将为流设置错误指示器(ferror)。

  • 如果sizecount为零,则该函数返回零,错误指示器保持不变。

例如:

向文件中写入字符串“abcdef”

#include

int main()
{
	FILE* fp = fopen("data.txt", "wb");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}

	char str[] = "abcdef";

	int ret = fwrite(str, sizeof(char), strlen(str), fp);
	printf("%d\n", ret);

	fclose(fp);
	fp = NULL;

	return 0;
}

output:

6

7. 文件的随机读写

注1:当文件被打开时,文件指针默认值向文件的起始位置

注2:当文件以追加的方式打开时,不允许人为改变文件指针位置

7.1 fseek

根据文件位置和偏移量来定位文件指针

int fseek ( FILE * stream, long int offset, int origin );
  • offset为偏移量
  • origin为起始位置,有三种取值:

SEEK_SET:文件头

SEEK_CUR:文件指针的当前位置

SEEK_END:文件尾

  • 如果成功,则返回0,否则返回非零值
  • 如果发生读写错误,那么就设置错误指示器(ferror)。

例如:

先向文件写入26个小写英文字母,再从文件头偏移3个单位,写入字符串“DEF”

#include

int main()
{
	FILE* fp = fopen("data.txt", "r+");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}
	
	char a = 'a';
	char str[30] = { 0 };
	for (int i = 0; i < 26; i++)
		fputc(a + i, fp);

	fseek(fp, 3, SEEK_SET);

	fprintf(fp, "%s", "DEF");

	fseek(fp, 0, SEEK_SET);
	
	fscanf(fp, "%s", str);
	puts(str);
	
	fclose(fp);
	fp = NULL;

	return 0;
}

output:

abcDEFghijklmnopqrstuvwxyz

7.2 ftell

返回文件指针相对于起始位置的偏移量

long int ftell ( FILE * stream );

例如:

#include

int main()
{
	FILE* fp = fopen("data.txt", "w");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}
	
	char a = 'a';
	for (int i = 0; i < 26; i++)
		fputc(a + i, fp);

	printf("%d\n", ftell(fp));
	
	fclose(fp);
	fp = NULL;

	return 0;
}

output:

26

7.3 rewind

让文件指针的位置回到文件起始位置

void rewind ( FILE * stream );

例如:

#include

int main()
{
	FILE* fp = fopen("data.txt", "w+");
	if (NULL == fp)
	{
		perror("fopen");
		return 1;
	}
	
	char str[30] = { 0 };

	for (int i = 0; i < 26; i++)
		fputc('a' + i, fp);

	rewind(fp);

	for (int i = 0; i < 10; i++)
		fputc('A' + i, fp);

	rewind(fp);

	fscanf(fp, "%s", str);
	puts(str);
	
	fclose(fp);
	fp = NULL;

	return 0;
}

output:

ABCDEFGHIJklmnopqrstuvwxyz

8. 文件读取结束的判定

牢记:在文件读取的过程中,不能使用feof函数的返回值直接来判断文件是否结束

feof的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件末尾

  • 文本文件读取是否结束:
    • fgetc判断返回值是否为EOF
    • fgets判断返回值是否为NULL
  • 二进制文件读取是否结束:
    • fread判断返回值是否小于实际要读的个数

本篇完

你可能感兴趣的:(C语言,c语言,开发语言)