【C语言】一节课拿捏---动态内存分配

谢谢观看!希望以下内容帮助到了你,对你起到作用的话,可以一键三连加关注!你们的支持是我更新地动力。
因作者水平有限,有错误还请指出,多多包涵,谢谢!


目录

  • 一、 为什么要有动态内存分配
  • 二、`malloc`和`free`
    • 2.1`malloc`
    • 2.2`free`
  • 三、`calloc`和`realloc`
    • 3.1`calloc`
    • 3.2`realloc`
  • 四、 常见的动态内存的错误
    • 4.1对NULL指针的解引用操作
    • 4.2对动态开辟空间的越界访问
    • 4.3对非动态开辟内存使用`free`释放
    • 4.4 使用`free`释放一块动态开辟内存的一部分
    • 4.5对同一块动态内存多次释放
    • 4.6 动态开辟内存忘记释放(内存泄漏)
  • 五、动态内存经典笔试题分析
    • 题目1
    • 题目2
    • 题目3
    • 题目4
  • 六、柔性数组
    • 6.1 柔性数组的特点:
    • 6.2 柔性数组的使用
    • 6.3 柔性数组的优势
  • 七、总结C/C++中程序内存区域划分

一、 为什么要有动态内存分配

  我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

  但是上述的开辟空间的方式有两个特点

  • 空间开辟大小是固定的。
  • 数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小会根据实际情况发生变化,如果使用数组的话,可能开辟的空间太大,也可能开辟的空间不够那数组的编译时开辟空间的方式就不能满足了

  所以在C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。这就需要使用到库函数来进行申请内存空间了。


  

二、mallocfree

2.1malloc

  C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);
//size表示需要申请的一块空间大小是多少,单位是字节
//void*表示返回那一块空间的起始位置的地址

  这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针

  注意事项

  • 如果开辟成功,则返回一个指向开辟好空间的指针
  • 如果开辟失败,则返回一个 NULL 指针,因此malloc的返回值⼀定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size0malloc的行为是标准是未定义的,取决于编译器
  • 使用函数malloc之前需要加上头文件#include<stdlib.h>

  具体使用方式

