[C语言]指针进阶详解

指针是C语言的精髓所以内容可能会比较多,需要我们认真学习


目录

1、字符指针

2、指针数组

3、数组指针

3.1数组指针的定义

3.2&数组名vs数组名

3.3数组指针的使用 

4、数组传参和指针传参

4.1一维数组传参

4.2二维数组传参

4.3一级指针传参

4.4二级指针传参

5、函数指针

6、函数指针数组

7、指向函数指针数组的指针

8、回调函数

8.1回调函数定义

8.2qsort库函数的用法

1、字符指针

在指针的类型中我们知道一种指针类型为字符指针char*

一般使用:

int main()

{

  char ch='w';

  char* pc=&ch;//*表示pc是指针,char表示pc指向的对象ch他的类型是char

  return 0;

}

#include<stdio.h>
int main()
{char* pc="abcdef";printf("%s\n",pc);return 0;
}

看这样一个代码的运行结果:

 这里需要注意pc里面放的并不是整个字符串,而是a的地址,非要放在pc(4字节)中是放不下的,而打印的是字符串(%s),我们只要知道首字符a的地址就能向后访问得到整个字符串的地址在char* 前面加上const会更加好一些。看这样一道题:

#include<stdio.h>
int main()
{const char* p1 = "abcdef";const char* p2 = "abcdef";char arr1[] = "abcdef";char arr2[] = "abcdef";if (p1 == p2)printf("p1==p2\n");elseprintf("p1!=p2");if (arr1 == arr2)printf("arr1==arr2");elseprintf("arr1!=arr2");return 0;
}

 这说明p1和p2指向同一个字符a,字符串abcdef是常量字符串,放在内存中的只读内存区,不能改变它的值,没有必要存在多份,只在内存中存一份,所以p1和p2都指向a的地址,p1就等于p2.而arr1和arr2不相等,是因为arr1[ ]和arr2[ ]是两个独立的数组,,每一个都在内存中开辟了一份独立的空间,所以地址肯定是不相同的。

2、指针数组

顾名思义指针数组就是存放指针的数组(本质是一个数组)

int* arr1[10];//整型指针的数组

char* arr2[10];//一级字符指针的数组

char **arr[5];//二级字符指针的数组 

#include<stdio.h>
int main()
{int arr1[] = { 1,2,3,4 };int arr2[] = { 2,3,4,5 };int arr3[] = { 3,4,5,6 };int* arr[3] = { arr1,arr2,arr3 };//每个数组的首元素地址,每个元素的类型是int*int i = 0;//代表arr的每个元素的下标for (i = 0; i < 3; i++){int j = 0;//j代表的是arr1、arr2、arr3中每个元素的下标for (j = 0; j < 4; j++){printf("%d ", *(arr[i] + j));//arr[i]代表各个数组的数组名,即每个数组的首元素地址,加上j并解引用就拿到了所有元素//因为*(p+i)等价于arr[i],所以有可以写成这样的形式//printf("%d ",*(*(arr+i)+j));或者//printf("%d ",arr[i][j]);}printf("\n");}return 0;
}

用了一个指针数组把三个一维数组关联起来了,模拟实现二维数组,但本质上并不是二维数组,因为这三个数组在内存中并不是连续存放的但二维数组在内存中是连续存放的。

3、数组指针

3.1数组指针的定义

数组指针的本质是指针。

整型指针:int* p;能够指向整形数据的指针

浮点型指针:float* p;能够指向浮点型数据的指针

所以说数组指针就是能够指向数组的指针。比如:

int (*p)[10];(*p)代表p是指针,指向的是整型数组中的10个元素,每个元素是int类型。

注意:[ ]的优先级要高于*号,因此必须加上()来保证p先和*相结合。

3.2&数组名vs数组名

对于下面的数组:

int arr[10];

arr和&arr的区别:

我们知道arr是数组名,数组名表示数组首元素的地址。那么&arr表示的是什么?

