C语言——文件相关操作

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 会从当前文件流的位置读取字符,直到发生以下情况之一:

  1. 读取了 num-1 个字符。
  2. 读到了换行符(\n),换行符也会被读取并存储到 str 指向的数组里。
  3. 到达了文件末尾。

无论在哪种情况下停止读取,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, ftellrewind 这些函数来手动操作文件位置指针。

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_SETSEEK_CURSEEK_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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/593414.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

XBoot:基于Spring Boot 2.x的一站式前后端分离快速开发平台

XBoot&#xff1a;基于Spring Boot 2.x的一站式前后端分离快速开发平台 摘要 随着信息技术的迅速发展&#xff0c;快速构建高质量、高可靠性的企业级应用成为了迫切需求。XBoot&#xff0c;作为一个基于Spring Boot 2.x的一站式前后端分离快速开发平台&#xff0c;通过整合微信…

AI-数学-高中56-成对数据统计-线性回归方程

原作者视频&#xff1a;【成对数据统计】【一数辞典】1线性回归方程_哔哩哔哩_bilibili 注意&#xff1a;高中只学线性回归。 最小二乘法&#xff08;残差和平方最小的直线、方差最小>拟合程度最好&#xff09;&#xff1a;

滑动验证码登陆测试编程示例

一、背景及原理 处理登录时的滑动验证码有两个难点&#xff0c;第一个是找到滑块需要移动的距离&#xff0c;第二个是模拟人手工拖动的轨迹。模拟轨迹在要求不是很严的情况下可以用先加速再减速拖动的方法&#xff0c;即路程的前半段加速度为正值&#xff0c;后半段为负值去模…

微搭低代码入门03页面管理

目录 1 创建页面2 页面布局3 页面跳转总结 上一篇我们介绍了应用的基本操作&#xff0c;掌握了应用的概念后接着我们需要掌握页面的常见操作。 1 创建页面 打开应用的编辑器&#xff0c;在顶部导航条点击创建页面图标 在创建页面的时候可以从空白新建&#xff0c;也可以使用模…

docker-本地私有仓库、harbor私有仓库部署与管理

一、本地私有仓库&#xff1a; 1、本地私有仓库简介&#xff1a; docker本地仓库&#xff0c;存放镜像&#xff0c;本地的机器上传和下载&#xff0c;pull/push。 使用私有仓库有许多优点&#xff1a; 节省网络带宽&#xff0c;针对于每个镜像不用每个人都去中央仓库上面去下…

JavaEE >> Spring Boot 日志

日志的作用以及什么是日志 日志就是为了当程序出错的时候&#xff0c;程序员们可以通过日志看到是哪部分出现错误了&#xff0c;为了发现和定位问题。当然&#xff0c;我们还可以通过日志实现一些功能&#xff0c;如下&#xff1a; 记录系统的操作⽇志&#xff0c;⽅便数据恢…

CSS探索之旅:定位

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文我们详细介绍 css中定位的相关知识点 定位的用处 先简单认识一下定位是做什么的。 其实&#xff0c;定位的功能就像他的名字一样&#xff0c;可以规定显示在网页的一个位置。 其他布局的效果 我们之前默认…

Java面试——不安全的集合类

​ 系统性学习&#xff0c;移步IT-BLOG-CN Java 中有许多的集合&#xff0c;常用的有List&#xff0c;Set&#xff0c;Queue&#xff0c;Map。 其中 List&#xff0c;Set&#xff0c;Queue都是Collection&#xff08;集合&#xff09;&#xff0c;List中<>的内容表示其中…

基于Pytorch深度学习——卷积神经网络(卷积层/池化层/多输入多输出通道/填充和步幅/)

本文章来源于对李沐动手深度学习代码以及原理的理解&#xff0c;并且由于李沐老师的代码能力很强&#xff0c;以及视频中讲解代码的部分较少&#xff0c;所以这里将代码进行尽量逐行详细解释 并且由于pytorch的语法有些小伙伴可能并不熟悉&#xff0c;所以我们会采用逐行解释小…

Java 笔记 15:Java 数组相关内容补充,多维数组,Arrays 类的常见用法,以及冒泡排序

