2.什么是文件 3.文件的打开和关闭 4.文件的顺序读写 5.文件的随机读写 6.文本文件和二进制文件 7.文件读取结束的判定 8.文件缓冲区
一、文件相关介绍
1、为什么使用文件
文件用于永久存储数据。通过使用文件,我们可以在程序关闭后保存数据,以便将来可以重新访问和修改。文件是数据存储和交换的重要手段。它们用于日志记录,数据分析,保存用户配置,程序间的数据传输等。
2、什么是文件
文件是存储在某种长期存储设备上的一系列数据的集合,如硬盘、SSD、光盘或USB驱动器。计算机中的文件可以包含文本、图像、音频、视频数据或任何其他形式的信息,并且通常通过文件系统来组织和访问。
在程序设计方面,我们一般讨论两种文件:程序文件和数据文件(从文件功能角度分析)。
1)程序文件:
程序文件包含了可执行代码或者源代码,它们被用来指导计算机进行特定的操作。可执行文件(如.exe
在Windows系统中或无扩展名的可执行文件在Unix系统中)是由源代码编译而成,可以直接被操作系统加载到内存中执行。源代码文件(如.c
、.java
或.py
文件)包含了用高级编程语言书写的指令,它们需要通过编译器或解释器转换为机器语言才能被计算机执行。
2)数据文件:
数据文件用于存储由程序创建或处理的数据。这些数据可能是输入数据,如配置文件、用户输入或外部来源数据;也可能是输出数据,如程序运行结果、日志文件或报告。数据文件通常不包含执行指令,而是存储信息,程序通过读写数据文件来维护状态、记录历史或与其他程序交互。
3)文件名和文件路径
文件名和文件路径是用于标识和定位计算机文件系统中文件的重要元素。
文件名: 文件名是分配给文件的唯一标识符。它允许用户和系统识别和引用文件。文件名通常包括文件主名称和文件扩展名两部分。文件扩展名通常由一个点(.)与主名称分隔,并用于指示文件的类型,例如 .txt
表示文本文件,.jpg
表示JPEG图像文件。操作系统通常会根据文件扩展名来决定如何处理或打开该文件。
文件路径: 文件路径是描述文件在文件系统中位置的字符串。它提供了从根目录或其他起始点到达文件所需遍历的目录(文件夹)序列。文件路径可以是绝对的或相对的。
-
绝对路径 定义了文件在文件系统中的确切位置,从根目录(在Windows中是驱动器号,如
C:\
;在UNIX-like系统中是/
)开始。例如,C:\Users\Example\Documents\file.txt
是一个绝对路径,它详细描述了如何从C盘根目录开始找到file.txt
。 -
相对路径 相对于当前工作目录来定义文件的位置。它不从根目录开始,而是使用
.
(表示当前目录)或..
(表示上级目录)等符号来导航。例如,如果当前工作目录是C:\Users\Example
,则相对路径Documents\file.txt
引用的是C:\Users\Example\Documents\file.txt
。
二、文件的相关操作
1、通过什么对文件操作
1)文件指针
缓冲文件操作系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE。
例如,一个典型的FILE
结构体可能包含以下信息(注意,这不是标准定义,仅供参考,不同编译器的实现可能不同,但大同小异):
typedef struct _iobuf {
char* _ptr; // 当前缓冲区的位置指针
int _cnt; // 缓冲区中剩余的字符数
char* _base; // 指向缓冲区基址的指针
int _flag; // 文件状态标志
int _file; // 文件描述符
int _charbuf; // 一个单独的字符(用于ungetc)
int _bufsiz; // 缓冲区大小
char* _tmpfname; // 临时文件名
} FILE;
但比较新的编译器已经将这些信息封装了,对于用户来说是不可见的,对FILE使用转到定义可能会看到这样的定义:
typedef struct _iobuf
{
void* _Placeholder;
} FILE;
通常情况下,我们只要通过标准的文件操作函数来使用FILE
指针,就不需要关心它内部的具体实现细节。
2)创建一个文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就可以访问到该文件。也就是说,通过文件指针变量可以访问到与它关联的文件。
FILE* pf;
3)文件指针的作用
在打开文件后,会产生一个FILE
结构体变量,我们可以通过FILE*
指针变量,来维护这个文件信息区,也就是这个结构体变量,这个结构体变量有与文件相关联,所以可以通过这个文件指针来对文件来进行操作。
2、文件操作相关函数
在打开文件后,使用文件,在使用文件完成后,要将文件关闭。在C语言中,打开文件的函数是fopen,关闭文件的函数是fclose。
1)打开文件fopen
在使用fopen函数打开一个文件成功后,fopen函数会返回一个文件指针,通过这个文件指针我们就得到了对文件的引用,就可以对文件进行操作了。该函数声明在 <stdio.h>
头文件中。
函数原型:
函数参数:
filename
参数是一个指向字符串的指针,代表你想要打开的文件名。
mode
参数定义了文件被打开的模式,即文件是用来读取、写入、追加等。
模式字符串包含文件访问模式,可以是:
- "r" 读取:用于输入操作的打开文件。文件必须存在。
- "w" 写入:为输出操作创建一个空文件。如果一个同名文件已经存在,它的内容会被丢弃,文件被当作一个新的空文件处理。
- "a" 追加:在文件末尾打开文件进行输出。输出操作总是在文件的末尾写入数据,扩展文件。文件定位操作(fseek、fsetpos、rewind)会被忽略。如果文件不存在,则创建文件。
- "r+" 读取/更新:打开文件进行更新(输入和输出均可)。文件必须存在。
- "w+" 写入/更新:创建一个空文件,并将其打开以进行更新(输入和输出均可)。如果一个同名文件已经存在,它的内容会被丢弃,文件被当作一个新的空文件处理。
- "a+" 追加/更新:打开文件以进行更新(输入和输出均可),所有输出操作都在文件的末尾写入数据。文件定位操作(fseek、fsetpos、rewind)会影响下一个输入操作,但输出操作会将位置移回文件末尾。如果文件不存在,则创建文件。
以上模式指定文件作为文本文件打开。为了将文件作为二进制文件打开,在模式字符串中必须包含一个“b”字符。这个额外的“b”字符可以附加在字符串的末尾(从而形成以下复合模式:“rb”、“wb”、“ab”、“r+b”、“w+b”、“a+b”),或者插入到字母和“+”号之间的混合模式中(“rb+”、“wb+”、“ab+”)。
C2011新的C标准(不包括C++)增加了一个新的标准子指示符(“x”),可以附加到任何“w”指定符上(形成“wx”、“wbx”、“w+x”或“w+bx”/“wb+x”)。这个子指示符强制函数在文件存在时失败,而不是覆盖它。
如果序列后跟随其他字符,行为取决于库的实现:一些实现可能会忽略额外的字符,所以例如额外的“t”(有时用来明确指出一个文本文件)被接受。
在某些库实现中,以更新模式打开或创建文本文件可能会将流视为二进制文件。
文本文件包含文本行序列。根据应用程序运行的环境,文本模式下的输入/输出操作可能会发生一些特殊字符转换,以适应系统特定的文本文件格式。尽管在某些环境中没有转换发生,文本文件和二进制文件被以同样方式对待,使用适当的模式可以提高可移植性。
对于打开以进行更新的文件(包括一个“+”号),允许进行输入和输出操作,在写操作之后的读操作之前,流应该被刷新(fflush)或重新定位(fseek、fsetpos、rewind)。在读操作之后的写操作之前,如果该操作没有到达文件末尾,流应该被重新定位(fseek、fsetpos、rewind)。
函数返回值:
如果文件成功打开,fopen
函数会返回一个指向 FILE
结构的指针。这个指针后续用于其他的文件操作函数。如果文件打开失败,例如mode为“r”时文件不存在,fopen
会返回 NULL
。
由于这里的fopen函数可能返回NULL指针,所以要在使用fopen函数之后对返回值进行检查:
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
//打开成功的操作
//...
2)关闭文件fclose
fclose
函数用于关闭一个已打开的文件。该函数也声明在 <stdio.h>
头文件中。
函数原型:
函数参数:
stream
参数是一个指向 FILE
结构的指针,代表了一个已打开的文件流。
函数返回值:
fclose
函数的作用是关闭文件流并释放所有相关资源。如果操作成功,函数返回0;如果失败,则返回EOF
一般在文件打开、操作完成后,都需要调用 fclose
函数来关闭文件。这是个良好的编程习惯,能够防止资源泄露等问题。
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
// 进行各种文件操作...
if (fclose(fp) != 0)
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
fp = NULL;
return 0;
}
三、对文件内容的操作
1、fputc
fputc
函数用于将一个字符写入到指定的文件流中。
函数原型:
函数参数:
char
是一个整数,它表示要写入的字符。尽管参数是 int
类型,但实际上只写入这个整数的最低有效字节(即一个字符)。
stream
是目标文件流的指针,之前必须已使用 fopen
等函数打开,并且文件需要以写入模式打开。
这里的文件流包含stdout。你可以使用 fputc
将一个字符写入到标准输出:
int ch = 'A';
fputc(ch, stdout); // 将字符 'A' 写入标准输出,通常是控制台
函数返回值:
fputc
函数返回写入的字符。如果发生错误,则返回 EOF
。
使用示例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
//写入a-z
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
可以发现a-z已经被写入文件中。
2、fgetc
fgetc
函数用于从指定的文件流中读取下一个字符。
函数原型:
函数参数:
stream
是源文件流的指针,之前必须已使用 fopen
等函数打开,并且文件需要以读取模式打开。
函数返回值:
fgetc
函数返回读取的字符。如果达到文件末尾或发生读取错误,则返回 EOF
。
使用示例:
如果一个文件中有a-z这26个字母:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");//mode应当是r读取模式
if (pf == NULL)//打开失败的情况
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
//读取文件内容
char ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
printf("\n");
fclose(pf);
pf = NULL;
return 0;
}
在这段代码中,while
循环会一直执行,直到 fgetc
返回 EOF
,即文件结束标志。每次循环迭代,fgetc
都从文件流 pf
读取下一个字符,存储到变量 ch
,然后 printf
打印该字符。当读取到文件末尾时,fgetc
返回 EOF
,循环终止。
运行结果:
3、fputs
fputs
函数用于把一个字符串写入到指定的文件流中。
函数原型:
函数参数:
str
是一个指向以空字符 '\0'
结尾的字符串的指针。
stream
是目标文件流的指针,之前必须已使用 fopen
等函数打开,并且文件需要以写入模式打开。
函数返回值:
fputs
函数返回一个非负数。如果发生错误,则返回 EOF
。
使用示例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)//打开失败的情况
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
//向文件中写入内容
fputs("Hello World!", pf);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
4、fgets
fgets
是一个在 C 标准库中提供的函数,它用于从文件流中读取字符串。fgets
函数的原型定义在 <stdio.h>
头文件中,并且它的行为是读取一行或者直到达到缓冲区大小的限制。
函数原型:
函数参数:
str
:指向一个元素类型为 char
的数组的首元素的指针,用于接收从文件流读取的字符串。
num
:指定要读取的最大字符数,包括最后的空字符(\0
)。fgets
会读取不超过 num-1
个字符,以确保总是有空间放置空字符。所以实际上fgets
读取的有效字符个数是 num-1
个。
stream
:指向 FILE
对象的指针,代表一个已打开的文件流,该文件流应以读模式打开。
函数返回值:
成功读取:当读取成功时,fgets
返回 str
指针。
文件结束或错误:如果读取到文件末尾或发生错误,将返回 NULL
。
行为细节:
fgets
会从当前文件流的位置读取字符,直到发生以下情况之一:
- 读取了
num-1
个字符。 - 读到了换行符(
\n
),换行符也会被读取并存储到str
指向的数组里。 - 到达了文件末尾。
无论在哪种情况下停止读取,fgets
都会在读取的字符串后添加一个空字符(\0
),以确保结果是一个合法的C字符串。
如果在读取任何字符之前就遇到了文件末尾,fgets
将返回 NULL
。如果在读取了至少一个字符后到达文件末尾或发生错误,fgets
仍然会返回 str
指针,并且缓冲区中的数据将是直到目前为止读取的字符。
使用示例:
文件中的内容是:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");//mode应当是r读取模式
if (pf == NULL)//打开失败的情况
{
printf("%s\n", strerror(errno));
return EXIT_FAILURE;
}
//向文件中读取内容
char str[20];
fgets(str, 6, pf);
printf("%s\n", str);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
标准错误报告函数perror
perror
是 C 语言中的标准错误报告函数,定义在 <stdio.h>
头文件中。它用于在标准错误输出 stderr
上打印一个描述性错误消息,通常用于报告系统调用失败的原因。这个错误消息是根据全局变量 errno
的当前值,来查找相应的错误描述。
全局变量 errno
是在 <errno.h>
头文件中定义的,它在发生系统调用和某些库函数调用失败时被设置为一个错误代码。每个错误代码对应着一个特定的错误情况。
函数原型:
函数参数:
string
:一个指向以空字符终止的字符串的指针。这个字符串通常包含了发生错误时正在尝试执行的操作的名称,它会被 perror
输出作为错误消息的前缀。如果 string
不是 NULL
,并且指向的字符串长度不是零,那么 perror
会在打印操作系统提供的错误消息前先打印这个前缀,后面跟着一个冒号和一个空格。如果 string
是 NULL
或指向的字符串长度为零,perror
只打印错误消息,不包括前缀。
函数行为:
当你调用 perror
时,它会查找 errno
当前值对应的错误描述字符串,并将其输出到标准错误输出 stderr
。输出格式通常是:string: error_description
,其中 string
是你传入的前缀字符串,而 error_description
是系统提供的错误描述。
使用示例:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("noexistfile.txt", "r");
if (pf == NULL)//打开失败的情况
{
perror("fopen");
return EXIT_FAILURE;
}
fclose(pf);
pf = NULL;
return 0;
}
由于在r模式下,文件必须存在,所以这里fopen函数会返回NULL指针。
运行结果:
5、fprintf
fprintf
函数用于向文件进行格式化输出。它的原型定义在stdio.h
头文件中。
函数原型:
函数参数:
FILE *stream
:是目标文件流的指针,之前必须已使用 fopen
等函数打开,并且文件需要以写入模式打开。
const char *format
:一个格式化字符串,就像printf
使用的那样,它规定了后续参数的输出格式。
...
:一个可变数量的参数,这些参数将按照format
字符串中的格式被输出到stream
指向的文件中。
函数返回值:
fprintf
函数会根据format
字符串格式化其后的参数,并将结果输出到stream
所指向的文件中。如果输出成功,它会返回写入的字符总数,出错则返回一个负数。
与printf比较:
printf
函数实际上是fprintf
函数的一个特例,其中fprintf
函数文件流stream
参数被固定为标准输出流stdout
就变成了printf
。
使用示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)//打开失败的情况
{
perror("fopen");
return EXIT_FAILURE;
}
//格式化写入文件内容
char* name[3] = { "Bob","Alice","Frank" };
for (int i = 1; i <= 3; i++)
{
fprintf(pf, "%03d %s\n", i, name[i - 1]);
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
6、fscanf
fscanf
函数用于从文件中进行格式化输入。它的原型也定义在stdio.h
头文件中。
函数原型:
函数参数:
FILE *stream
:是目标文件流的指针,之前必须已使用 fopen
等函数打开,并且文件需要以读取模式打开。
const char *format
:一个格式化字符串,规定了输入数据需要匹配的格式。
...
:一个可变数量的参数,提供了一系列指针,用以存放fscanf
从文件中读取并按照format
格式化后的数据。
函数返回值:
fscanf
函数会尝试从stream
所指向的文件中读取内容,并根据format
字符串解析内容,将解析出的数据存储在其后的指针指向的变量中。如果成功,函数返回成功匹配并赋值的输入项个数,读取失败或者到达文件结束返回EOF。
与scanf比较:
scanf
函数实际上是fscanf
函数的一个特例,其中fscanf
函数文件流stream
参数被固定为标准输出流stdoin
就变成了scanf
。
使用示例:
一个文件内容为:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Stu {
int id;
char name[10];
}Student;
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)//打开失败的情况
{
perror("fopen");
return EXIT_FAILURE;
}
//格式化读取文件内容
Student s = { 0,0 };
for (int i = 1; i <= 3; i++)
{
fscanf(pf, "%03d %s", &(s.id), s.name);
printf("%03d %s\n", s.id, s.name);
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
7、fwrite
fwrite
函数用于向文件流中写入数据块。
函数原型:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
函数参数:
const void *ptr
:指向要写入文件的数据块的指针。
size_t size
:每个元素的大小,以字节为单位。
size_t nmemb
:要写入的元素数量。
FILE *stream
:指向FILE
对象的指针,代表一个打开的文件流。
函数返回值:
fwrite
会从 ptr
指向的内存地址开始,向 stream
指向的文件写入 nmemb
个数据块,每个块的大小为 size
字节。函数返回成功写入的元素数量。如果返回的元素个数小于请求的数量,可能是因为发生了错误。
使用示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Stu {
int id;
char name[10];
}Student;
int main()
{
FILE* pf = fopen("test.bin", "wb");
if (pf == NULL)//打开失败的情况
{
perror("fopen");
return EXIT_FAILURE;
}
//二进制写入文件
Student student[2] = { {1,"Bob"},{2,"Alice"} };
fwrite(student, sizeof(Student), 2, pf);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
由于是使用二进制形式写入的,我们使用二进制编辑器打开文件查看:
8、fread
fread
函数用于从文件流中读取数据块。
函数原型:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
函数参数:
void *ptr
:指向一个内存块的指针,从文件读取的数据将被存储在这个内存块中。
size_t size
:每个元素的大小,以字节为单位。
size_t nmemb
:要读取的元素数量。
FILE *stream
:指向FILE
对象的指针,代表一个打开的文件流。
函数返回值:
fread
会尝试从 stream
指向的文件中读取 nmemb
个数据块,每个块的大小为 size
字节,保存到 ptr
指向的内存地址。函数返回成功读取的元素数量。如果返回的元素个数小于请求的数量,可能是遇到了文件结尾或发生了错误。
使用示例:
一个二进制文件的内容是:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Stu {
int id;
char name[10];
}Student;
int main()
{
FILE* pf = fopen("test.bin", "rb");
if (pf == NULL)//打开失败的情况
{
perror("fopen");
return EXIT_FAILURE;
}
//二进制读取文件内容
Student student[2] = { 0 };
fread(student, sizeof(Student), 2, pf);
for (int i = 0; i < 2; i++)
{
printf("%03d %s\n", student[i].id, student[i].name);
}
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
stdin、stdout和stderr
在前面我们提到了 stdin 和 stdout 也是文件流,对于 scanf 和 printf 就是使用 stdin 和 stdout 文件流,那为什么我们在使用 scanf 和 printf 时不需要打开这两个流呢(就像我们上面使用文件时要打开文件才能使用)?
实际上对于一个标准的C程序,在运行时会自动打开三个预定义的流,它们分别是标准输入流(stdin)、标准输出流(stdout)和标准错误流(stderr)。这三个流是在程序启动时由运行时环境自动打开的,因此程序员可以直接使用它们进行输入和输出操作,而无需手动开启。
stdin
是标准输入流,通常代表键盘输入或其他程序的输出。stdout
是标准输出流,通常代表屏幕输出。stderr
是标准错误流,它用于输出错误信息,即使标准输出被重定向到一个文件或者其他地方,标准错误通常也会显示在屏幕上。
在C语言中,这三个流被定义为 FILE
类型的指针,它们分别定义在 <stdio.h>
头文件中。
总结:
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
printf、fprintf、sprintf
1)sprintf介绍:
sprintf
函数用于将格式化的数据写入一个字符串中。它根据指定的格式将若干个变量的内容组合、格式化后,存放在第一个参数指定的字符串数组中。
函数原型:
函数参数:
buffer
: 要存储输出结果的字符串数组。
format
: 格式化字符串,指定输出的格式。
...
: 可变参数列表,包含了需要格式化的数据。
函数返回值:
返回写入数组的字符数(不包括终止的 null 字符)。
使用示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Stu {
int id;
char name[10];
}Student;
int main()
{
Student student[3] = { {1,"Bob"},{2,"Alice"},{3,"Frank"} };
char buffer[20] = { 0 };
//将数据格式化写入buffer中
for(int i = 0;i < 3;i++)
{
sprintf(buffer, "%03d %s\n", student[i].id, student[i].name);
//打印buffer中的字符串
printf("%s", buffer);
}
return 0;
}
运行结果:
函数用途:
sprintf
函数的功能是将格式化的数据写入到一个字符串缓冲区中,这在嵌入式编程中非常有用,因为它允许开发者创建具有特定格式的消息,然后可以将这些消息通过串口发送给其他设备,或者用于显示在屏幕上,或者记录到日志中。
例如,如果需要将温度读数和时间戳发送给电脑或其他设备,你可能会使用sprintf
来将这些信息格式化为一个字符串,然后通过串口发送。
char message[50];
int temperature = 25;
unsigned long timestamp = 1714656717;
sprintf(message, "Temperature: %dC Time: %lu", temperature, timestamp);
// 现在 message 包含了 "Temperature: 25C Time: 1714656717"
然而,需要注意的是,在嵌入式系统中使用 sprintf
或任何其他格式化函数时,必须确保目标缓冲区足够大,以避免溢出,这是一个常见的安全问题。此外,对于资源受限的嵌入式系统,snprintf
函数可能是一个更安全的选择,因为它允许指定缓冲区的最大大小,从而减少溢出的风险。
2)对比三者:
- printf 是针对标准输出流的格式化输出函数。
- fprintf 是针对所有输出流的格式化输出函数。
- sprintf 用于将格式化的数据写入一个字符串中。
scanf、fscanf、sscanf
1)sscanf介绍:
sscanf
函数用于从一个字符串中读取数据,根据指定的格式解析字符串,将数据存储在其余参数中提供的位置。
函数原型:
函数参数:
buffer
: 包含了要解析的数据的字符串。
format
: 格式化字符串,指定输入数据应当如何被解析。
...
: 可变参数列表,包括指向存储数据的变量的指针。
函数返回值:
返回成功匹配并赋值的输入项数目。
使用示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct Stu {
int id;
char name[10];
}Student;
int main()
{
char buffer[20] = "1 Bob";
Student student = { 0 };
//将buffer中的数据格式化读取到提供的参数中
sscanf(buffer, "%d %s", &(student.id), student.name);
printf("%03d %s\n", student.id, student.name);
return 0;
}
运行结果:
2)对比三者:
- scanf 是针对标准输入流的格式化输入函数。
- fscanf 是针对所有输入流的格式化输入函数。
- sscanf 用于将字符串解析成格式化的数据。
四、对文件的位置指针的操作(对文件的随机访问)
在以 r 或 w 等模式打开文件时,文件位置指针默认在文件最开头,在每次进行文件读取或写入后,会更新到最后读取或写入的位置后面。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
char ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
具体图解:
如果需要在文件中移动到不同的位置进行读写,我们就需要使用 fseek
, ftell
或 rewind
这些函数来手动操作文件位置指针。
1、ftell
ftell
函数用于获取当前在文件流中的读写位置。
函数原型:
函数参数:
stream
:指向 FILE
对象的指针,该对象代表一个打开的文件流。
函数返回值:
成功时,返回当前的位置,相对于文件开头的字节偏移。失败时,返回 -1L
并设置错误标志。
使用示例:
使用上面的例子,
这里的位置是2,相对于开头的位置是2。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
char ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
long position = ftell(pf);
if (position == -1)
{
perror("ftell");
fclose(pf);
return EXIT_FAILURE;
}
printf("Current position after reading: %ld\n", position);
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
2、fseek
fseek
函数用于移动文件流的读写位置到给定的位置。
函数原型:
函数参数:
stream
:指向 FILE
对象的指针,该 FILE
对象标识了流。
offset
:相对于 origin
的偏移量,以字节为单位。
origin
:这是一个起始点,决定从文件的哪里开始偏移。它通常有三个值:
SEEK_SET
:文件的开头。SEEK_CUR
:当前的读写位置。SEEK_END
:文件的末尾。
#define SEEK_CUR 1
#define SEEK_END 2
#define SEEK_SET 0
对于以二进制模式打开的流,新位置由 origin
位置加上偏移量(offset
)来定义。
在文本模式下打开文件时,移植性问题变得重要。因为文本模式下的文件可能会进行换行符的转换(如在Windows系统中,\n
可能会被转换成\r\n
),所以随机访问就变得复杂。
如果流以文本模式打开,偏移量(offset)
支持的值是零(适用于任何起点 origin
SEEK_SET、SEEK_CUR或SEEK_END)或者早先通过对同一文件关联的流调用ftell得到的返回值(只适用于SEEK_SET起点)。
如果此函数调用时使用了其他值作为这些参数,其支持依赖于特定的系统和库实现(可能没有报错或警告,但不具移植性)。
函数返回值:
成功时返回 0
。失败时返回非零值,并设置错误标志。
使用示例:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
//读取文件内容
char ch = 0;
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//保存当前的读取位置
long position = ftell(pf);
if (position == -1)
{
perror("ftell");
fclose(pf);
return EXIT_FAILURE;
}
//关闭文件
fclose(pf);
pf = NULL;
//再次打开文件
pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
//使用fseek定位到上次访问的位置
if(fseek(pf,position,SEEK_SET) != 0)
{
perror("fseek");
fclose(pf);
return EXIT_FAILURE;
}
//接着上次的位置继续读取文件内容
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
3、rewind
rewind
函数用于将文件流的位置指针重置到文件的开头,它相当于 fseek(stream, 0, SEEK_SET)
,但不返回值,并且清除错误标志。
“rewind” 意为“倒带”,就是指使影像制品的进程倒退到自己需要的区域的过程,一般是将磁带上的数据回滚至开始的位置。在计算机领域,"rewind" 一词被借用来描述一个类似的过程,即将文件的读写指针移回到文件的起始位置。
函数原型:
函数参数:
stream
:指向 FILE
对象的指针,标识需要操作的流。
使用示例:
一个文件的内容是:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return EXIT_FAILURE;
}
//读取数据
char ch = 0;
for (int i = 0; i < 6; i++)
{
ch = fgetc(pf);
printf("%c ", ch);
}
printf("\n");
//将位置重置为开头
rewind(pf);
//再次读取数据
for (int i = 0; i < 6; i++)
{
ch = fgetc(pf);
printf("%c ", ch);
}
printf("\n");
fclose(pf);
pf = NULL;
return 0;
}
运行结果:
五、通讯录改进
通过对文件的操作来将通讯录改进成为在程序退出后可以保存通讯录的内容。
contacts_list.c/cpp
#include "contacts_list.h"
void menu()
{
printf("*************************************\n");
printf("***** 1.add 2.del ****\n");
printf("***** 3.search 4.modify ****\n");
printf("***** 5.display 6.sort ****\n");
printf("***** 0.exit ****\n");
printf("*************************************\n");
printf("please choose a option>:");
}
int InitContact(Contacts_list* list)
{
list->capacity = DEFUALT_CONTACT;//容量设为DEFUALT_CONTACT
list->num = 0;//联系人个数设为0
list->dataPtr = (PeopleInfo*)malloc(DEFUALT_CONTACT * sizeof(PeopleInfo));//开辟DEFUALT_CONTACT个联系人大小的内存块
if (list->dataPtr == NULL)//分配失败
{
printf("InitContact:%s\n", strerror(errno));//抛出错误信息
return 1;
}
//分配成功
return 0;
}
int IncreaseCapacity(Contacts_list* list, int number)
{
PeopleInfo* temp = (PeopleInfo*)realloc(list->dataPtr, (list->capacity + number) * sizeof(PeopleInfo));
if (temp == NULL)//分配失败
{
printf("IncreaseCapacity:%s\n", strerror(errno));//抛出错误信息
return 1;
}
else//分配成功
{
list->dataPtr = temp;//更新内存块的指针
temp = NULL;//将临时指针置空
list->capacity += number;//更新容量
}
return 0;
}
void AddContact(Contacts_list* list)
{
//增容
if (list->num == list->capacity)
{
IncreaseCapacity(list, DEFUALT_ALLOC_CONTACT);//默认增容DEFUALT_ALLOC_CONTACT大小
}
//录入信息
printf("please type in name(maximum 20)>:");
scanf("%s", list->dataPtr[list->num].name);
printf("please type in gender(maximum 8)>:");
scanf("%s", list->dataPtr[list->num].gender);
printf("please type in age>:");
scanf("%d", &(list->dataPtr[list->num].age));
list->num++;
printf("AddContact successfully\n");
}
void DisplayContact(const Contacts_list* list)
{
int i = 0;
printf("------------------------------------------------\n");
printf("%-20s%-8s%-s\n", "name", "gender", "age");
for (i = 0; i < list->num; i++)
{
printf("%-20s%-8s%d\n", list->dataPtr[i].name, list->dataPtr[i].gender, list->dataPtr[i].age);
}
printf("------------------------------------------------\n");
printf("display successfully\n");
}
void DestroyContact(Contacts_list* list)
{
free(list->dataPtr);//释放开辟的空间
list->dataPtr = NULL;//将指向内存块指针置空
}
void DeleteContact(Contacts_list* list)
{
char name[21];
if (list->num == 0)
{
printf("there is no contact can be deleted\n");//联系人为零,无需删除
return;
}
printf("please type in the name who you want to delete>:");
scanf("%s", name);
int res = SearchContact(list, name);
if (res != -1)
{
int i = 0;
for (i = res; i < list->num - 1; i++)
{
list->dataPtr[i] = list->dataPtr[i + 1];
}
list->num--;
printf("delete successfully\n");
}
else
{
printf("cannot find\n");//没找到要删除的联系人
return;
}
}
int SearchContact(Contacts_list* list, const char* string)
{
int i = 0;
for (i = 0; i < list->num; i++)
{
if (strcmp(list->dataPtr[i].name, string) == 0)
{
return i;//查找到返回下标
}
}
return -1;//未查找到返回-1
}
void ModifyContact(Contacts_list* list, const char* string)
{
int res = SearchContact(list, string);
if (res == -1)
{
printf("cannot find\n");
return;
}
printf("please type in name(maximum 20)>:");
scanf("%s", list->dataPtr[res].name);
printf("please type in gender(maximum 8)>:");
scanf("%s", list->dataPtr[res].gender);
printf("please type in age>:");
scanf("%d", &(list->dataPtr[res].age));
printf("modification successfully\n");
}
int cmp_by_name(const void* element1, const void* element2)
{
PeopleInfo* ele1 = (PeopleInfo*)element1;//强制转换为联系人结构体类型,方便访问name变量
PeopleInfo* ele2 = (PeopleInfo*)element2;
return strcmp(ele1->name, ele2->name);
}
int cmp_by_age(const void* element1, const void* element2)
{
int res = ((PeopleInfo*)element1)->age - ((PeopleInfo*)element2)->age;
return ((-res < 0) - (res < 0));
}
void SortContact(Contacts_list* list, int num)
{
switch (num)
{
case 1:
qsort(list->dataPtr, list->num, sizeof(PeopleInfo), cmp_by_name);
printf("sort successfully\n");
break;
case 2:
qsort(list->dataPtr, list->num, sizeof(PeopleInfo), cmp_by_age);
printf("sort successfully\n");
break;
default:
printf("error\n");
break;
}
}
void update_contact_file(Contacts_list* list)
{
FILE* contact_file = fopen("contact_data.txt", "w");
if (contact_file == NULL)
{
perror("FUNC(update_contact_file)");
return;
}
for(int i = 0;i < list->num;i++)
{
fprintf(contact_file, "%s %s %d\n", list->dataPtr[i].name, list->dataPtr[i].gender, list->dataPtr[i].age);
}
fclose(contact_file);
contact_file = NULL;
}
void load_contact_from_file(Contacts_list* list)
{
FILE* contact_file = fopen("contact_data.txt", "r");
if (contact_file == NULL)
{
perror("FUNC(load_contact_from_file)");
return;
}
int i = 0;
while (fscanf(contact_file, "%s %s %d", list->dataPtr[i].name, list->dataPtr[i].gender, &(list->dataPtr[i].age)) != EOF)
{
list->num++;
if (list->num == list->capacity)
{
IncreaseCapacity(list, DEFUALT_ALLOC_CONTACT);
}
i++;
}
fclose(contact_file);
contact_file = NULL;
}
contacts_list.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//默认联系人个数
#define DEFUALT_CONTACT 3
//联系人空间不够时,默认分配空间个数
#define DEFUALT_ALLOC_CONTACT 3
//打印菜单函数
void menu();
//声明联系人信息结构体变量
typedef struct PeopleInfo
{
char name[21];
char gender[9];
int age;
}PeopleInfo;
//声明通讯录结构体变量
typedef struct Contacts_list
{
PeopleInfo* dataPtr;//存放联系人数据,指向内存块的指针
int num;//记录通讯录联系人个数
int capacity;//记录通讯录容量
}Contacts_list;
//初始化函数,默认初始化DEFUALT_CONTACT个联系人的空间,分配成功返回0,失败返回1
int InitContact(Contacts_list* list);
//增容函数,增加number个大小,成功返回0,失败返回1
int IncreaseCapacity(Contacts_list* list, int number);
//添加联系人,如果空间不足,默认再开辟DEFUALT_ALLOC_CONTACT个联系人的空间,分配成功返回0,失败返回1
void AddContact(Contacts_list* list);
//摧毁通讯录,释放开辟的空间
void DestroyContact(Contacts_list* list);
//显示通讯录
void DisplayContact(const Contacts_list* list);
//删除联系人
void DeleteContact(Contacts_list* list);
//查找联系人
int SearchContact(Contacts_list* list, const char* string);
//修改联系人
void ModifyContact(Contacts_list* list, const char* string);
//排序联系人
void SortContact(Contacts_list* list, int num);
//按名字排序
int cmp_by_name(const void* element1, const void* element2);
//按年龄排序
int cmp_by_age(const void* element1, const void* element2);
//更新通讯录文件内容
void update_contact_file(Contacts_list* list);
//从通讯录文件中读取数据
void load_contact_from_file(Contacts_list* list);
test.c/cpp
#include "contacts_list.h"
int main()
{
int status = 0;
Contacts_list contacts_list;//通讯录结构体变量
if (InitContact(&contacts_list))//初始化失败会返回1,如果初始化失败则程序以1返回
{
printf("Initialization Error\n");
return 1;
}
load_contact_from_file(&contacts_list);//从文件中加载通讯录数据
int res = 0;
int num = 0;
do
{
menu();//打印菜单
scanf("%d", &status);
system("cls");
switch (status)
{
case 1:
AddContact(&contacts_list);
break;
case 2:
DeleteContact(&contacts_list);
break;
case 3:
char name1[21];
printf("please type in the name>:");
scanf("%s", name1);
res = SearchContact(&contacts_list, name1);
if (res != -1)
{
printf("%d\n", res);
}
else
{
printf("cannot find\n");
}
break;
case 4:
char name2[21];
printf("please type in the name>:");
scanf("%s", name2);
ModifyContact(&contacts_list, name2);
break;
case 5:
DisplayContact(&contacts_list);
break;
case 6:
printf("1.sort by name\n");
printf("2.sort by age\n");
scanf("%d", &num);
SortContact(&contacts_list, num);
break;
case 0:
update_contact_file(&contacts_list);//将通讯录数据保存到文件中
DestroyContact(&contacts_list);//释放空间,摧毁通讯录
printf("exit\n");
break;
default:
printf("error\n");
break;
}
} while (status);
return 0;
}