#include<stdio.h>
int main()
{int arr[10] = { 0 };printf("%p\n", arr);printf("%p\n", &arr[0]);printf("%p\n", &arr);return 0;
}
//数组名通常表示的是数组首元素的地址,但有两个例外:
//1.sizeof(数组名),这里的数组名表示的是整个数组
//2.&数组名,这里的数组名依然表示的是整个数组,所以&arr取出的是整个数组的地址

这里我们会发现运行结果显示,三个地址是完全相同的,但是&arr的步长和arr的步长是不同的,arr+1(类型是int*)跳过4个字节,但&arr+1跳过28(16进制)字节,即40字节。那么应该怎么存放整个数组的地址呢?

存放数组首元素的地址:int *p=arr;

存放整个数组的地址:int (*p)[10];(数组指针用来存放整个数组的地址)。它的类型为int (*)[10]。

3.3数组指针的使用 

#include<stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int(*p)[10] = &arr;int sz = sizeof(arr) / sizeof(arr[0]);int i = 0;for (i = 0; i < sz; i++){printf("%d ", *(*p + i));//因为p指向的是整个数组,(不包含数组大小),*p表示的是整个数组的内容(数组名)即首元素地址//也可以这样说:*p表示从p所指向的内存地址开始的数组内存的布局。这通常被理解为该数组本身,实际上它只是一个别名或引用,指向已经存在的数组//简单理解就是如果int* p=arr,那*p就是找到arr的内容,如果int (*p)arr[10]=&arr,则*p就是找到*&arr即arr(数组名)}return 0;
}

这样用我们会感到相当别扭,很不舒服,所以很不建议这样用! 建议大家用在二维数组上。

