18.C语言文件操作详解:指针、打开、读取与写入
目录
- 1.文件指针
- 2.fopen
- 3.标准流
- 4.fclose
- 5.EOF
- 6.freopen()
- 7.fgetc与getc
- 8. fputc与putc
- 9.fprintf
- 10.fscanf
- 11.fgets
- 12.fputs
- 13.fwrite
- 14.fread
- 15.feof
- 16.fseek
- 17.ftell
- 18.rewind
- 19.fgetpos()与fsetpos
- 20.ferror与clearerr
- 21.remove
- 22.rename
1.文件指针
本篇原文为:C语言文件操作详解:指针、打开、读取与写入。
更多C++进阶、rust、python、逆向等等教程,可点击此链接查看:酷程网
C 语言提供了一个 FILE
数据结构,记录了操作一个文件所需要的信息,该结构定义在头文件stdio.h
,所有文件操作函数都要通过这个数据结构,获取文件信息。
开始操作一个文件之前,就要定义一个指向该文件的 FILE 指针,相当于获取一块内存区域,用来保存文件信息。
比如定义一个 FILE
指针fp
的代码如下:
FILE* fp;
下面是一个读取文件的完整示例:
#include <stdio.h>int main(void) {FILE* fp;char c;fp = fopen("hello.txt", "r");if (fp == NULL) {return -1;}c = fgetc(fp);printf("%c\n", c);fclose(fp);return 0;
}
示例中,新建文件指针fp
以后,依次使用了下面三个文件操作函数,分成三个步骤,其他的文件操作,大致上也是这样的步骤。
第一步,使用fopen()
打开指定文件,返回一个 File 指针,如果出错,返回 NULL。
它相当于将指定文件的信息与新建的文件指针fp
相关联,在 FILE 结构内部记录了这样一些信息:文件内部的当前读写位置、读写报错的记录、文件结尾指示器、缓冲区开始位置的指针、文件标识符、一个计数器(统计拷贝进缓冲区的字节数)等等。
后继的操作就可以使用这个指针(而不是文件名)来处理指定文件。
同时,它还为文件建立一个缓存区,由于存在缓存区,也可以说fopen()
函数“打开了一个流”,后继的读写文件都是流模式。
第二步,使用读写函数,从文件读取数据,或者向文件写入数据,上例使用了fgetc()
函数,从已经打开的文件里面,读取一个字符。
fgetc()
一调用,文件的数据块先拷贝到缓冲区,不同的计算机有不同的缓冲区大小,一般是512字节或是它的倍数,如4096或16384,随着计算机硬盘容量越来越大,缓冲区也越来越大。
fgetc()
从缓冲区读取数据,同时将文件指针内部的读写位置指示器,指向所读取字符的下一个字符。
所有的文件读取函数都使用相同的缓冲区,后面再调用任何一个读取函数,都将从指示器指向的位置,即上一次读取函数停止的位置开始读取。
当读取函数发现已读完缓冲区里面的所有字符时,会请求把下一个缓冲区大小的数据块,从文件拷贝到缓冲区中,读取函数就以这种方式,读完文件的所有内容,直到文件结尾。
不过,上例是只从缓存区读取一个字符,当函数在缓冲区里面,读完文件的最后一个字符时,就把 FILE 结构里面的文件结尾指示器设置为真。于是,下一次再调用读取函数时,会返回常量 EOF。EOF 是一个整数值,代表文件结尾,一般是-1
。
第三步,fclose()
关闭文件,同时清空缓存区。
上面是文件读取的过程,文件写入也是类似的方式,先把数据写入缓冲区,当缓冲区填满后,缓存区的数据将被转移到文件中。
2.fopen
fopen()
函数用来打开文件。所有文件操作的第一步,都是使用fopen()
打开指定文件,这个函数的原型定义在头文件stdio.h
:
FILE* fopen(char* filename, char* mode);
它接受两个参数。第一个参数是文件名(可以包含路径),第二个参数是模式字符串,指定对文件执行的操作,比如下面的例子中,r
表示以读取模式打开文件:
fp = fopen("in.dat", "r");
成功打开文件以后,fopen()
返回一个 FILE 指针,其他函数可以用这个指针操作文件。
如果无法打开文件(比如文件不存在或没有权限),会返回空指针 NULL。所以,执行fopen()
以后,最好判断一下有没有打开成功:
fp = fopen("hello.txt", "r");if (fp == NULL) {printf("Can't open file!\n");exit(EXIT_FAILURE);
}
上面示例中,如果fopen()
返回一个空指针,程序就会报错。
fopen()
的模式字符串有以下几种。
r
:读模式,只用来读取数据。如果文件不存在,返回 NULL 指针。w
:写模式,只用来写入数据。如果文件存在,文件长度会被截为0,然后再写入;如果文件不存在,则创建该文件。a
:写模式,只用来在文件尾部追加数据。如果文件不存在,则创建该文件。r+
:读写模式。如果文件存在,指针指向文件开始处,可以在文件头部添加数据。如果文件不存在,返回 NULL 指针。w+
:读写模式。如果文件存在,文件长度会被截为0,然后再写入数据。这种模式实际上读不到数据,反而会擦掉数据。如果文件不存在,则创建该文件。a+
:读写模式。如果文件存在,指针指向文件结尾,可以在现有文件末尾添加内容。如果文件不存在,则创建该文件。
上一小节说过,fopen()
函数会为打开的文件创建一个缓冲区。读模式下,创建的是读缓存区;写模式下,创建的是写缓存区;读写模式下,会同时创建两个缓冲区,C 语言通过缓存区,以流的形式,向文件读写数据。
数据在文件里面,都是以二进制形式存储,但在读取的时候,有不同的解读方法。
以原本的二进制形式解读,叫做“二进制流”;将二进制数据转成文本,以文本形式解读,叫做“文本流”。写入操作也是如此,分成以二进制写入和以文本写入,后者会多一个文本转二进制的步骤。
fopen()
的模式字符串,默认是以文本流读写。如果添加b
后缀(表示 binary),就会以“二进制流”进行读写。
比如,rb
是读取二进制数据模式,wb
是写入二进制数据模式。
模式字符串还有一个x
后缀,表示独占模式(exclusive)。如果文件已经存在,则打开文件失败;
如果文件不存在,则新建文件,打开后不再允许其他程序或线程访问当前文件。
比如,wx
表示以独占模式写入文件,如果文件已经存在,就会打开失败。
3.标准流
Linux 系统默认提供三个已经打开的文件,它们的文件指针如下。
stdin
(标准输入):默认来源为键盘,文件指针编号为0
。stdout
(标准输出):默认目的地为显示器,文件指针编号为1
。stderr
(标准错误):默认目的地为显示器,文件指针编号为2
。
Linux 系统的文件,不一定是数据文件,也可以是设备文件,即文件代表一个可以读或写的设备。
文件指针stdin
默认是把键盘看作一个文件,读取这个文件,就能获取用户的键盘输入。
同理,stdout
和stderr
默认是把显示器看作一个文件,将程序的运行结果写入这个文件,用户就能看到运行结果了。
它们的区别是,stdout
写入的是程序的正常运行结果,stderr
写入的是程序的报错信息。
这三个输入和输出渠道,是 Linux 默认提供的,所以分别称为标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。因为它们的实现是一样的,都是文件流,所以合称为“标准流”。
Linux 允许改变这三个文件指针(文件流)指向的文件,这称为重定向(redirection)。
如果标准输入不绑定键盘,而是绑定其他文件,可以在文件名前面加上小于号<
,跟在程序名后面。这叫做“输入重定向”(input redirection)。
demo < in.dat
上面示例中,demo
程序代码里面的stdin
,将指向文件in.dat
,即从in.dat
获取数据。
如果标准输出绑定其他文件,而不是显示器,可以在文件名前加上大于号>
,跟在程序名后面。这叫做“输出重定向”(output redirection):
demo > out.dat
上面示例中,demo
程序代码里面的stdout
,将指向文件out.dat
,即向out.dat
写入数据。
输出重定向>
会先擦去out.dat
的所有原有的内容,然后再写入。如果希望写入的信息追加在out.dat
的结尾,可以使用>>
符号:
demo >> out.dat
上面示例中,demo
程序代码里面的stdout
,将向文件out.dat
写入数据。与>
不同的是,写入的开始位置是out.dat
的文件结尾。
标准错误的重定向符号是2>
。其中的2
代表文件指针的编号,即2>
表示将2号文件指针的写入,重定向到err.txt
,2号文件指针就是标准错误stderr
:
demo > out.dat 2> err.txt
上面示例中,demo
程序代码里面的stderr
,会向文件err.txt
写入报错信息。而stdout
向文件out.dat
写入。
输入重定向和输出重定向,也可以结合在一条命令里面:
demo < in.dat > out.dat// or
demo > out.dat < in.dat
重定向还有另一种情况,就是将一个程序的标准输出stdout
,指向另一个程序的标准输入stdin
,这时要使用|
符号:
random | sum
上面示例中,random
程序代码里面的stdout
的写入,会从sum
程序代码里面的stdin
被读取。
4.fclose
fclose()
用来关闭已经使用fopen()
打开的文件,它的原型定义在stdio.h
:
int fclose(FILE* stream);
它接受一个文件指针fp
作为参数。如果成功关闭文件,fclose()
函数返回整数0
;如果操作失败(比如磁盘已满,或者出现 I/O 错误),则返回一个特殊值 EOF
。
if (fclose(fp) != 0)printf("Something wrong.");
不再使用的文件,都应该使用fclose()
关闭,否则无法释放资源。一般来说,系统对同时打开的文件数量有限制,及时关闭文件可以避免超过这个限制。
5.EOF
C 语言的文件操作函数的设计是,如果遇到文件结尾,就返回一个特殊值。程序接收到这个特殊值,就知道已经到达文件结尾了。
头文件stdio.h
为这个特殊值定义了一个宏EOF
(end of file 的缩写),它的值一般是-1
。
这是因为从文件读取的二进制值,不管作为无符号数字解释,还是作为 ASCII 码解释,都不可能是负值,所以可以很安全地返回-1
,不会跟文件本身的数据相冲突。
需要注意的是,不像字符串结尾真的存储了\0
这个值,EOF
并不存储在文件结尾,文件中并不存在这个值,完全是文件操作函数发现到达了文件结尾,而返回这个值。
6.freopen()
freopen()
用于新打开一个文件,直接关联到某个已经打开的文件指针,这样可以复用文件指针。它的原型定义在头文件stdio.h
:
FILE* freopen(char* filename, char* mode, FILE* stream);
它跟fopen()
相比,就是多出了第三个参数,表示要复用的文件指针,其他两个参数都一样,分别是文件名和打开模式:
freopen("output.txt", "w", stdout);
printf("hello");
上面示例将文件output.txt
关联到stdout
,此后向stdout
写入的内容,都会写入output.txt
。
由于printf()
默认就是输出到stdout
,所以运行上面的代码以后,文件output.txt
会被写入hello
。
freopen()
的返回值是它的第三个参数(文件指针),如果打开失败(比如文件不存在),会返回空指针 NULL。
freopen()
会自动关闭原先已经打开的文件,如果文件指针并没有指向已经打开的文件,则freopen()
等同于fopen()
。
下面是freopen()
关联scanf()
的例子:
int i, i2;scanf("%d", &i); freopen("someints.txt", "r", stdin);
scanf("%d", &i2);
上面例子中,一共调用了两次scanf()
,第一次调用是从键盘读取,然后使用freopen()
将stdin
指针关联到某个文件,第二次调用就会从该文件读取。
某些系统允许使用freopen()
,改变文件的打开模式,这时,freopen()
的第一个参数应该是 NULL:
freopen(NULL, "wb", stdout);
上面示例将stdout
的打开模式从w
改成了wb
。
7.fgetc与getc
fgetc()
和getc()
用于从文件读取一个字符,它们的用法跟getchar()
类似,区别是getchar()
只用来从stdin
读取,而这两个函数是从任意指定的文件读取。
它们的原型定义在头文件stdio.h
:
int fgetc(FILE *stream)
int getc(FILE *stream);
fgetc()
与getc()
的用法是一样的,都只有文件指针一个参数,两者的区别是,getc()
一般用宏来实现,而fgetc()
是函数实现,所以前者的性能可能更好一些。
注意,虽然这两个函数返回的是一个字符,但是它们的返回值类型却不是char
,而是int
,这是因为读取失败的情况下,它们会返回 EOF,这个值一般是-1
:
#include <stdio.h>int main(void) {FILE *fp;fp = fopen("hello.txt", "r");int c;while ((c = getc(fp)) != EOF)printf("%c", c);fclose(fp);
}
上面示例中,getc()
依次读取文件的每个字符,将其放入变量c
,直到读到文件结尾,返回 EOF,循环终止。变量c
的类型是int
,而不是char
,因为有可能等于负值,所以设为int
更好一些。
8. fputc与putc
fputc()
和putc()
用于向文件写入一个字符,它们的用法跟putchar()
类似,区别是putchar()
是向stdout
写入,而这两个函数是向文件写入,它们的原型定义在头文件stdio.h
:
int fputc(int char, FILE *stream);
int putc(int char, FILE *stream);
fputc()
与putc()
的用法是一样,都接受两个参数,第一个参数是待写入的字符,第二个参数是文件指针。
它们的区别是,putc()
通常是使用宏来实现,而fputc()
只作为函数来实现,所以理论上,putc()
的性能会好一点。
写入成功时,它们返回写入的字符;写入失败时,返回 EOF。
9.fprintf
fprintf()
用于向文件写入格式化字符串,用法与printf()
类似。区别是printf()
总是写入stdout
,而fprintf()
则是写入指定的文件,它的第一个参数必须是一个文件指针。
它的原型定义在头文件stdio.h
:
int fprintf(FILE* stream, const char* format, ...)
fprintf()
可以替代printf()
:
printf("Hello, world!\n");
fprintf(stdout, "Hello, world!\n");
上面例子中,指定fprintf()
写入stdout
,结果就等同于调用printf()
:
fprintf(fp, "Sum: %d\n", sum);
上面示例是向文件指针fp
写入指定格式的字符串,下面是向stderr
输出错误信息的例子:
fprintf(stderr, "Something number.\n");
10.fscanf
fscanf()
用于按照给定的模式,从文件中读取内容,用法跟scanf()
类似。
区别是scanf()
总是从stdin
读取数据,而fscanf()
是从文件读入数据,它的原型定义在头文件stdio.h
,第一个参数必须是文件指针:
int fscanf(FILE* stream, const char* format, ...);
下面是一个例子:
fscanf(fp, "%d%d", &i, &j);
上面示例中,fscanf()
从文件fp
里面,读取两个整数,放入变量i
和j
。
使用fscanf()
的前提是知道文件的结构,它的占位符解析规则与scanf()
完全一致。
由于fscanf()
可以连续读取,直到读到文件尾,或者发生错误(读取失败、匹配失败),才会停止读取,所以fscanf()
通常放在循环里面。
while(fscanf(fp, "%s", words) == 1)puts(words);
上面示例中,fscanf()
依次读取文件的每个词,将它们一行打印一个,直到文件结束。
fscanf()
的返回值是赋值成功的变量数量,如果赋值失败会返回 EOF。
11.fgets
fgets()
用于从文件读取指定长度的字符串,它名字的第一个字符是f
,就代表file
。它的原型定义在头文件stdio.h
。
char* fgets(char* str, int STRLEN, File* fp);
它的第一个参数str
是一个字符串指针,用于存放读取的内容。第二个参数STRLEN
指定读取的长度,第三个参数是一个 FILE 指针,指向要读取的文件。
fgets()
读取 STRLEN - 1 个字符之后,或者遇到换行符与文件结尾,就会停止读取,然后在已经读取的内容末尾添加一个空字符\0
,使之成为一个字符串。注意,fgets()
会将换行符(\n
)存储进字符串。
如果fgets
的第三个参数是stdin
,就可以读取标准输入,等同于scanf()
:
fgets(str, sizeof(str), stdin);
读取成功时,fgets()
的返回值是它的第一个参数,即指向字符串的指针,否则返回空指针 NULL。
fgets()
可以用来读取文件的每一行,下面是读取文件所有行的例子:
#include <stdio.h>int main(void) {FILE* fp;char s[1024]; // 数组必须足够大,足以放下一行int linecount = 0;fp = fopen("hello.txt", "r");while (fgets(s, sizeof s, fp) != NULL)printf("%d: %s", ++linecount, s);fclose(fp);
}
上面示例中,每读取一行,都会输出行号和该行的内容。
下面的例子是循环读取用户的输入:
char words[10];puts("Enter strings (q to quit):");while (fgets(words, 10, stdin) != NULL) {if (words[0] == 'q' && words[1] == '\n')break;puts(words);
}puts("Done.");
上面的示例中,如果用户输入的字符串大于9个字符,fgets()
会多次读取。直到遇到q
+ 回车键,才会退出循环。
12.fputs
fputs()
函数用于向文件写入字符串,和puts()
函数只有一点不同,那就是它不会在字符串末尾添加换行符。
这是因为fgets()
保留了换行符,所以fputs()
就不添加了。fputs()
函数通常与fgets()
配对使用。
它的原型定义在stdio.h
:
int fputs(const char* str, FILE* stream);
它接受两个参数,第一个参数是字符串指针,第二个参数是要写入的文件指针。如果第二个参数为stdout
(标准输出),就是将内容输出到计算机屏幕,等同于printf()
。
char words[14];puts("Enter a string, please.");
fgets(words, 14, stdin);puts("This is your string:");
fputs(words, stdout);
上面示例中,先用fgets()
从stdin
读取用户输入,然后用fputs()
输出到stdout
。
写入成功时,fputs()
返回一个非负整数,否则返回 EOF。
13.fwrite
fwrite()
用来一次性写入较大的数据块,主要用途是将数组数据一次性写入文件,适合写入二进制数据。它的原型定义在stdio.h
:
size_t fwrite(const void* ptr,size_t size,size_t nmemb,FILE* fp
);
它接受四个参数。
ptr
:数组指针。size
:每个数组成员的大小,单位字节。nmemb
:数组成员的数量。fp
:要写入的文件指针。
注意,fwrite()
原型的第一个参数类型是void*
,这是一个无类型指针,编译器会自动将参数指针转成void*
类型。正是由于fwrite()
不知道数组成员的类型,所以才需要知道每个成员的大小(第二个参数)和成员数量(第三个参数)。
fwrite()
函数的返回值是成功写入的数组成员的数量(注意不是字节数)。正常情况下,该返回值就是第三个参数nmemb
,但如果出现写入错误,只写入了一部分成员,返回值会比nmemb
小。
要将整个数组arr
写入文件,可以采用下面的写法:
fwrite(arr,sizeof(arr[0]),sizeof(arr) / sizeof(arr[0]),fp
);
上面示例中,sizeof(a[0])
是每个数组成员占用的字节,sizeof(a) / sizeof(a[0])
是整个数组的成员数量。
下面的例子是将一个大小为256字节的字符串写入文件:
char buffer[256];fwrite(buffer, 1, 256, fp);
上面示例中,数组buffer
每个成员是1个字节,一共有256个成员。由于fwrite()
是连续内存复制,所以写成fwrite(buffer, 256, 1, fp)
也能达到目的。
fwrite()
没有规定一定要写入整个数组,只写入数组的一部分也是可以的。
任何类型的数据都可以看成是1字节数据组成的数组,或者是一个成员的数组,所以fwrite()
实际上可以写入任何类型的数据,而不仅仅是数组。
比如,fwrite()
可以将一个 Struct 结构写入文件保存:
fwrite(&s, sizeof(s), 1, fp);
上面示例中,s
是一个 Struct 结构指针,可以看成是一个成员的数组。注意,如果s
的属性包含指针,存储时需要小心,因为保存指针可能没意义,还原出来的时候,并不能保证指针指向的数据还存在。
fwrite()
以及后面要介绍的fread()
,比较适合读写二进制数据,因为它们不会对写入的数据进行解读。
二进制数据可能包含空字符\0
,这是 C 语言的字符串结尾标记,所以读写二进制文件,不适合使用文本读写函数(比如fprintf()
等)。
下面是一个写入二进制文件的例子:
#include <stdio.h>int main(void) {FILE* fp;unsigned char bytes[] = {5, 37, 0, 88, 255, 12};fp = fopen("output.bin", "wb");fwrite(bytes, sizeof(char), sizeof(bytes), fp);fclose(fp);return 0;
}
上面示例中,写入二进制文件时,fopen()
要使用wb
模式打开,表示二进制写入。
fwrite()
可以把数据解释成单字节数组,因此它的第二个参数是sizeof(char)
,第三个参数是数组的总字节数sizeof(bytes)
。
上面例子写入的文件output.bin
,使用十六进制编辑器打开,会是下面的内容:
05 25 00 58 ff 0c
fwrite()
还可以连续向一个文件写入数据:
struct clientData myClient = {1, 'foo bar'};for (int i = 1; i <= 100; i++) {fwrite(&myClient, sizeof(struct clientData), 1, cfPtr);
}
上面示例中,fwrite()
连续将100条数据写入文件。
14.fread
fread()
函数用于一次性从文件读取较大的数据块,主要用途是将文件内容读入一个数组,适合读取二进制数据,它的原型定义在头文件stdio.h
:
size_t fread(void* ptr,size_t size,size_t nmemb,FILE* fp
);
它接受四个参数,与fwrite()
完全相同。
ptr
:数组地址。size
:每个数组成员的大小,单位为字节。nmemb
:数组的成员数量。fp
:文件指针。
要将文件内容读入数组arr
,可以采用下面的写法:
fread(arr,sizeof(arr[0]),sizeof(arr) / sizeof(arr[0]),fp
);
上面示例中,数组长度(第二个参数)和每个成员的大小(第三个参数)的乘积,就是数组占用的内存空间的大小。
fread()
会从文件(第四个参数)里面读取相同大小的内容,然后将ptr
(第一个参数)指向这些内容的内存地址。
下面的例子是将文件内容读入一个10个成员的双精度浮点数数组:
double earnings[10];
fread(earnings, sizeof(double), 10, fp);
上面示例中,每个数组成员的大小是sizeof(double)
,一个有10个成员,就会从文件fp
读取sizeof(double) * 10
大小的内容。
fread()
函数的返回值是成功读取的数组成员的数量。正常情况下,该返回值就是第三个参数nmemb
,但如果出现读取错误或读到文件结尾,该返回值就会比nmemb
小。所以,检查fread()
的返回值是非常重要的。
fread()
和fwrite()
可以配合使用。在程序终止之前,使用fwrite()
将数据保存进文件,下次运行时再用fread()
将数据还原进入内存。
下面是读取上一节生成的二进制文件output.bin
的例子:
#include <stdio.h>int main(void) {FILE* fp;unsigned char c;fp = fopen("output.bin", "rb");while (fread(&c, sizeof(char), 1, fp) > 0)printf("%d\n", c);return 0;
}
运行后,得到如下结果:
5
37
0
88
255
12
15.feof
feof()
函数判断文件的内部指针是否指向文件结尾,它的原型定义在头文件stdio.h
:
int feof(FILE *fp);
feof()
接受一个文件指针作为参数。如果已经到达文件结尾,会返回一个非零值(表示 true),否则返回0
(表示 false)。
诸如fgetc()
这样的文件读取函数,如果返回 EOF,有两种可能,一种可能是已读取到文件结尾,另一种可能是出现读取错误。feof()
可以用来判断到底是那一种情况。
下面是通过feof()
判断是否到达文件结尾,从而循环读取整个文件的例子:
int num;
char name[50];FILE* cfPtr = fopen("clients.txt", "r");while (!feof(cfPtr)) {fscanf(cfPtr, "%d%s\n", &num, name);printf("%d %s\n", num, name);
}fclose(cfPtr);
上面示例通过循环判断feof()
是否读到文件结尾,从而实现读出整个文件内容。
feof()
为真时,可以通过fseek()
、rewind()
、fsetpos()
函数改变文件内部读写位置的指示器,从而清除这个函数的状态。
16.fseek
每个文件指针都有一个内部指示器(内部指针),记录当前打开的文件的读写位置(file position),即下一次读写从哪里开始。
文件操作函数(比如getc()
、fgets()
、fscanf()
和fread()
等)都从这个指示器指定的位置开始按顺序读写文件。
如果希望改变这个指示器,将它移到文件的指定位置,可以使用fseek()
函数,它的原型定义在头文件stdio.h
:
int fseek(FILE* stream, long int offset, int whence);
fseek()
接受3个参数。
stream
:文件指针。offset
:距离基准(第三个参数)的字节数。类型为 long int,可以为正值(向文件末尾移动)、负值(向文件开始处移动)或 0(保持不动)。whence
:位置基准,用来确定计算起点。它的值是以下三个宏(定义在stdio.h
):SEEK_SET
(文件开始处)、SEEK_CUR
(内部指针的当前位置)、SEEK_END
(文件末尾)
请看下面的例子:
// 定位到文件开始处
fseek(fp, 0L, SEEK_SET);// 定位到文件末尾
fseek(fp, 0L, SEEK_END);// 从当前位置后移2个字节
fseek(fp, 2L, SEEK_CUR);// 定位到文件第10个字节
fseek(fp, 10L, SEEK_SET);// 定位到文件倒数第10个字节
fseek(fp, -10L, SEEK_END);
上面示例中,fseek()
的第二个参数为 long 类型,所以移动距离必须加上后缀L
,将其转为 long 类型。
下面的示例逆向输出文件的所有字节:
for (count = 1L; count <= size; count++) {fseek(fp, -count, SEEK_END);ch = getc(fp);
}
注意,fseek()
最好只用来操作二进制文件,不要用来读取文本文件。因为文本文件的字符有不同的编码,某个位置的准确字节位置不容易确定。
正常情况下,fseek()
的返回值为0。如果发生错误(如移动的距离超出文件的范围),返回值为非零值(比如-1
)。
17.ftell
ftell()
函数返回文件内部指示器的当前位置,它的原型定义在头文件stdio.h
:
long int ftell(FILE* stream);
它接受一个文件指针作为参数。返回值是一个 long 类型的整数,表示内部指示器的当前位置,即文件开始处到当前位置的字节数,0
表示文件开始处。如果发生错误,ftell()
返回-1L
。
ftell()
可以跟fseek()
配合使用,先记录内部指针的位置,一系列操作过后,再用fseek()
返回原来的位置:
long file_pos = ftell(fp);// 一系列文件操作之后
fseek(fp, file_pos, SEEK_SET);
下面的例子先将指示器定位到文件结尾,然后得到文件开始处到结尾的字节数:
fseek(fp, 0L, SEEK_END);
size = ftell(fp);
18.rewind
rewind()
函数可以让文件的内部指示器回到文件开始处。它的原型定义在stdio.h
。
void rewind(file* stream);
它接受一个文件指针作为参数。
rewind(fp)
基本等价于fseek(fp, 0l, seek_set)
,唯一的区别是rewind()
没有返回值,而且会清除当前文件的错误指示器。
19.fgetpos()与fsetpos
fseek()
和ftell()
有一个潜在的问题,那就是它们都把文件大小限制在 long int 类型能表示的范围内。
这看起来相当大,但是在32位计算机上,long int 的长度为4个字节,能够表示的范围最大为 4GB。随着存储设备的容量迅猛增长,文件也越来越大,往往会超出这个范围。
鉴于此,C 语言新增了两个处理大文件的新定位函数:fgetpos()
和fsetpos()
。
它们的原型都定义在头文件stdio.h
:
int fgetpos(FILE* stream, fpos_t* pos);
int fsetpos(FILE* stream, const fpos_t* pos);
fgetpos()
函数会将文件内部指示器的当前位置,存储在指针变量pos
。该函数接受两个参数,第一个是文件指针,第二个存储指示器位置的变量。
fsetpos()
函数会将文件内部指示器的位置,移动到指针变量pos
指定的地址。
注意,变量pos
必须是通过调用fgetpos()
方法获得的,fsetpos()
的两个参数与fgetpos()
必须是一样的。
记录文件内部指示器位置的指针变量pos
,类型为fpos_t*
(file position type 的缩写,意为文件定位类型)。它不一定是整数,也可能是一个 Struct 结构。
下面是用法示例:
fpos_t file_pos;
fgetpos(fp, &file_pos);// 一系列文件操作之后
fsetpos(fp, &file_pos);
上面示例中,先用fgetpos()
获取内部指针的位置,后面再用fsetpos()
恢复指针的位置。
执行成功时,fgetpos()
和fsetpos()
都会返回0
,否则返回非零值。
20.ferror与clearerr
所有的文件操作函数如果执行失败,都会在文件指针里面记录错误状态。后面的操作只要读取错误指示器,就知道前面的操作出错了。
ferror()
函数用来返回错误指示器的状态。可以通过这个函数,判断前面的文件操作是否成功,它的原型定义在头文件stdio.h
。
int ferror(FILE *stream);
它接受一个文件指针作为参数。如果前面的操作出现错误,ferror()
就会返回一个非零整数(表示 true),否则返回0
。
clearerr()
函数用来重置出错指示器。它的原型定义在头文件stdio.h
:
void clearerr(FILE* fp);
它接受一个文件指针作为参数,没有返回值,下面是一个例子:
FILE* fp = fopen("file.txt", "w");
char c = fgetc(fp);if (ferror(fp)) {printf("读取文件:file.txt 时发生错误\n");
}clearerr(fp);
上面示例中,fgetc()
尝试读取一个以”写模式“打开的文件,读取失败就会返回 EOF,这时调用ferror()
就可以知道上一步操作出错了。处理完以后,再用clearerr()
清除出错状态。
文件操作函数如果正常执行,ferror()
和feof()
都会返回零,如果执行不正常,就要判断到底是哪里出了问题:
if (fscanf(fp, "%d", &n) != 1) {if (ferror(fp)) {printf("io error\n");}if (feof(fp)) {printf("end of file\n");}clearerr(fp);fclose(fp);
}
上面示例中,当fscanf()
函数报错时,通过检查ferror()
和feof()
,确定到底发生什么问题。
这两个指示器改变状态后,会保持不变,所以要用clearerr()
清除它们,clearerr()
可以同时清除两个指示器。
21.remove
remove()
函数用于删除指定文件。它的原型定义在头文件stdio.h
:
int remove(const char* filename);
它接受文件名作为参数。如果删除成功,remove()
返回0
,否则返回非零值:
remove("foo.txt");
上面示例删除了foo.txt
文件。
注意,删除文件必须是在文件关闭的状态下。如果是用fopen()
打开的文件,必须先用fclose()
关闭后再删除。
22.rename
rename()
函数用于文件改名,也用于移动文件,它的原型定义在头文件stdio.h
:
int rename(const char* old_filename, const char* new_filename);
它接受两个参数,第一个参数是现在的文件名,第二个参数是新的文件名。如果改名成功,rename()
返回0
,否则返回非零值:
rename("foo.txt", "bar.txt");
上面示例将foo.txt
改名为bar.txt
。
注意,改名后的文件不能与现有文件同名。另外,如果要改名的文件已经打开了,必须先关闭,然后再改名,对打开的文件进行改名会失败。
下面是移动文件的例子:
rename("/tmp/evidence.txt", "/home/beej/nothing.txt");
相关文章:
18.C语言文件操作详解:指针、打开、读取与写入
目录 1.文件指针2.fopen3.标准流4.fclose5.EOF6.freopen()7.fgetc与getc8. fputc与putc9.fprintf10.fscanf11.fgets12.fputs13.fwrite14.fread15.feof16.fseek17.ftell18.rewind19.fgetpos()与fsetpos20.ferror与clearerr21.remove22.rename 1.文件指针 本篇原文为:…...
深入浅出 OpenResty
1. 引言 1.1. OpenResty简介 OpenResty 是一个基于 Nginx 的高性能 Web 平台,它集成了大量模块,并原生支持 Lua 脚本。这使得开发者能够以非常灵活的方式实现复杂的逻辑,而无需重新编译或扩展 Nginx 核心。OpenResty 的主要特点包括&#x…...

