c语言程序文件包括源文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。程序文件通常包含变量和常量的定义,以及对这些变量和常量进行操作的函数和控制结构。程序文件中的数据通常是用来进行计算、处理和操作的,而不是用来存储和持久化的。如果需要将数据保存到文件中,通常会使用数据文件来实现。
数据文件专门用于存储数据,可以使用不同的格式和扩展名来区分不同类型的数据,如文本文件、图像文件、音频文件等。这些数据文件可以被程序读取、修改和更新,以实现数据的持久化和存储。
c语言程序文件通常是以ASCII码形式保存的,当程序文件被编译时会被转换成二进制的目标文件或者可执行文件,但是源代码本身依旧是以ASCII码形式保存的文本文件
数据文件可以是二进制形式保存的,也可以是各种编码格式保存的文本文件(包括ASCII码)
比如一个整数10000,它如果以ASCII码形式存储,1会转变为对应的ASCII码值00110001,后面的0也会转变为对应的ASCII码形式00110000,所以1000的ASCII码存储为00110001 00110000 00110000 00110000 00110000 00110000
而10000的二进制形式为00000000 00000000 00100111 00010000?
每个被使用的数据文件都会在内存中开辟一个相对应的文件信息区,用来存放文件的相关信息(文件名称,文件状态以及文件当前的位置等)。这些信息是保存在一个结构体变量当中的,被重命名为FILE,这个结构体是默认已经建好了的,不用我们重新写,一般很多对文件的操作是通过结构体指针FILE *来完成的。
文件在进行读写操作前,应该先打开文件,结束后关闭文件,打开是用fopen函数实现的,
const char *filename是指你现在要操作的文件名,const char * mode是指你要执行的操作模式,是读还是写。最后的返回值是FILE *结构体指针形式
如FILE * pf=fopen("data.txt","w")以写的形式的来打开文件data.txt,如果没有这个文件就重新建一个文件data.txt
FILE * pf=fopen("data.txt","r")以读的形式的来打开文件data.txt,如果没有这个文件会直接报错,不会重新建一个文件
常见的文件操作模式如下
“r”(只读)?为了读数据,打开?个已经存在的?本?件,如果这个文本文件不存在会直接报错
“w”(只写)? ? 为了写数据,打开一个已经存在的文本文件,如果这个文件不存在那么就直接建一个这个文件名相同的文件
“a”(追加)向?本?件尾添加数据,如果这个文件不存在,建??个新的?件
“rb”(只读)为了读数据,打开?个?进制文件,如果这个文件不存在会报错
“wb”(只写)为了写数据,打开?个?进制?件,如果这个文件不存在就建??个新的?件
“ab”(追加)?向?个?进制?件尾添加数据,如果这个文件不存在就建??个新的?件
“r+”(读写)意思是我即要读也要在后面追加新的数据,打开?个?本?件,如果这个文件不存在就直接报错
“w+”(读写)为了读和写,建议?个新的?件,如果文件不存在,那么就建??个新的?件
“a+”(读写)?打开?个?件,在?件尾进?读写,如果文件不存在就建??个新的?件
“rb+”(读写)为了读和写打开?个?进制?件,如果文件不存在就建??个新的?件
“wb+”(读写)为了读和写,新建?个新的?进制?件,如果文件不存在就建??个新的?件
“ab+”(读写)打开?个?进制?件,在?件尾进?读和写,如果文件不存在就建??个新的?件
总结一下只要是“r”形式,就算是“r+”,这种要追加新内容的,如果文件不存在那么都会报错。“wb”和“rb”这种后面有b的都是打开二进制文件。
文件的关闭操作是通过fclose函数来实现的,但是你得先打开了才有关闭
FILE * stream是指文件指针流,fclose
函数用于关闭一个已打开的文件流,并释放与该流相关的资源。fclose
函数的作用是关闭文件流,而不是释放指针所指的空间。当调用fclose
函数关闭文件流后,文件指针pf
仍然存在,只是不再指向任何有效的文件流。将文件指针置为NULL的目的是为了避免程序在之后误用已经关闭的文件指针,因为使用已经关闭的文件指针可能导致未定义的行为,所以要在后面pf=NULL
所以文件打开和关闭的操作基本框架如下
int main()
{
FILE* pf = fopen("data.txt", "w");//以写的形式打开文件data.txt
if (pf != NULL)
{
//各种操作。。。。。。//
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
值得注意的是文件路径data.txt是相对路径,是相对于当前这个工作文件的路径,如果data.txt文件在桌面上?,那么就得把根目录什么的都写上。比如C:\Users\86177\Desktop
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输?输出操作各不相同,为了?便程序员对各种设备进??便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。C程序针对?件、画?、键盘等的数据输?输出操作都是通过流操作的。?般情况下,我们要想向流?写数据,或者从流中读取数据,都是要打开流,然后操作C语言中的流是一个抽象概念,它代表了数据的流动,可以是输入流或输出流。流的概念并不直接涉及到空间的划分,而是将数据的流动抽象为一个逻辑上的概念。在C语言中,可以使用流来进行文件的读写操作,但并不意味着流本身划分了一块空间。实际上,流是通过文件指针来操作文件的,而文件指针则是指向文件在磁盘上的位置,而不是直接划分了一块空间。
实在不明白就直接把文件指针当作流吧,在C语言中,流(stream)通常是通过FILE类型的指针来表示的。因此,当我们使用fopen函数打开一个文件时,返回的指针实际上是一个指向FILE类型的流的指针。所以在这种情况下,可以将流视为指针。
FILE* pf = fopen("data.txt", "w")这行代码打开了一个名为"data.txt"的文件,并将其设置为写入模式。返回的指针pf是一个指向FILE类型的流,可以用于向文件中写入数据。那么问题来了不是说流?写数据,或者从流中读取数据,都是要打开流吗,不是说键盘和屏幕输入输出也是要通过流去操作吗,FILE* pf = fopen("data.txt", "w")这个我明白打开了流,我的printf和scanf这些直接输入输出在屏幕上也没见打开流啊?
stdout
是C语言中的标准输出流。它是一个指向FILE
类型的指针,代表了程序的标准输出设备。通常情况下,stdout
指向屏幕,这意味着使用printf
函数输出的数据会显示在屏幕上。
stdin 标准输入流,在大多数环境中从键盘输入,scanf就是从标准输入流中读取数据的
stderr 标准错误流,和stdout类似都是直接输出到屏幕上
这三个标准流不用打开,C语言直接默认打开了它们三个流,程序文件也是文本文件因此这三个流的类型也是FILE *类型,其实也可以叫做文件指针
比如scanf用键盘输入一个数,保存在内存中这叫输入。但是对于内存来说这也可以叫读,内存从键盘中读入数据到内存中。虽然对于我们来说scanf输入数应该是写啊,但是对于内存来说scanf输入数据对应读
printf输出一个数据到屏幕上,相对于内存来说其实是把内存中的数据写到屏幕上,所以输出printf其实对应的是写
这两个不能搞混了,输入是读,输出是写
fputc是字符输出函数
int character是指你需要输出(写)到文件当中的字符ASCII码值,FILE * stream是指你要在哪个打开的流进行操作
?fputc('A',pf)和fputc(65,pf)打开文件data.txt都是A,因为A的ASCII码值是65,所以它会把65对应的符号打印在你操作的文件中
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
fputc(65, pf);
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
pf是我要进行操作的文件data.txt的流,stdout是屏幕的流,如果你要打印到屏幕上,把pf改成stdout就行了
?
为什么fputc返回值是int类型呢,?具体来说,fputc
函数返回的是写入的字符的unsigned char类型的值,或者在发生错误时返回EOF
。EOF
是一个表示文件结束或者出现错误的特殊值,它通常被定义为一个负数。因此,为了能够同时返回写入的字符值和表示错误的EOF
值,fputc
的返回类型被定义为int
,以便能够区分这两种情况。
写26个字母写进文件里
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('A' + i,pf);
}
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
fgetc字符输入函数(读)?
我们上面已经写了26个英文字母进去,用fgetc挨个读出来放到屏幕上
现在是读模式,所以首先“w”要改成“r”,fgetc读完文件结束标志是EOF,所以用EOF作为判断结束标志
const * str 是你要输出(写)到文件中的字符串
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
const char* str = "abcdefg";
fputs(str, pf);
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
fgets文本行输入函数
int num是指你要读出几个数据,char * str是存放读出数据的容器(字符串数组)?
值得注意的是虽然你规定的是读出num个数据,但实际上显示的只有num-1个数据,因为最后一个默认为‘\0’
fprintf和printf非常相似
?唯一的区别是fprintf多了FILE * stream,printf是默认输出到屏幕上,fprintf是输出到FILE * stream 对应的文件夹里
printf("%d",m) m是个变量,对应的fprintf应该为fprintf(pf,"%d",m);
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
int m = 5;
fprintf(pf,"%d", m);
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
对于有多个类型成员的结构体来说,printf函数是这样打印到屏幕里的?
?
而fprintf函数是这样打印输出的
#include <stdio.h>
struct student
{
const char* name;
int age;
int sno;
};
int main()
{
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
struct student st = { "zhangsan",20,2023 };
fprintf(pf,"%s %d %d", st.name, st.age, st.sno);
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
?文件里的情况是这样的
同样fscanf和scanf也只是多了一个流pf而已?
scanf是这样写的
fscanf是这样写的
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct student
{
char name[20];
int age;
int sno;
};
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf != NULL)
{
struct student st = {0};
fscanf(pf,"%s %d %d", st.name, &(st.age),&(st.sno));
printf("%s %d %d", st.name,st.age, st.sno);
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
?
区别在于scanf是键盘输入的打印到屏幕上,而fscanf是将原先文件中已经有了的读出来打印到屏幕上
之前的所有读写函数适用于所有输入输出流,而?fread和fwrite只适用于二进制输入输出流
?const void * ptr是你要写的数据元素的首地址,size_t size是你要写的一个数据元素的大小,size_t count是你要写几个元素的数量,FILE * stream是写进的内容对应的文件夹
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "wb");
if (pf != NULL)
{
int arr[6] = {6,8,3,4,5,6};
fwrite(arr, sizeof(arr[0]), 4, pf);
fclose(pf);//完成操作后关闭文件
pf = NULL;//文件指针置为空
}
}
?
直接打开文件查看结果为什么是这么奇怪的符号,因为fwriter是以二进制的形式直接写进去的,而记事本是直接通过ASCII码或者UTF-8等一系列编码后的解码结果,所以才会看上去那么奇怪。可以通过二进制读形式fread读出来
fread的形式看上去和fwrite很像,但是所表达的含义却大有不同。
FILE * stream是只从这个流里面取出size_t count个size_t size大小的数据元素放到ptr首元素地址的容器中,这个ptr类型是void*?所以容器可以是数组也可以是结构体
这就把原先fwrite写进data.txt文件中的6 8 3 4取出来放到arr中了,而且也不是乱码,所以二进制写进去的要用二进制读出来
顺序读写是指按照文件中数据的存储顺序进行读写。在顺序读写时,我们通常从头到尾或从尾到头地读取或写入数据。这种方式的优点是简单易懂,不需要关心数据的具体位置,只需要按照顺序读取或写入即可。但是,如果需要读取或写入的数据不连续,顺序读写可能会浪费大量的时间和资源。随机读写是指按照指定的位置或偏移量对文件进行读写。在随机读写时,我们需要知道数据的具体位置,然后直接定位到该位置进行读取或写入。这种方式的优点是可以快速地定位到需要的数据,避免顺序读写时的大量时间和资源浪费。但是,随机读写需要更多的内存和计算资源,同时还需要考虑数据的位置和偏移量等问题。
fseek函数是根据文件指针的位置和偏移量来定位文件指针的函数
offset
:从whence
位置开始的偏移量。偏移量可以是正数或负数,表示向前或向后移动
origin:起始位置,它有三个可能的值
? ?SEEK_SET
:文件开始处(即0位置)。
? ?SEEK_CUR
:当前位置
? ?SEEK_END
:文件结尾。
#include <stdio.h>
int main()
{
int i = 0;
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
fputs("this is banaancn", pf);
fseek(pf, 9, SEEK_SET);
fputs("呵呵", pf);
fclose(pf);
pf = NULL;
}
}
在我第一次输入完?this is banaancn后光标本来是应该在cn后面接着输入的,按顺序输出本应该输出这样的结果
?可是现在却是这样的结果
fseek就是改变输出光标位置的函数,从而改变输出结果。fseek(pf, 9, SEEK_SET);这个的意思就是说从开头位置算起挪动到第九个位置也就是b的后面,所以后面打印sw是在b字母的后面
?返回文件指针相对于起始位置的偏移量
举个例子
?
为什么最后偏移量是18呢,上一个this字符串总共有16个字符,加上sw共18个字符?,光标相对于起始位置是18,所以ftell偏移量为18
而不把fseek屏蔽结果如下
虽然输出完第一个this字符串光标在字符串末尾第16位,但是通过fseek函数把光标挪到距离起始位置偏移量为9的位置上了,然后再输出第二个字符串sw,所以此时光标在偏移量为11的位置上
让文件指针的位置直接回到文件的起始位置
和ftell用法差不多,直接rewind(pf)就可以了
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int i = 0;
FILE* pf = fopen("data.txt", "w");
if (pf != NULL)
{
fputs("this is banaancn", pf);
rewind(pf);
fputs("sw", pf);
long ret=ftell(pf);
printf("%ld", ret);
fclose(pf);
pf = NULL;
}
}
文件读取结束有两种情况,一种是真的已经读完了文件所以结束,另一种是读取过程中出了错误直接强行停止
feof函数的作用是:当文件读取结束时,判断是否读取结束的原因是遇到文件尾结束的,因为读取过程中遇到文件尾也会结束
ferror函数的作用是在文件读取结束后,判断是否因为错误而停止
当使用诸如?fgetc
、fgets
、fscanf
?等函数从文件中读取数据时,如果发生了错误(如文件不存在、权限问题、文件被占用等),这些函数会返回?EOF
,同时会自动设置文件错误指示器为活动状态。你可以通过调用?ferror
?函数来检查文件错误指示器的状态。
对于?feof
?函数,它用于检测文件结束指示器的状态,以确定是否已经到达了文件的末尾。feof
?函数没有返回值,它的作用是返回一个非零值(通常是1)或零(0),表示文件结束指示器的状态。
具体来说,当调用?feof
?函数时,如果文件结束指示器处于文件结束的状态,函数会返回非零值1,表示已经到达了文件末尾。否则,如果文件结束指示器不处于文件结束的状态,函数会返回零0,表示尚未到达文件末尾。
对于fgetc来说光是检查是否返回EOF是不够的,因为fgetc读取失败或者文件结束的都会返回EOF,所以还得检查错误指示器的状态,两者都没有问题才代表真的是因为文件读取结束而结束
对于fgets来说读取失败和文件读取结束都会返回NULL,所以还得检查错误指示器的状态,两者都没有问题才代表真的是因为文件读取结束而结束
二进制文件中读取结束判断是判断返回值是否小于实际要读的个数,fread
?函数是用于从文件中读取数据块的函数,其返回值是成功读取的数据块数量。
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
ptr
:指向要读取数据的缓冲区的指针。size
:每个数据块的字节数。count
:要读取的数据块的数量。stream
:指向要读取的文件的指针。所以直接拿fread返回值和count要读的数量相比是否相等就可以判断是否文件读取结束
文件错误指示器和文件结束指示器实际上是通过一些标志位来表示的。标准库中的文件类型?FILE
?结构体通常会包含一个用于表示文件错误的标志位(比如?ferror
)和一个用于表示文件结束的标志位(比如?feof
)。这些标志位的具体实现和维护是由C标准库来处理的,你无需直接操作这些标志位。相反,你可以使用相应的函数(feof和ferror)来查询这些标志位的状态。
需要注意的是,文件结束指示器和文件错误指示器是相互独立的。即使在没有发生错误的情况下,当读取操作到达文件末尾时,文件结束指示器也会被设置为活动状态。因此,在处理文件读取时,通常需要先检查文件错误指示器的状态,再检查文件结束指示器的状态
文本文件的例子
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf != NULL)
{
char c = 0;
while ((c = fgetc(pf) )!= EOF)
{
printf("%c ", c);
}
if (ferror(pf))
printf("出错了,没打印完");
else if (feof(pf))
printf("文件读取完毕");
fclose(pf);
pf = NULL;
}
}
二进制文件的例子
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
FILE* pf = fopen("data.txt", "wb");
if (pf != NULL)
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,8 };
fwrite(arr, sizeof(arr[0]), 5, pf);
fclose(pf);
pf = NULL;
}
FILE* pp = fopen("data.txt", "rb");
if (pp != NULL)
{
int brr[10];
int ret = fread(brr, sizeof (brr[0]), 5, pp);
if (ret == 5)//如果相等说明文件读取结束了,看一下现在里面的内容
{
for (int i = 0; i < 5; i++)
{
printf("%d", brr[i]);
}
}
else
{
if (ferror(pp))
printf("出错了,没打印完");
else if (feof(pp))
printf("文件读取完毕");
}
fclose(pp);
}
}
ret == 5
?是通过判断?fread
?函数的返回值来确定读取的元素数量是否正确。如果返回值等于 5,表示成功读取了 5 个元素,即文件读取完毕。这是一种更准确的判断方法。
而?feof(pp)
?则是通过判断文件结束指示器的状态来确定文件是否读取完毕。如果?feof(pp)
?返回非零值,表示文件已经到达了末尾,即文件读取完毕
如果你只关心文件是否读取完毕,而不需要进一步区分错误类型,那么你可以省略?feof(pp)
?的判断,只使用?ret == 5
?来判断文件读取完毕即可,feof(pp)
?和?ret == 5
?都可以用来判断文件是否读取完毕。它们的含义是相同的,都表示文件已经到达了末尾,没有更多的数据可读取了
? ? ? ???