#include<stdio.h>
void print1(int arr[3][5], int r, int c)
{int i = 0;for (i = 0; i < r; i++){int j = 0;for (j = 0; j < c; j++){printf("%d ",arr[i][j]);}printf("\n");}
}
void print2(int (*p)[5], int r, int c)
{int i = 0;for (i = 0; i < r; i++){int j = 0;for (j = 0; j < c; j++){printf("%d ",*(*(p+i)+j));//p相当于是第一行的地址,解引用相当于首元素地址}printf("\n");}
}
int main()
{int arr[3][5] = { 1,2,3,4,5,2,3,4,5,6,3,4,5,6,7 };print1(arr, 3, 5);print2(arr, 3, 5);//传过去的是二维数组的首元素地址,那二维数组的首元素地址是什么呢?//其实是二维数组的第一行,所以传过去的其实是数组内容为5个整型的一维数组的地址return 0;
}

int (*parr[10])[5];parr先和[5]配对说明是个数组数组的类型是int (*)[5]是个数组指针。所以parr就是存放数组指针的数组 。

4、数组传参和指针传参

写代码时难免会把数组或指针传给函数,那么函数的参数该如何设计呢?

4.1一维数组传参

#include<stdio.h>
void test1(int arr1[]) {

}
void test1(int arr1[10]) {

}
void test1(int* arr1) {

}
void test2(int *arr2[10]) { 

} //10可以省略
void test2(int** arr2) {

}
int main()
{
    int arr1[10] = { 0 };
    int* arr2[10] = { 0 };
    test1(arr1);
    test2(arr2);
    return 0;
}

4.2二维数组传参

#include<stdio.h>
void test(int arr[3][5]) {

}
void test(int arr[][5]) {

}//二维数组传参,函数的形参设计只能省略行,因为对于一个二维数组,可以不知道有多少行,但是必须知道一行有多少个元素
void test(int (*p)[5]) {

}//二维数组传参传的是第一行元素的地址,需要拿一个数组指针才能接收
int main()
{
    int arr[3][5] = {0};
    test(arr);
    return 0;
}

4.3一级指针传参

#include<stdio.h>
void print(int* p, int sz)
{
    int i = 0;
    for (i = 0; i < sz; i++)
    {
        printf("%d ", *(p + i));
    }
}
int main()
{
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    int* p = arr;
    int sz = sizeof(arr) / sizeof(arr[0]);
    print(p, sz);//p是一级指针变量
    return 0;
}

4.4二级指针传参

#include<stdio.h>
void print(int** ptr)
{
    printf("%d\n", **ptr);
}
int main()
{
    int n = 10;
    int* p = &n;
    int** pp = &p;
    print(&p);
    return 0;
}

当函数的参数为二级指针的时候,可以接收什么参数?

可以说指向一级指针的变量,也可以是指向一级指针的数组,二级指针变量本身

5、函数指针

指向函数的指针就叫做函数指针,用来存放函数的地址

#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{printf("%p\n", &Add);return 0;
}

 由此可见打印出来的值就是Add函数的地址。对于函数来说,&函数名和函数名都是函数的地址

那怎么用指针把函数地址存起来呢?

拿此例子来说:int (*pf)(int,int)=&Add;

#include<stdio.h>
int Add(int x, int y)
{return x + y;
}
int main()
{int (*pf)(int, int) = &Add;int ret = (*pf)(2, 3);//解引用就相当于找到了这个函数,*是可以省略的printf("%d\n", ret);return 0;
}

 int main()
{
    (*(void(*) () )0)();//把0强制转换成函数指针类型(我们可以认为0就是一个地址),解引用调用这个函数,但什么参数都没有传,所以本质上是一次函数调用,调用的是0作为地址处的函数

    void(* signal(int,void(*)(int) ) )(int);

    //signal与括号先结合是函数名,其中void(*)(int)是函数指针类型,去掉

    signal(int,void(*)(int) )我们会发现剩下部分也是返回也是一个函数返回类型。也就是说这个代码是一次函数声明。声明的signal函数第一个参数的类型是int,第二个参数的类型是函数指针。该函数指针指向的函数参数是int,返回类型是void;而signal函数的返回类型也是一个函数指针,该函数指针指向的参数是int,返回类型也是void。
    return 0;
}

函数指针的用途:(写一个计算器能够实现简单的加法、减法、除法、乘法)

#include<stdio.h>
void menu()
{printf("*******************\n");printf("****1.add 2.sub****\n");printf("****3.mul 4.div****\n");printf("****   0.exit  ****\n");printf("*******************\n");
}
int add(int x, int y)
{return x + y;
}
int sub(int x, int y)
{return x - y;
}
int mul(int x, int y)
{return x * y;
}
int div(int x, int y)
{return x / y;
}
//回调函数
void calc(int (*p)(int, int))
{int x = 0;int y = 0;int ret = 0;printf("请输入两个操作数:>\n");scanf("%d%d", &x, &y);ret = (*p)(x, y);printf("%d\n", ret);
}
int main()
{int input = 0;do{menu();printf("请选择:>");scanf("%d", &input);switch (input){case 1:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出程序\n");break;default:printf("选择错误请重新选择:>\n");break;}} while (input);return 0;
}

6、函数指针数组

把存放函数的地址存到一个数组中,那么这个数组就叫做函数指针数组,如何定义函数指针数组?

int add(int x, int y)
{
    return x + y;
}
int sub(int x, int y)
{
    return x - y;
}
int mul(int x, int y)
{
    return x * y;
}
int div(int x, int y)
{
    return x / y;
}

int main( )

{

    int (*arr[4])(int,int)={add,sub,mul,div};//arr先于[4]结合表明本质是个数组,数组每个元素的类型为int (*)(int,int)即函数指针类型

    int i = 0;
   for (i = 0; i < 4; i++)
   {
      int ret = arr[i](8, 4);
      printf("%d ", ret);
    }

    return 0;

}

有什么用途呢?以上面模拟计算机为例,我们还可以对代码进行简化,并且增加计算机功能时也相对比较容易,极大简化了修改功能时的工作量。

#include<stdio.h>
void menu()
{printf("*******************\n");printf("****1.add 2.sub****\n");printf("****3.mul 4.div****\n");printf("****   0.exit  ****\n");printf("*******************\n");
}
int add(int x, int y)
{return x + y;
}
int sub(int x, int y)
{return x - y;
}
int mul(int x, int y)
{return x * y;
}
int div(int x, int y)
{return x / y;
}
int main()
{int x = 0;int y = 0;int input = 0;int ret = 0;//转移表int (*pfarr[5])(int, int) = { 0,add,sub,mul,div };//之所以第一个元素放0,是为了,与为了调用时和菜单选项对应起来,比如输入下标为1是调用的是add函数do{menu();printf("请选择:>");scanf("%d", &input);if (input == 0){printf("退出程序\n");}else if (input >= 1 && input <= 4){printf("请输入两个操作数:>\n");scanf("%d%d", &x, &y);ret = pfarr[input](x, y);printf("%d\n", ret);}else{printf("选择错误请重新选择\n");}} while (input);return 0;
}

这个代码现在就变得清爽了许多,非常的简洁。 

7、指向函数指针数组的指针

指向函数指针数组的指针本质上就是一个指针,指针指向一个数组,数组里面的元素都是函数指针

int main()
{
    int (*pfarr[5])(int, int) = { 0,add,sub,mul,div };
    int (*(*ppfarr)[5])(int,int) = &pfarr;
    return 0;
}

但用的相对并不多,以后会详细介绍 

8、回调函数

8.1回调函数定义

回调函数就是通过函数指针调用的函数。如果把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方式直接调用,而是在特定的事件或条件发生时有另外的一方的调用的,用于对该事件或条件进行响应。

8.2qsort库函数的用法

qsort是C标准库<stdlib.h>中的一个函数,用于对数组进行快速排序。它接受一个指向要排序的数组的指针、数组中元素的数量、每个元素的大小(字节为单位)以及一个比较函数作为参数。

//qsort的声明

void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));

base:指向要排序的数组的第一个元素的指针(数据的起始位置)

nmemb:数组中元素的数量

size:数组中每个元素的大小

compar:一个指向比较函数的指针,该函数用于确定元素的排序顺序 

比较函数应该接受两个只想要比较的元素的指针,并返回一个整型,表示他们的相对顺序。如果第一个元素排在第二个元素之前返回负数(第一个数小于第二个数),反之返回整数,相等则返回0.

qsort具体要怎么用呢?现在我们对整型数组arr[10]={10,9,8,7,6,5,4,3,2,1}排成升序之前我们学过冒泡排序的思想,现在我们换种方法来实现

void print(int arr[],int sz)//打印数组内容
{int i = 0;for (i = 0;i < sz;i++){printf("%d ", arr[i]);}printf("\n");
}
//比较两个整型元素,e1指向一个整数,e2也指向一个整数
int cmp_int(const void* e1, const void* e2)
{return (*(int*)e1 - *(int*)e2);//强制类型转换成int*类型
}//如果我们像排成降序只需要把e1改成e2,把e2改成e1,逻辑相反
int main()
{int arr[10] = { 10,9,8,7,6,5,4,3,2,1 };int sz = sizeof(arr) / sizeof(arr[0]);qsort(arr, sz, sizeof(arr[0]), cmp_int);print(arr, sz);return 0;
}
//void* 是无具体类型的指针,不能解引用操作,也不能加减整数

qsort不仅仅可以排序整型数据也可以排结构体数据,也可以排字符数据

我们来看对结构体数据排序的实例:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
struct Stu
{char name[20];int age;};
int cmp_stu_by_name(const void* e1, const void* e2)
{return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);//e1是指针变量//strcmp函数如果第一个字符串比第二个大返回正数,小于返回负数,否则返回0
}
int main()
{struct Stu s[] = { {"张三",15},{"李四",16},{"王五",18}};int sz = sizeof(s) / sizeof(s[0]);qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);int i = 0;for (i = 0; i < sz; i++){printf("name=%s age=%d\n",(s+i)->name,(s+i)->age);//s虽然本身不是指针,是数组名,但会"退化为"指向第一个元素的指针。这是为了与期望接受指针的函数(qsort)兼容//也可以用s[i].name,s[i].age的形式来访问结构体成员}return 0;
}
}

 

 

 

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学习如何使用PyQt5实现notebook功能