在 .NET 9 中使用 Scalar 替代 Swagger
前言 在.NET 9发布以后ASP.NET Core官方团队发布公告已经将Swashbuckle.AspNetCore(一个为ASP.NET Core API提供Swagger工具的项目)从ASP.NET Core Web API模板中移除,这意味着以后我们创建Web API项目的时候不会再自动生成Swagger API文档了…...

ue5 蒙太奇,即上半身动画和下半身组合在一起,并使用。学习b站库得科技
本文核心 正常跑步动画端枪动画跑起来也端枪 正常跑步动画 端枪动画的上半身 跑起来也端枪 三步走: 第一步制作动画蒙太奇和插槽 第二步动画蓝图选择使用上半身动画还是全身动画,将上半身端枪和下半身走路结合 第三步使用动画蒙太奇 1.开始把&a…...

多活架构的实现原理与应用场景解析
一、多活架构为何如此重要? 企业的业务运营与各类线上服务紧密相连,从日常的购物消费、社交娱乐,到金融交易、在线教育等关键领域,无一不依赖于稳定可靠的信息系统。多活架构的重要性愈发凸显,它宛如一位忠诚的卫士,为业务的平稳运行保驾护航。 回想那些因系统故障引发的…...
一 rk3568 Android 11固件开发环境搭建 (docker)
一 目标 搭建 rk3568 android 系统内核 及固件开发编译调试环境, 支持开发环境导出分享 基于荣品 rk3568 核心板 系统环境: ubuntu22.04 /ubuntu20.04 64位桌面版 编译环境: docker + ubuntu20.04 , 独立的容器隔离环境,不受系统库版本冲突等影响,无性能损耗, 可…...
小结:华为路由器常用的操作指令
以下是华为路由器常用的操作指令(适用于华为企业级路由器,基于VRP操作系统): 1. 基本操作命令 进入用户视图模式 <Huawei> (默认进入用户视图) 进入系统视图模式 <Huawei> system-view [Huawe…...
旅游网站设计与实现
文末附有完整项目代码 在当今数字化时代,旅游网站成为人们获取旅游信息的重要途径。本文将详细介绍旅游网站的设计与实现,让你轻松了解其中的技术奥秘! 一、项目背景 随着社会经济的发展,人们对精神消费愈发重视,旅游…...

【AI大模型】BERT GPT ELMo模型的对比
目录 🍔 BERT, GPT, ELMo之间的不同点 🍔 BERT, GPT, ELMo各自的优点和缺点 🍔 小结 学习目标 理解BERT, GPT, ELMo相互间的不同点理解BERT, GPT, ELMo相互比较下的各自优点和缺点 🍔 BERT, GPT, ELMo之间的不同点 关于特征提取…...

qt 快捷功能 快速生成 setter getter 构造函数 父类虚函数重写 成员函数实现 代码框架 查看父类及父类中的虚函数
qt 快速生成 setter getter 构造函数 父类虚函数重写 成员函数实现 代码框架 1、找到要实现的头文件 2、鼠标移动到在头文件中的类定义的类名上,右键进行选择。 这是插入父类虚函数(父类虚函数重写) 选项弹出来的结果。可以查看到所有父类及父类中的所有的虚函数...

【计算机网络】深入浅出计算机网络
第一章 计算机网络在信息时代的作用 计算机网络已由一种通信基础设施发展成一种重要的信息服务基础设施 CNNIC 中国互联网网络信息中心 因特网概述 网络、互联网和因特网 网络(Network)由若干结点(Node)和连接这些结点的链路…...

springMVC---resultful风格
目录 一、创建项目 pom.xml 二、配置文件 1.web.xml 2.spring-mvc.xml 三、图解 四、controller 一、创建项目 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi…...

汽车基础软件AutoSAR自学攻略(三)-AutoSAR CP分层架构(2)
汽车基础软件AutoSAR自学攻略(三)-AutoSAR CP分层架构(2) 下面我们继续来介绍AutoSAR CP分层架构,下面的文字和图来自AutoSAR官网目前最新的标准R24-11的分层架构手册。该手册详细讲解了AutoSAR分层架构的设计,下面让我们来一起学习一下。 Introductio…...
IntelliJ IDEA 主题插件
在 IntelliJ IDEA 中,有很多优秀的主题插件可以帮助你改变 IDE 的外观和配色方案,使得开发过程更加愉悦和高效。以下是一些非常受欢迎和实用的 主题插件,以及如何安装和使用它们的步骤: 🌟 流行主题插件推荐 1️⃣ Ma…...

2025最新JAVA面试八股文【基础篇】
1、面向对象和面向过程的区别 2、标识符的命名规则 3、Java自动装箱与拆箱 4、 方法重载和方法重写的区别 5、 equals与 的区别 6、 Hashcode的作用 7、 String、StringBuffer 和 StringBuilder 的区别是什么? 8、ArrayList和linkedList的区别 9、 HashMap和HashTabl…...
element plus 使用 el-tree 组件设置默认选中和获取所有选中节点id
1. 设置默认选中: 使用 default-checked-keys 属性,设置默认要选中的节点,以数组形式,如下: <el-treeref"treeRef":data"data"show-checkboxnode-key"id":props"defaultProps…...
高级java每日一道面试题-2025年01月09日-数据库篇-视图是什么?
如果有遗漏,评论区告诉我进行补充 面试官: 视图是什么? 我回答: 在Java高级面试中,当讨论到数据库中的视图(View)时,通常指的是一种虚拟表或逻辑表。视图并不存储实际数据,而是基于SQL查询定义的一种数据表示形式。…...
python学习整理
在cmd界面里 安装python notebook 1.pip install notebook -i https://pypi.tuna.tsinghua.edu.cn/simple 打开 notebook jupyter notebook 更改默认路径 找到配置文件路径: jupyter notebook --generate-config 搜索’notebook_dir 内容进行修改路径&#x…...

Qt 5.14.2 学习记录 —— 십이 QLineEdit、QTextEdit
文章目录 1、QLineEdit1、写程序2、正则表达式检查电话号码3、验证两次输入的密码是否一致4、切换显示密码状态 2、TextEdit1、多行编写2、信号 1、QLineEdit text在代码上改变或者界面上直接改动都会修改这个属性。 clearButtonEnabled,输入框为空,没有…...
【LC】2270. 分割数组的方案数
题目描述: 给你一个下标从 0 开始长度为 n 的整数数组 nums 。 如果以下描述为真,那么 nums 在下标 i 处有一个 合法的分割 : 前 i 1 个元素的和 大于等于 剩下的 n - i - 1 个元素的和。下标 i 的右边 至少有一个 元素,也就是…...

大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
golang循环变量捕获问题
在 Go 语言中,当在循环中启动协程(goroutine)时,如果在协程闭包中直接引用循环变量,可能会遇到一个常见的陷阱 - 循环变量捕获问题。让我详细解释一下: 问题背景 看这个代码片段: fo…...

【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
相机Camera日志分析之三十一:高通Camx HAL十种流程基础分析关键字汇总(后续持续更新中)
【关注我,后续持续新增专题博文,谢谢!!!】 上一篇我们讲了:有对最普通的场景进行各个日志注释讲解,但相机场景太多,日志差异也巨大。后面将展示各种场景下的日志。 通过notepad++打开场景下的日志,通过下列分类关键字搜索,即可清晰的分析不同场景的相机运行流程差异…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...