一、前言 记录时间 [2024-05-05] 系列文章简摘&#xff1a; Java 笔记 01&#xff1a;Java 概述&#xff0c;MarkDown 常用语法整理 Java 笔记 02&#xff1a;Java 开发环境的搭建&#xff0c;IDEA / Notepad / JDK 安装及环境配置&#xff0c;编写第一个 Java 程序 Java 笔记 …

【在线OJ】Vue在线OJ项目

一、主页 二、题库 三、在线编译器 四、比赛 五、搜索 六、个人主页

【区块链】比特币架构

比特币架构 2009年1月&#xff0c;在比特币系统论文发表两个月之后&#xff0c;比特币系统正式运行并开放了源码&#xff0c;标志着比特币网络的正式诞生。通过其构建的一个公开透明、去中心化、防篡改的账本系统&#xff0c;比特币开展了一场规模空前的加密数字货币体验。在区…

vue3(实现上下无限来往滚动)

一、问题描述 一般在大屏项目中&#xff0c;很常见的效果&#xff0c;就是容器中的内容缓慢地向下移动&#xff0c;直到底部停止&#xff0c;然后快速滚动回顶部&#xff0c;然后接着缓慢滚动到底部。并且在特定的情况下&#xff0c;还需要进行一些小交互&#xff0c;那就还得让…

RabbitMQ之生产批量发送

为什么要用生产批量发送&#xff1f; 批量发送消息&#xff0c;可以提高MQ发送性能。但是 RabbitMQ 并没有提供了批量发送消息的 API 接口,使用 spring-amqp 的 BatchingRabbitTemplate 实现批量能力。 SimpleBatchingStrategy 发送策略满足以下规则会进行发送&#xff1a; ba…

FreeRTOS低功耗模式(1-19)

低功耗模式简介(了解) 很多应用场合对于功耗的要求很严格&#xff0c;比如可穿戴低功耗产品、物联网低功耗产品等 一般MCU都有相应的低功耗模式,裸机开发时可以使用MCU的低功耗模式。 FreeRTOS也提供了一个叫Tickless的低功耗模式,方便带FreeRTOS操作系统的应用开发 stm32的低…

C#创建obj三维模型文件

介绍 使用开源库创建obj三维模型文件。 开源库地址&#xff1a;https://github.com/JeremyAnsel/JeremyAnsel.Media.WavefrontObj 相关API地址&#xff1a;https://jeremyansel.github.io/JeremyAnsel.Media.WavefrontObj/api/JeremyAnsel.Media.WavefrontObj.ObjFile.html …

docker desktop实战部署oracle篇

1、前言 oracle数据库官方已提供现成的镜像&#xff0c;可以直接拿来部署了。 由于项目中需要使用oracle数据库的分表功能&#xff0c;之前安装的是standard版本&#xff0c;无奈只能重新安装。网上查了一番&#xff0c;使用的方法都比较传统老旧&#xff1a;下载安装包手动安…

多线程局部存储技术

问题 多线程上下文中&#xff0c;每个线程需要使用一个专属的全局变量&#xff0c;该如何实现&#xff1f; 代码示例 一种可能的解决方案 test1.c #define _GNU_SOURCE /* To get pthread_getattr_np() declaration */ #define _XOPEN_SOURCE > 500 || _POSIX_C_SOURC…

谷歌上架,为什么会触发填表单,可以避免吗?怎么填表单可以提高通过率?

在谷歌上架过程中&#xff0c;相信大部分开发者都有收到过谷歌发来表单填写的邮件通知&#xff0c;要求开发者们在14天内根据表单要求回复关于应用部分情况。邮件如图&#xff1a; 根据触发填表单的开发者分享的经验来看&#xff0c;填完表之后出现的情况不尽相同&#xff0c;且…

【华为】路由综合实验(OSPF+BGP基础)

【华为】路由综合实验 实验需求拓扑配置AR1AR2AR3AR4AR5PC1PC2 查看通信OSPF邻居OSPF路由表 BGPBGP邻居BGP 路由表 配置文档 实验需求 ① 自行规划IP地址 ② 在区域1里面 启用OSPF ③ 在区域1和区域2 启用BGP&#xff0c;使AR4和AR3成为eBGP&#xff0c;AR4和AR5成为iBGP对等体…