百度搜索“pyqt5中notebook控件”&#xff0c;AI自动生成相应例子的代码。在 PyQt5 中&#xff0c;QTabWidget 类被用作 Notebook 控件。以下是一个简单的示例&#xff0c;展示如何创建一个带有两个标签的 Notebook 控件&#xff0c;并在每个标签中放置一些文本。 import sys f…

45. UE5 RPG 增加角色受击反馈

在前面的文章中&#xff0c;我们实现了对敌人的属性的初始化&#xff0c;现在敌人也拥有的自己的属性值&#xff0c;技能击中敌人后&#xff0c;也能够实现血量的减少。 现在还需要的就是在技能击中敌人后&#xff0c;需要敌人进行一些击中反馈&#xff0c;比如敌人被技能击中后…

使用macof发起MAC地址泛洪攻击

使用macof发起MAC地址泛洪攻击 MAC地址泛洪攻击原理&#xff1a; MAC地址泛洪攻击是一种针对交换机的攻击方式&#xff0c;目的是监听同一局域网中用户的通信数据。交换机的工作核心&#xff1a;端口- MAC地址映射表。这张表记录了交换机每个端口和与之相连的主机MAC地址之间…

Spring Boot与JSP的浪漫邂逅:轻松构建动态Web应用的秘诀