//代码一
int* p = (int*)malloc(10*sizeof(int));
char* p = (char*)malloc(10*sizeof(char));//代码二
#include<stdio.h>
#include<stdlib.h>int main()
{int* p = (int*)malloc(10*sizeof(int));if(p == NULL){//空间开辟失败perror("malloc");//perror函数会打印出错误信息return 1;//1表示异常返回,在主函数main()中}//可以使用40个字节的空间了int i = 0;for(i = 0;i < 10;i++){scanf("%d",(p+i));//scanf需要的参数是空间的地址}for(i = 0;i < 10;i++){printf("%d ",*(p+i));}return 0;
}

  那么malloc申请的空间和数组的空间有什么区别呢?

  • 动态内存的大小是可以调整的
  • 开辟空间的位置不一样,malloc开辟的空间在堆区,数组arr开辟的空间在栈区

在这里插入图片描述

  

2.2free

  C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下::

void free (void* ptr);
//ptr表示形参是指针,传递地址给它
//void *表示传递任何类型的指针都可以

  free函数用来释放动态开辟的内存

  注意事项

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
  • 如果参数 ptrNULL指针,则函数什么事都不做。

  具体使用方式

#include<stdio.h>
#include<stdlib.h>int main()
{int* p = (int*)malloc(10*sizeof(int));if(p == NULL){//空间开辟失败perror("malloc");//perror函数会打印出错误信息return 1;//1表示异常返回,在主函数main()中}//可以使用40个字节的空间了int i = 0;for(i = 0;i < 10;i++){scanf("%d",(p+i));//scanf需要的参数是空间的地址}for(i = 0;i < 10;i++){printf("%d ",*(p+i));}free(p);//动态开辟的空间使用后要进行释放//释放的是指针p指向的空间,使空间归还给操作系统//但是p依然指向着那块空间,还保留着地址,此时p就是野指针了,所以还得对p指针赋值NULLp = NULL;//将NULL赋值给p,表示p是空指针return 0;
}

总结mallocfree函数最好成对使用


  

三、callocrealloc

3.1calloc

  C语言还提供了一个函数叫 calloccalloc 函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);
//num表示开辟空间的个数
//size表示一个空间的大小是多少,单位是字节
//void* 表示返回所开辟空间的起始地址

  注意事项

  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,其他使用方法和malloc函数一模一样。

  具体使用方式

#include<stdio.h>
#include<stdlib.h>int main()
{//申请10个整型的空间//int* p = (int*)malloc(10*sizeof(int));int* p = (int*)calloc(10,sizeof(int));if(p == NULL){//空间开辟失败perror("calloc");//perror函数会打印出错误信息return 1;//1表示异常返回,在主函数main()中}//可以使用40个字节的空间了int i = 0;for(i = 0;i < 10;i++){printf("%d ",*(p+i));//结果为:0 0 0 0 0 0 0 0 0 0 }free(p);//动态开辟的空间使用后要进行释放//释放的是指针p指向的空间,使空间归还给操作系统//但是p依然指向着那块空间,还保留着地址,此时p就是野指针了,所以还得对p指针赋值NULLp = NULL;//将NULL赋值给p,表示p是空指针return 0;
}

  

3.2realloc

  函数原型如下:

void* realloc (void* ptr, size_t size);
//ptr表示要调整的内存地址
//size表示调整之后新大小
//返回值为调整之后的内存起始位置

  这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

  具体使用方式

int main()
{//申请10个整型的空间int* p = (int*)calloc(10, sizeof(int));if (p == NULL){perror("calloc");return 1;}//使用空间int i = 0;for (i = 0; i < 10; i++){printf("%d ", p[i]);//*(p+i)}//调整空间-希望变成20个整型空间int* p = (int*)realloc(p, 20 * sizeof(int));//这里对realloc函数返回空间的起始地址传递给指针p是否正确?if (ptr != NULL){p = ptr;}//使用//...//释放free(p);p = NULL;return 0;
}//用法二:realloc不仅可以调整空间,可以开辟空间
int * p = (int *)realloc(NULL,40);
//等价于 int * p = (int*)malloc(10*sizeof(int))

  在上面的代码中的这一部分,我们怎么判断是否正确呢?这就需要了解一下,realloc函数调整内存空间的细节了

//调整空间-希望变成20个整型空间int* p = (int*)realloc(p, 20 * sizeof(int));//这里对realloc函数返回空间的起始地址传递给指针p是否正确?

  realloc在调整内存空间的是存在两种情况:

  • 情况1:原有空间之后有足够大的空间
  • 情况2:原有空间之后没有足够大的空间

  注意情况1情况2都是正常可以分配空间的情况,如果未分配成功的话,会返回NULL空指针。
在这里插入图片描述
  
  所以通过上面的细节,我们知道了假如realloc函数分配空间失败后返回NULL,然后我们将指针p去接受,会导致我们连原来已经分配好的空间都找不到了但是我们希望就算分配失败了也不应该将原来的空间都找不到。所以我们可以使用另一个指针ptr去接收它的返回值,然后再进行判断这个指针是否为NULL,如果不是NULL,则再将prt的值赋值给p

int main()
{//申请10个整型的空间int* p = (int*)calloc(10, sizeof(int));if (p == NULL){perror("calloc");return 1;}//使用空间int i = 0;for (i = 0; i < 10; i++){printf("%d ", p[i]);//*(p+i)}//调整空间-希望变成20个整型空间//int* p = (int*)realloc(p, 20 * sizeof(int));不使用这种方式,有危险int* ptr = (int*)realloc(p, 20 * sizeof(int));//ptr不为NULL时,再赋值给pif (ptr != NULL){p = ptr;}//使用//...//释放free(p);p = NULL;return 0;
}

  

四、 常见的动态内存的错误

4.1对NULL指针的解引用操作

//错误写法
void test(){int *p = (int *)malloc(INT_MAX/4);*p = 20;//如果p的值是NULL,就会有问题//因为malloc函数可能会分配空间失败返回NULL,然而当对NULL空指针解引用操作*会出错free(p);p=NULL;}//正确写法
void test(){int *p = (int *)malloc(INT_MAX/4);if(p == NULL){perror("malloc");return 1;}*p = 20;free(p);p=NULL;}

4.2对动态开辟空间的越界访问

//错误写法
void test(){int i = 0;int *p = (int *)malloc(10*sizeof(int));if(NULL == p){exit(EXIT_FAILURE);}for(i=0; i<40; i++){*(p+i) = i;//当i是10的时候越界访问}free(p);}//正确写法
void test(){int i = 0;int *p = (int *)malloc(10*sizeof(int));if(NULL == p){exit(EXIT_FAILURE);}for(i=0; i<10; i++){*(p+i) = i;//当i是10的时候越界访问}free(p);}

4.3对非动态开辟内存使用free释放

//错误写法
void test()
{int a = 10;int *p = &a;free(p);//ok?//free只能对动态内存处理}

4.4 使用free释放一块动态开辟内存的一部分

//错误写法
void test(){int *p = (int *)malloc(100);p++;//此时的p指针并不是指向了动态内存分配的起始地址,而是跳过了4个字节的空间free(p);//p不再指向动态内存的起始位置}

4.5对同一块动态内存多次释放

//错误写法
void test(){int *p = (int *)malloc(100);free(p);free(p);//重复释放,相当于对p野指针进行了释放,这是不允许的}//正确写法
void test(){int *p = (int *)malloc(100);//...free(p);p=NULL;//...free(p);p=NULL;}

4.6 动态开辟内存忘记释放(内存泄漏)

void test(){int *p = (int *)malloc(100);if(NULL != p){*p = 20;}}
int main(){test();while(1);}

  


五、动态内存经典笔试题分析

题目1

//错误写法
void GetMemory(char *p)//p是str的临时拷贝{p = (char *)malloc(100);//后面没有free释放,导致内存泄漏}
void Test(void){char *str = NULL;GetMemory(str);strcpy(str, "hello world");//str还是空指针,导致了对NULL指针解引用操作,程序崩溃//要知道strcpy的实现printf(str);//这样的写法没有错,是对的,类型于printf("hehe\n"),"hehe\n"是常量字符串,值是首字符的地址}//正确写法
//写法一
void GetMemory(char **p){*p = (char *)malloc(100);}
void Test(void){char *str = NULL;GetMemory(&str);strcpy(str, "hello world");printf(str);free(str)str = NULL;}
//写法二
void GetMemory(){char * p = (char *)malloc(100);return p;}
void Test(void){char *str = NULL;str =GetMemory();strcpy(str, "hello world");printf(str);free(str)str = NULL;}

题目2

char *GetMemory(void)
{char p[] = "hello world";//当出了该函数,数组是局部变量,有作用域,空间中的数据会被销毁return p;
}
void Test(void)
{char *str = NULL;str = GetMemory();//虽然str指针接收了字符数组的首元素地址,指向了原来的字符数组空间,但是那空间已经归还操作系统了,不在属于这个程序了//str是野指针了printf(str);
}

  注意:题目2就是典型的错误返回栈空间地址的问题。

题目3

//错误写法
void GetMemory(char **p, int num){*p = (char *)malloc(num);}
void Test(void){char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);//结果可以打印出hello,但是不使用后,要将动态申请的内存空间给释放掉,//所以导致内存泄漏}//正确写法
void GetMemory(char **p, int num){*p = (char *)malloc(num);}
void Test(void){char *str = NULL;GetMemory(&str, 100);strcpy(str, "hello");printf(str);free(str);str = NULL;}

题目4

//错误写法
void Test(void){char *str = (char *) malloc(100);strcpy(str, "hello");free(str);//此时str是野指针,但是str还是指向原来的空间if(str != NULL){strcpy(str, "world");//strcpy函数中年会对野指针str进行了解引用操作,非法访问printf(str);}}//正确写法
void Test(void){char *str = (char *) malloc(100);strcpy(str, "hello");free(str);str = NULL;if(str != NULL){strcpy(str, "world");printf(str);}}

  


六、柔性数组

  也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

struct S
{int n;char c;double d;int arr[];//未知大小的数组 - arr就是柔性数组的成员//也可以写成int arr[0]
};

6.1 柔性数组的特点:

  • 结构中的柔性数组成员前面必须至少一个其他成员。
  • sizeof 返回的这种结构大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
struct S
{int n;int arr[0];//柔性数组成员
};
int main()
{printf("%d\n", sizeof(struct S));//输出的是4return 0;
}

6.2 柔性数组的使用

struct S
{int n;int arr[];//柔性数组成员
};
int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 20*sizeof(int));//20*sizeof(int)是为arr数组动态分配的,表示数组arr有20个整型if(ps == NULL){perror("malloc()");return 1;}//使用空间ps->n=100;int i = 0;for(i = 0; i < 20 ; i++){ps->arr[i] = i + 1;}//调整ps指向空间的大小struct S* ptr = (struct S*)realloc(ps,sizeof(struct S) + 40 * sizeof(int));if(ps != NULL){ps = ptr;ptr = NULL;//防止ptr为野指针}else{return 1;}//使用空间for(i = 0 ; i < 40 ; i++){printf("%d ",ps->arr[i]);}//释放空间free(ps);ps = NULL;return 0;
}

6.3 柔性数组的优势

//代码一
struct S
{int n;int* arr;
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){perror("malloc");return 1;}int*tmp = (int*)malloc(20*sizeof(int));if (tmp != NULL){ps->arr = tmp;}else{return 1;}ps->n = 100;int i = 0;//给arr中的20个元素赋值为1~20for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//调整空间tmp = (int*)realloc(ps->arr, 40*sizeof(int));if (tmp != NULL){ps->arr = tmp;}else{perror("realloc");return 1;}//for (i = 0; i < 40; i++){printf("%d ", ps->arr[i]);}//释放free(ps->arr);//先释放第二次申请的空间ps->arr = NULL;free(ps);//再释放第一次申请的空间ps = NULL;return 0;
}
//代码二
struct S
{int n;//4int arr[];
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S) + 20*sizeof(int));if(ps == NULL){perror("malloc()");return 1;}//使用这些空间ps->n = 100;int i = 0;for (i = 0; i < 20; i++){ps->arr[i] = i + 1;}//调整ps指向空间的大小struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));if (ptr != NULL) {ps = ptr;ptr = NULL;}else{return 1;}//使用for (i = 0; i < 40; i++){printf("%d ", ps->arr[i]);}//释放空间free(ps);ps = NULL;return 0;
}

  
  上述 代码一代码二 可以完成同样的功能,但是 代码二 的实现有两个好处
在这里插入图片描述

  


七、总结C/C++中程序内存区域划分

在这里插入图片描述
在这里插入图片描述

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

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

相关文章

PgSQL技术内幕 - psql与服务端连接与交互机制

PgSQL技术内幕 - 客户端psql与服务端连接与交互机制 简单来说&#xff0c;PgSQL的psql客户端向服务端发起连接请求&#xff0c;服务端接收到请求后&#xff0c;fork出一个子进程&#xff0c;之后由该子进程和客户端进行交互&#xff0c;处理客户端的SQL等&#xff0c;并将结果返…

【Python】一文向您详细介绍 __str__ 的作用和用法

【Python】一文向您详细介绍 str 的作用和用法 下滑即可查看博客内容 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我静心耕耘深度学习领域、真诚分享知识与智慧的小天地&#xff01;&#x1f387; &#x1f393; 博主简介&#xff1a;985高校的普通本硕&…

【CS.AI】AI引领编程新时代:深度探索GitHub Copilot

文章目录 引言0. TOP TAKEAWAYS 重要要点1. Copilot的基本功能2. 技术原理3. 优势与局限优势局限 4. 使用体验4.1 初次使用4.2 在 JetBrains 全家桶中使用 GitHub Copilot1. 安装插件2. 配置插件3. 使用 GitHub Copilot 4.3 日常开发4.4 体验与反馈 5. 对开发者生态系统的影响5…

梯度下降: 02. 批量梯度下降BGD,随机梯度下降SGD,小批量梯度下降MBGD

简介 本文从原理上介绍了三种梯度下降的方法,相同点,异同点,优缺点。 内容包含了数学公式的推导与说明 1. 梯度下降的3种方法 梯度下降分三类,原理基本相同,操作方式略有区别 批量梯度下降BGD(BatchGradient Descent):使用全量数据进行特征抽取,模型训练小批量梯度下降…

VueRouter3学习笔记

文章目录 1&#xff0c;入门案例2&#xff0c;一些细节高亮效果非当前路由会被销毁 3&#xff0c;嵌套路由4&#xff0c; 传递查询参数5&#xff0c;命名路由6&#xff0c;传递路径参数7&#xff0c;路径参数转props8&#xff0c;查询参数转props9&#xff0c;replace模式10&am…

【C++】深入理解decltype和decltype(auto)

深入理解decltype和decltype&#xff08;auto&#xff09; 一、decltype语法介绍二、decltype的推导规则1. expr不加括号2. expr加上括号 三、关于decltype的CV属性推导四、 decltype(auto) 的使用 一、decltype语法介绍 decltype关键字是C11新标准引入的关键字&#xff0c;它…

Hadoop3:MapReduce源码解读之Map阶段的Job任务提交流程(1)

3、Job工作机制源码解读 用之前wordcount案例进行源码阅读&#xff0c;debug断点打在Job任务提交时 提交任务前&#xff0c;建立客户单连接 如下图&#xff0c;可以看出&#xff0c;只有两个客户端提供者&#xff0c;一个是YarnClient&#xff0c;一个是LocalClient。 显然&a…

Objective-C 学习笔记 | 基础

Objective-C 学习笔记 | 基础 参考书&#xff1a;《Objective-C 编程&#xff08;第2版&#xff09;》 第1部分 入门 Objective-C语言是以C语言为基础的&#xff0c;但增加了对面向对象编程的支持。Objective-C语言是用来开发在苹果iOS以及OS X操作系统上运行的应用的编程语…

植物大战僵尸杂交版 2.0 下载及配置图文教程

文章目录 Part.I IntroductionPart.II 下载Chap.I 下载地址Chap.II 网盘直链下载 Part.III 配置Chap.I 解压与安装Chap.II 加载存档Chap.III 其他设置 Reference Part.I Introduction 最近看大仙儿直播植物大战僵尸&#xff0c;觉得挺好玩的。它大概长这样&#xff1a; 就上网…

使用proteus仿真51单片机的流水灯实现

proteus介绍&#xff1a; proteus是一个十分便捷的用于电路仿真的软件&#xff0c;可以用于实现电路的设计、仿真、调试等。并且可以在对应的代码编辑区域&#xff0c;使用代码实现电路功能的仿真。 汇编语言介绍&#xff1a; 百度百科介绍如下&#xff1a; 汇编语言是培养…

Python GUI编程:深入探索现代GUI库及其创新应用

目录 引言 Python GUI库概览 1. Tkinter 2. PyQt/PySide 3. wxPython 4. Kivy 5. PyGTK 6.FLTK (pyFLTK) 创新应用案例 1. 交互式数据分析工具 2. 智能物联网(IoT)仪表板 3. 增强现实(AR)辅助设计软件 4. 跨平台的科学计算软件 5. 交互式教育软件 实战示例1&…

.NET MAUI 了解MVVM

MVVM 模式中有三个核心组件&#xff1a;模型、视图和视图模型。 每个组件的用途不同。 下图显示了这三个组件之间的关系。 视图 视图负责定义用户在屏幕上看到的结构、布局和外观。 理想情况下&#xff0c;每个视图在 XAML 中定义&#xff0c;代码隐藏有限&#xff0c;不包含业…

linux shell实现打印国际象棋棋盘

chess.sh #!/bin/bashfor i in {1..8} dofor j in {1..8}dosum$[ij]if [ $[sum%2] -eq 0 ];thenecho -ne "\033[46m \033[0m"elseecho -ne "\033[47m \033[0m"fidoneecho done验证&#xff1a;

微信小程序学习笔记(4)

文章目录 1、< template >< / template >2、样式导入i、wxmlii、wxss 3、flex布局i、容器属性ii、项目属性 1、< template >< / template > 模板可以重复调用 首先要定义一个模板&#xff1a; <template name"test"><view>{{…

AbstractMap和SimpleEntry

一、AbstractMap 位置&#xff1a;在java.util包 二、SimpleEntry 1、概述 继承了Map中的内部接口Entry<K,V> SimpleEntry<K,V>不仅继承了Map.Entry<K,V>&#xff0c;还继承了序列化的接口 2、构造方法 方法说明SimpleEntry(K key,V value)通过键值对初…

RabbitMQ-工作模式(Publish模式Routing模式)

文章目录 发布/订阅&#xff08;Publish/Subscribe&#xff09;交换机临时队列绑定总体代码示例 路由&#xff08;Routing&#xff09;绑定直连交换机多重绑定发送日志订阅总体代码示例 更多相关内容可查看 发布/订阅&#xff08;Publish/Subscribe&#xff09; 构建一个简单的…

vue antdesgin table 动态表头动态数据示例

以下是一个基于 Vue 和 Ant Design Vue 的示例&#xff0c;可以动态生成表格的表头和数据&#xff1a; <template><div><a-button click"addColumn">添加列</a-button><a-table :columns"columns" :dataSource"dataSource…

HC-05蓝牙模块配置连接和使用

文章目录 1. 前期准备 2. 进入AT模式 3. 电脑串口配置 4. 配置过程 5. 主从机蓝牙连接 6. 蓝牙模块HC-05和电脑连接 1. 前期准备 首先需要准备一个USB转TTL连接器&#xff0c;电脑安装一个串口助手&#xff0c;然后按照下面的连接方式将其相连。 VCCVCCGNDGNDRXDTXDTXD…

ICLR24大模型提示(8) | 退一步思考:在大型语言模型中通过抽象引发推理

【摘要】我们提出了一种简单的提示技术&#xff0c;即后退提示法&#xff0c;它使 LLM 能够进行抽象&#xff0c;从包含特定细节的实例中得出高级概念和第一原理。通过使用概念和原理来指导推理&#xff0c;LLM 显著提高了遵循正确推理路径解决问题的能力。我们使用 PaLM-2L、G…

Facebook企业户 | Facebook公共主页经营

Facebook作为社交媒体巨头&#xff0c;拥有庞大的用户基数&#xff0c;因此&#xff0c;有效经营公共主页是获取持续流量、提升客户信任度和粘性、促进产品或服务销售与转化的关键。要优化Facebook主页&#xff0c;关注以下几点&#xff1a; 1、参与度是关键指标&#xff1a;因…