磁盘上的文件是文件
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
而数据文件又分为文本文件和二进制文件
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
如果要求在外存上以ASCII码形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件
例如对于整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符占一个字节),而二进制形式输出,则在磁盘上只占4个字节,如图所示
可能有小伙伴会问:文本文件只是二进制二进制文件的特殊形式,为什么要特殊照顾文本文件呢?
- 文本文件符合人类的阅读习惯、应用场景多,可以让人类简化操作
- 保障数据读写的真实意图,这样能在不同的系统里有较好的兼容性
- 可以有换行的概念‘\n’,字符有合法范围因此文件结束符可以特殊
一个文件对应一个唯一的文件标识(即文件名)
同一路径下不可能出现同名文件
文件名包含3部分:文件路径 + 文件名主干 + 文件后缀,例如:
注:文件名中不一定包含后缀名
文件路径又分为绝对路径和相对路径:
文件的绝对路径是指从根目录开始到文件的完整路径,包括所有的目录层级。例如,Windows系统中的绝对路径可能是:“C:\Users\username\Documents\file.txt”
**相对路径是指相对于当前工作目录或者其他已知目录的路径。相对路径不包含根目录,而是使用特定的标识符来表示路径的位置关系。**例如,如果当前工作目录是"/home/username/Documents",那么相对路径"file.txt"表示文件位于当前工作目录下的文件"file.txt"。
在相对路径中,还可以使用特殊的标识符来表示位置关系。例如,"…“表示父级目录,”."表示当前目录。因此,如果当前工作目录是"/home/username/Documents",那么相对路径"…/file.txt"表示文件位于父级目录下的文件"file.txt"。
需要注意的是,相对路径是相对于当前工作目录或其他已知目录的路径,所以在不同的环境中可能会有不同的结果。因此,在编写代码或指定文件路径时,最好使用绝对路径来确保准确性和可移植性。
什么是流?
在C语言中,“流”(stream)是一种用于输入和输出数据的抽象概念。它是一种数据的传输方式,可以将数据从一个地方传送到另一个地方。在C语言中,输入流和输出流是通过一组标准库函数来实现的,这些函数允许程序从键盘或文件中读取数据,或者将数据写入到屏幕或文件中。
C语言中的流可以分为标准流(standard streams)和文件流(file streams)
注:C语言中操作流的主要函数是标准I/O库中的stdio.h
头文件中定义的函数。
我们需要清楚,C语言程序,只要运行起来,就会默认打开3个流(标准流)
标准输入流(stdin):用于读取输入数据,默认情况下是键盘输入。
标准输出流(stdout):用于向终端或命令行窗口输出数据。
标准错误流(stderr):用于输出错误信息。
C语言中的文件流是一种用于在程序中读取和写入文件的流。通过文件流,可以在C程序中打开文件,从文件中读取数据或将数据写入文件中。这样可以有效地处理大量数据、持久性存储以及与文件系统的交互。
本次,我们重点讨论文件流
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE。
我们一般都是用过一个FILE的指针来维护这个FILE结构的变量,例如:
FILE* pf; //文件指针变量
可以通过这个FILE指针pf来找到与这个文件对应的文件信息区
在使用文件之前应该打开文件,使用完之后应该关闭文件
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;
}
#include
int main()
{
//用写的方式打开文件,如果文件不存在,会在该路径底下创建一个新的名为data.txt的文件
FILE* fp = fopen("data.txt", "w");
if (NULL == fp)
{
perror("fopen");
return 1;
}
fclose(fp);
fp = NULL;
return;
}
在上面我们使用的都是与执行文件相同路径底下的文件,如果是其他路径的文件又该如何打开呢?
有两种方法:
方法一:使用相对路径
例如我们要打开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;
}
方法二:绝对路径
例如,我们要打开桌面上的名为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”
可能有许多小伙伴会认为,文件的输入就是将数据写到文件里,文件的输出就是将文件的内容读取出来,然而事实却恰恰相反
文件输入(File Input): 文件输入是指将外部文件中的数据读取到程序中进行处理的过程。
文件输出(File Output):是指将程序中的数据写入到外部文件中的过程。
我们也可以画一个图来表示这个关系:
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本输入函数 | fgets | 所有输入流 |
文本输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
int fgetc ( FILE * stream );
示例:
若在.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
int fputc ( int character, FILE * stream );
示例:
向同一文件逐个写入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
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
fgets
:
char * fgets ( char * str, int num, FILE * stream );
gets
:
char * gets ( char * str );
例如:
#include
int main()
{
char str1[20] = { 0 };
char str2[20] = { 0 };
gets(str1);
fgets(str2, 5 + 1, stdin);
return 0;
}
同时在标准输入流读入“abcd\n”,利用调试,可以发现二者区别
int fputs ( const char * str, FILE * stream );
例如:
将字符串“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
fputs
:
int fputs ( const char * str, FILE * stream );
puts
:
int puts ( const char * str );
例如:
#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
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
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
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
中加入一个空格,那么返回值就会加一
printf
:
int printf ( const char * format, ... );
sprintf
:
int sprintf ( char * str, const char * format, ... );
fprintf
:
int fprintf ( FILE * stream, const char * format, ... );
例如:
#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
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
以二进制的形式从流中读取count个元素,每个元素的大小为size字节,并将它们存储在 ptr 指定的内存块中。
返回成功读取的元素总数。
如果此数字与 count
参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都会设置正确的指标,可以分别用 ferror 和 feof 进行检查。
如果size
或count
为零,则该函数返回零,并且流状态和 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
1200
是文本信息,每个字符对应的ASCII码为49,50,48,48
,转换为二进制码:0011 0001 0011 0010 0011 0000 0011 0000
808464945
的二进制形式:注:
对小端字节序存储该不太了解的同学请看这里传送门
在C语言中,文件的数据存储方式取决于计算机的体系结构和操作系统。通常情况下,对于大多数计算机系统,文件的数据是按照字节顺序存储的,这意味着在内存中,文件前面的数据存储在较低的地址,后面的数据存储在较高的地址。
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
以二进制的形式将ptr存储的数据写入流中,一共写入count个元素,每个元素的大小为size字节。
返回成功写入的元素总数。
如果此数字与 count
参数不同,则写入错误阻止函数完成。在这种情况下,将为流设置错误指示器(ferror)。
如果size
或count
为零,则该函数返回零,错误指示器保持不变。
例如:
向文件中写入字符串“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
注1:当文件被打开时,文件指针默认值向文件的起始位置
注2:当文件以追加的方式打开时,不允许人为改变文件指针位置
根据文件位置和偏移量来定位文件指针
int fseek ( FILE * stream, long int offset, int origin );
offset
为偏移量origin
为起始位置,有三种取值:SEEK_SET:文件头
SEEK_CUR:文件指针的当前位置
SEEK_END:文件尾
例如:
先向文件写入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
返回文件指针相对于起始位置的偏移量
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
让文件指针的位置回到文件起始位置
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
牢记:在文件读取的过程中,不能使用feof
函数的返回值直接来判断文件是否结束
feof
的作用是:当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件末尾
fgetc
判断返回值是否为EOFfgets
判断返回值是否为NULLfread
判断返回值是否小于实际要读的个数本篇完