本文介绍 Spring Boot 集成 JSP。 1、pom.xml 增加对 JSP 的支持 Spring Boot 的默认视图支持是 Thymeleaf 模板引擎&#xff0c;如果想要使用 JSP 页面&#xff0c;需要配置 servlet 依赖和 tomcat 的支持。 在 pom.xml 文件中增加如下代码&#xff1a; <!-- servlet依赖 -…

(六)SQL系列练习题(下)#CDA学习打卡

目录 三. 查询信息 16&#xff09;检索"1"课程分数小于60&#xff0c;按分数降序排列的学生信息​ 17&#xff09;*按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩 18&#xff09;*查询各科成绩最高分、最低分和平均分 19&#xff09;*按各科成绩…

Apache和Nginx的区别以及如何选择

近来遇到一些客户需要lnmp环境的虚拟主机&#xff0c;但是Hostease这边的虚拟主机都是基于Apache的&#xff0c;尽管二者是不同的服务器软件&#xff0c;但是大多数情况下&#xff0c;通过适当的配置和调整两者程序也是可以兼容的。 目前市面上有许多Web服务器软件&#xff0c;…

rust使用Atomic创建全局变量和使用

Mutex用起来简单&#xff0c;但是无法并发读&#xff0c;RwLock可以并发读&#xff0c;但是使用场景较为受限且性能不够&#xff0c;那么有没有一种全能性选手呢&#xff1f; 欢迎我们的Atomic闪亮登场。 从 Rust1.34 版本后&#xff0c;就正式支持原子类型。原子指的是一系列…

HCIP第二节

OSPF&#xff1a;开放式最短路径协议&#xff08;属于IGP-内部网关路由协议&#xff09; 优点&#xff1a;相比与静态可以实时收敛 更新方式&#xff1a;触发更新&#xff1a;224.0.0.5/6 周期更新&#xff1a;30min 在华为设备欸中&#xff0c;默认ospf优先级是10&#…

对于子数组问题的动态规划

前言 先讲讲我对于这个问题的理解吧 当谈到解决子数组问题时&#xff0c;动态规划(DP)是一个强大的工具&#xff0c;它在处理各种算法挑战时发挥着重要作用。动态规划是一种思想&#xff0c;它通过将问题分解成更小的子问题并以一种递归的方式解决它们&#xff0c;然后利用这些…

500行代码实现贪吃蛇(1)

文章目录 目录1. Win32 API 介绍1.1 Win32 API1.2 控制台程序&#xff08;Console&#xff09;1.3 控制台屏幕上的坐标COORD1.4 [GetStdHandle](https://learn.microsoft.com/zh-cn/windows/console/getstdhandle)1.5 [GetConsoleCursorInfo](https://learn.microsoft.com/zh-c…

【论文阅读】Sparse is Enough in Scaling Transformers

Sparse is Enough in Scaling Transformers 论文地址摘要1 介绍2 相关工作模型压缩。模型修剪模型蒸馏。稀疏注意力。张量分解。稀疏前馈。 3 Sparse is Enough3.1 稀疏前馈层3.2 稀疏 QKV 层3.3 稀疏损失层。 4 长序列的稀疏性4.1 长序列架构4.2 内存效率的可逆性4.3 泛化的循…

泰克示波器电流探头如何抓浪涌电流波形?

泰克示波器是一种常见的电子测量仪器&#xff0c;广泛应用于电子工程、通信工程、医疗设备等领域。它的主要功能是实时显示电信号的波形&#xff0c;从而帮助工程师和技术人员分析和调试电路。而在一些特定的应用场景中&#xff0c;例如电源、电机、电器设备等&#xff0c;我们…

分布式与一致性协议之ZAB协议(二)

ZAB协议 ZAB协议是如何实现操作地顺序性的&#xff1f; 如果用一句话解释ZAB协议到底是什么&#xff0c;我觉得它是能保证操作顺序性的、基于主备模式的原子广播协议。 接下来&#xff0c;还是以指令X、Y为例具体演示一下&#xff0c;帮助你更好地理解为什么ZAB协议能实现操作…

【不使用深度学习框架】多层感知机实现手写Minist数据集识别

手写Minist识别是一个非常经典的问题&#xff0c;其数据集共有70000张28*28像素的图片&#xff0c;其中60000张作为训练集&#xff0c;剩下的10000张作为测试集&#xff0c;每一张图片都表示了一个手写数字&#xff0c;经过了灰度处理。 本文延续前面文章提到的多层感知机&…

【Osek网络管理测试】[TG1_TC12]网络管理报文ID范围

&#x1f64b;‍♂️ 【Osek网络管理测试】系列&#x1f481;‍♂️点击跳转 文章目录 1.环境搭建2.测试目的3.测试步骤4.预期结果5.测试结果 1.环境搭建 硬件&#xff1a;VN1630 软件&#xff1a;CANoe 2.测试目的 验证DUT可识别的网络管理报文NMID(0x400~0x46F) 3.测试…

从一到无穷大 #26 Velox:Meta用cpp实现的大一统模块化执行引擎

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言业务案例PrestoSparkXStreamDistributed messaging systemData IngestionData Pr…

JavaScript的操作符运算符

前言&#xff1a; JavaScript的运算符与C/C一致 算数运算符&#xff1a; 算数运算符说明加-减*乘%除/取余 递增递减运算符&#xff1a; 运算符说明递增1-- 递减1 补充&#xff1a; 令a1&#xff0c;b1 运算a b ab12ab22ab--10a--b00 比较(关系)运算符&#xff1a; 运算…

(优作)基于STM32 人群定位、调速智能风扇设计(程序、设计报告、视频演示)

引言 当今生活中&#xff0c;风扇已成为人们解暑的重要工具&#xff0c;然而使用风扇缓解夏日酷热的同时也存在着一些问题。比如&#xff0c;由于风扇的转动方向只能机械式的保持在一定范围内&#xff0c;而不能根据人群的位置做出具体的调整&#xff0c;即在一片区域内&#x…

MongoDB详解

目录 一、MongoDB概述 1.MongoDB定义 2.MongoDB主要特点 2.1文档 2.2集合 2.3数据库 2.4数据模型 二、安装MongoDB 1.Windows安装MongoDB 1.1下载MongoDB 1.2安装MongoDB 1.3配置MongoDB 1.3.1可能遇到的问题 1.4安装一盒可视化工具 2.Linux安装MongoDB 2.1下载…

苍穹外卖项目

Day01 收获 补习git Git学习之路-CSDN博客 nginx 作用&#xff1a;反向代理和负载均衡 swagger Swagger 与 Yapi Swagger&#xff1a; 可以自动的帮助开发人员生成接口文档&#xff0c;并对接口进行测试。 项目接口文档网址&#xff1a; ​​​​​​​http://localhost:808…