当前位置: 首页 > news >正文

C和C++内存管理

C和C++内存管理

  • (一)C/C++内存分布
  • (二)C语言动态内存管理
  • (三)c++内存管理
    • (3.1)new/delete操作内置类型
    • (3.2)new和delete操作自定义类型
  • (四)operator new与operator delete函数
  • (五)new与delete的实现原理
    • (5.1)内置类型
    • (5.2)自定义类型
    • (5.3)不匹配使用(面试可能会考)
  • (六)定位new表达式(placement-new) (了解)
  • (七)malloc/free和new/delete的区别(重点)

(一)C/C++内存分布

在这里插入图片描述
我们可以在任务管理器中查看电脑当前进程:
在这里插入图片描述

下图是在c语言阶段我们学过的内存区域划分,这一块在C++同样适用:
在这里插入图片描述
说明:

  1. 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段–存储全局数据和静态数据。
  5. 代码段–可执行的代码/只读常量。

那我们先根据几个题目来回忆我们c语言中内存如何分配的:
在这里插入图片描述
前五个比较简单,大家一定不能出错
第一个:A,main函数外定义的变量都是静态的,生命周期都是从程序开始到结束
第二个:A,全局静态一样
第三个:A,由static修饰的也是全局的
第四个:A,没有static修饰的都是局部的
第五个:A,这里的数组名代表整个数组,也是在局部的栈里存放
后面六个难度上升了很多:
第六个:A,char2也是数组,并且我是我们主动申请的,所以也在栈
第七个:A,*char2是解引用,这里的char2代表的首元素地址,首元素a在栈上
第八个:A, pchar3是一个指针,不管它的const修饰指向的内容还是指针,指针都存于栈
第九个:D,*pchar3也是指向第一个元素的地址,但是内容部分用const修饰了,所以在常量区
第十个:A,ptr1是一个指针
第十一个:B,ptr1所指向的内容使我们主动开辟的,所以在堆区。
在这里插入图片描述

(二)C语言动态内存管理

C语言中动态内存管理方式:malloc/calloc/realloc/free
这一块在c语言部分讲的很多了,所以我就简单的带大家回忆一下,
malloc:开固定的空间,不初始化。
calloc:开空间,初始化0.
realloc:支持拓展我们原来开的空间。
free:释放我们主动开辟的空间。

【面试题】

  1. malloc/calloc/realloc的区别?
  2. malloc的实现原理? 视频链接:https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0

这一块就这样过了,如果这一块还不懂得,建议回去补一下c语言的课。

(三)c++内存管理

(3.1)new/delete操作内置类型

我们简单得来使用一下new和delete

int main()
{int* p1 = new int;int* p2 = new int[10];//简化了c语言sizeof与强转部分delete p1;delete[] p2;//初始化int* p3 = new int(3);int* p4 = new int[10] {0};//全都初始化为0int* p5 = new int[10] {1, 2, 3, 4};//前四个值初始化delete p3;delete[] p4;delete[] p5;return 0;
}

在这里插入图片描述

(3.2)new和delete操作自定义类型

class A
{
public:A(int a = 0){_a = a;cout << "A()" << endl;}~A(){cout << "~A()" << endl;}void print(){cout << _a << endl;}
private:int _a;
};int main()
{A* p1 = new A;A* p2 = new A(1);delete p1;delete p2;return 0;
}

在这里插入图片描述
大家可以看到c++这块new与delete会自动调用自定义类型的构造和析构函数。
这一块非常的方便我们来演示一下c++里的链表:

struct ListNode
{int _val;ListNode* next;ListNode(int val) :_val(val),next(nullptr){}
};
//c++的struct里的变量与函数均为共有的,这一部分前面讲过
int main()
{ListNode* n1 = new ListNode(1);ListNode* n2 = new ListNode(2);ListNode* n3 = new ListNode(3);ListNode* n4 = new ListNode(4);ListNode* n5 = new ListNode(5);n1->next = n2;n2->next = n3;n3->next = n4;n4->next = n5;return 0;
}

这里是不是相较于c里的初始化啊,插入什么的简单许多。

在这里插入图片描述
总之我们只需要记住在c++的使用过程中使用new,delete就行,因为new包含了malloc,delete包含了free。
此外我们在使用new和时是不是与malloc还有一个差别,那就是没有判断是否申请成功,C++引入了抛异常。
抛异常在c++进阶里才会详细的讲解,这里我们简单的演示一下:

int main()
{try{throw try catchvoid* p1 = new char[1024 * 1024 * 1024];cout << "p1" << endl;void* p2 = new char[1024 * 1024 * 1024];cout << "p2" << endl;void* p3 = new char[1024 * 1024 * 1024];cout << "p3" << endl;}catch (const exception& e){cout << e.what() << endl;//发生了什么错误}return 0;
}void func()
{//throw try catchvoid* p1 = new char[1024 * 1024 * 1024];cout << "p1" << endl;void* p2 = new char[1024 * 1024 * 1024];cout << "p2" << endl;void* p3 = new char[1024 * 1024 * 1024];cout << "p3" << endl;
}int main()
{try{func();}catch (const exception& e){cout << e.what() << endl;//发生了什么错误}return 0;
}

上面是在main函数里申请失败,下面是在调用的一个函数里失败。

void func()
{int n = 0;while (1){char* p1 = new char[1024 * 1024];//1024*1024byte是1Mcout << "p1->" << endl;n++;cout << n << endl;;}
}int main()
{try{func();}catch (const exception& e){cout << e.what() << endl;//发生了什么错误}return 0;
}

这个函数就是来看在32位环境下究竟能申请多少MB
在这里插入图片描述
大家可以看到32位(总共4G)下大概只能申请1.7G左右,所以堆在内存占大部分。

(四)operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new 与operator delete底层源码如下,这一块大家不需要看的特别明白,

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空               间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid* p;while ((p = malloc(size)) == 0)//mallocif (_callnewh(size) == 0){// report no memory// 如果申请内存失败了,这里会抛出bad_alloc 类型异常static const std::bad_alloc nomem;_RAISE(nomem);}return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{_CrtMemBlockHeader* pHead;RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));if (pUserData == NULL)return;_mlock(_HEAP_LOCK);  /* block other threads */__TRY/* get a pointer to memory block header */pHead = pHdr(pUserData);/* verify block type */_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK);  /* release other threads */__END_TRY_FINALLYreturn;
}
/*
free的实现
*/
#define   free(p)               _free_dbg(p, _NORMAL_BLOCK)//free

通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

(五)new与delete的实现原理

(5.1)内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

(5.2)自定义类型

(1)new的原理

  1. 调用operator new函数申请空间
  2. 在申请的空间上执行构造函数,完成对象的构造
    (2)delete的原理
  3. 在空间上执行析构函数,完成对象中资源的清理工作
  4. 调用operator delete函数释放对象的空间
    (3)new T[N]的原理
  5. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
  6. 在申请的空间上执行N次构造函数
    (4)delete[]的原理
  7. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
  8. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

(5.3)不匹配使用(面试可能会考)

在这里插入图片描述

上面第一组是可以的,第二组不行,我们可以清楚的看到第二组的free少调用了析构函数,这里有内存泄漏的风险。
那我们来看一个神奇的东西:

class A
{
public:A(int a = 0, int b = 0){_a = a;_b = 1;cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;int _b;
};
class B
{
private:int _a;int _b;
};
int main()
{int* p1 = new int[10];//这里没有调用构造函数,实质上只有mallocdelete p1;//这里虽然不匹配,但是内存也不会出现泄漏,实质这里只使用freefree(p1);A* p2 = new A[10];delete p2;//这里有问题B* p3 = new B[10];delete p3;//这里没有问题return 0;
}

大家自己可以去vs里运行一下,那为什么A有问题,B没有问题呢?

首先我们的A与B大小是不是都是8个字节,那我们都调用了10次,那么10个这样的对象是不是80个字节,但是实际上A的10个对象占了84个字节,B的10个对象占了80字节,多的4个字节是用来存储对象的个数。
在这里插入图片描述
那为什么A会多开4个字节存储个数呢?
这是因为B没有析构函数,所以编译器就给优化了,严格的来说多存储的个数是给delete后面的[]使用的,当类没有析构函数时,就认为没有开辟的空间需要释放,只要我们写了析构,编译器就不会再有优化,那B的大小就变为84,。

这里总结就是一定要匹配使用,不要错配。

(六)定位new表达式(placement-new) (了解)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
上面什么意思呢?
就是我们可以只开空间,不初始化,也就是malloc的功能。

class A
{
public:A(int a = 0, int b = 0){_a = a;_b = 1;cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;int _b;
};
int main()
{A* ptr1 = new A;//调用了构造A* ptr2 = (A*)operator new(sizeof(A));//不调构造new(ptr2)A(1, 0);//定位new//下面两步合起来等于上面一步delete ptr1;ptr2->~A();//析构函数是可以显示调用的operator delete(ptr2);//下面两步合起来等于上面一步return 0;
}

大家可能认为这有点脱了裤子放屁对吧,但我想说的是—说得对,但它也有他自己的用途,在生活中99%的场景用不到,需要用的场景是内存池、线程池,连接池,这一块不讲,只是给大家说下有这个东西。
在这里插入图片描述

(七)malloc/free和new/delete的区别(重点)

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

  1. malloc和free是函数,new和delete是操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
  4. malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放

相关文章:

C和C++内存管理

C和C内存管理 &#xff08;一&#xff09;C/C内存分布&#xff08;二&#xff09;C语言动态内存管理&#xff08;三&#xff09;c内存管理&#xff08;3.1&#xff09;new/delete操作内置类型&#xff08;3.2&#xff09;new和delete操作自定义类型 &#xff08;四&#xff09;…...

axios取消请求

1.使用CancelToken: class RequestHttp {service: AxiosInstance;public constructor(config: AxiosRequestConfig) {// 实例化axiosthis.service axios.create(config);/*** description 请求拦截器* 客户端发送请求 -> [请求拦截器] -> 服务器*/this.service.interce…...

阿里中间件——diamond

一、前言 最近工作不忙闲来无事&#xff0c;仔细分析了公司整个项目架构&#xff0c;发现用到了很多阿里巴巴集团开源的框架&#xff0c;今天要介绍的是中间件diamond. 二、diamond学习笔记 1、diamond简介 diamond是一个管理持久配置&#xff08;持久配置是指配置数据会持久化…...

pyenv -- 一款macos下开源的多版本python环境安装管理工具 国内加速版安装 + 项目venv虚拟环境 pip加速 使用与总结

一个比较方便实用的python多版本环境安装管理工具, 阿里云加速版本 pyenv安装方法: 直接克隆本下面到你的本地目录,然后设置环境变量即可 git clone https://gitee.com/tekintian/pyenv.git ~/.pyenv 环境变量配置 在~/.bash_profile 或者 .zshrc 中增加环境变量 export …...

VitePress 自定义 CSS 指南

VitePress 是一款基于 Vite 和 Vue 3 的静态网站生成器&#xff0c;专为文档编写而设计。尽管 VitePress 提供了丰富的默认主题&#xff0c;但在某些情况下&#xff0c;我们可能需要对其进行更深入的定制以满足特定的视觉需求。本文将详细介绍如何通过覆盖根级别的 CSS 变量来自…...

【舍入,取整,取小数,取余数丨Excel 函数】

数学函数 1、Round函数 Roundup函数 Rounddown函数 取整&#xff1a;(Int /Trunc)其他舍入函数&#xff1a; 2、Mod函数用Mod函数提取小数用Mod函数 分奇偶通过身份证号码判断性别 1、Round函数 Roundup函数 Rounddown函数 Round(数字&#xff0c;保留几位小数)&#xff08;四…...

无线信道中ph和ph^2的场景

使用 p h ph ph的情况&#xff1a; Rayleigh 分布的随机变量可以通过两个独立且相同分布的零均值、高斯分布的随机变量表示。设两个高斯随机变量为 X ∼ N ( 0 , σ 2 ) X \sim \mathcal{N}(0, \sigma^2) X∼N(0,σ2)和 Y ∼ N ( 0 , σ 2 ) Y \sim \mathcal{N}(0, \sigma^2)…...

HCIA--实验五:静态路由综合实验

静态路由综合实验 一、实验内容&#xff1a; 1.需求/目的&#xff1a; 在ensp模拟器中使用四个路由器&#xff0c;并且在路由器上创建loopback接口&#xff0c;相当于连接了一台主机&#xff0c;通过配置静态路由的方式实现全网通。 二、实验过程 1.道具&#xff1a; 4个…...

不同vlan之间的通信方法

1.通过路由器的物理接口 1.给PC1,PC2配置IP地址&#xff0c;网关2.进入交换机配置vlan&#xff0c;交换机所有口都配置access口并绑定vlan3.配置路由器&#xff0c;进入路由器的两个接口配置网关IP和掩码缺点&#xff1a;成本高&#xff0c;每增加一个vlan就需要一个物理端口和…...

java后端框架

框架就是对技术的封装。 本篇博客小博主首先对以后我们要学习的框架进行简单概述&#xff0c;使大家对框架有一定的基本概念。 一.mybatis mybatis就是对jdbc&#xff08;数据库连接&#xff09;进行封装&#xff0c;避免了jdbc中手动设置参数&#xff0c;手动映射结果的操作。…...

如何在Word中插入复选框

如何在Word中插入复选框&#xff1a;详细教程与技巧 在Word中插入复选框是一项非常实用的技巧&#xff0c;尤其是在制作问卷调查、待办事项清单、交互式表单或文档中需要用户进行选择时&#xff0c;复选框不仅能提高文档的功能性&#xff0c;还能显得更加专业。本文将详细讲解…...

Android 源码中jni项目 加载so目录小结

Android 源码中jni项目 加载so目录小结 文章目录 Android 源码中jni项目 加载so目录小结一、前言二、so目录验证测试1、jni so文件错误报错&#xff08;1&#xff09;报错1 - 未找到so文件&#xff1a;&#xff08;2&#xff09;报错2 - so文件中未找到native方法&#xff1a; …...

24/9/6算法笔记 kaggle 房屋价格

预测模型主要分为两大类&#xff1a; 回归模型&#xff1a;当你的目标变量是连续的数值时&#xff0c;你会使用回归模型进行预测。回归模型试图找到输入特征和连续输出之间的关联。一些常见的回归模型包括&#xff1a; 线性回归&#xff08;Linear Regression&#xff09;岭回归…...

【MA35D1】buildroot 编译使用经验

文章目录 芯片介绍Buildroot开发Linux实践环境搭建代码获取编译执行步骤&#xff08;仅适用于我公司产品&#xff09; 后续有需要更改的输出文件目录 芯片介绍 NuMicro MA35D1系列为一颗异核同构的多核心微处理器&#xff0c;适用于高端 Edge IIoT Gateway。它是基于双核 64 位…...

排查 MyBatis XML 配置中的 IF 语句与传值名称不匹配的 Bug

文章目录 本文档只是为了留档方便以后工作运维&#xff0c;或者给同事分享文档内容比较简陋命令也不是特别全&#xff0c;不适合小白观看&#xff0c;如有不懂可以私信&#xff0c;上班期间都是在得 前言&#xff0c;在改一个bug得时候发现一个有意思得问题&#xff0c;就是myb…...

数字证书与公钥基础设施

关注这个证书的其他相关笔记&#xff1a;NISP 一级 —— 考证笔记合集-CSDN博客 0x01&#xff1a;数字证书 数字证书是由第三方可信机构&#xff08;一般是证书服务器&#xff09;颁发的数字证书&#xff0c;可以证明身份的可信度。 数字证书具有以下特点以及性质&#xff1a…...

拥抱数智化,JNPF低代码平台如何推动企业转型升级

随着信息技术的飞速发展&#xff0c;企业面临的市场竞争日益激烈&#xff0c;传统的业务流程和管理模式已经难以满足快速变化的市场需求。数智化转型成为企业持续发展的必由之路。在这一过程中&#xff0c;低代码开发平台扮演了至关重要的角色。本文将探讨JNPF低代码平台如何助…...

Linux shell脚本 (十二)case语句_linux awk case语句

​ case工作方式如上所示。取值后面必须为关键字 in&#xff0c;每一模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后&#xff0c;其间所有命令开始执行直至 ;;。;; 与其他语言中的 break 类似&#xff0c;意思是跳到整个 case 语句的最后。​ 取值将…...

【二等奖成品论文】2024年数学建模国赛B题25页成品论文+完整matlab代码、python代码等(后续会更新)

您的点赞收藏是我继续更新的最大动力&#xff01; 一定要点击如下的卡片&#xff0c;那是获取资料的入口&#xff01; 【全网最全】2024年数学建模国赛B题31页完整建模过程25页成品论文matlab/python代码等&#xff08;后续会更新「首先来看看目前已有的资料&#xff0c;还会…...

国内快速高效下载 HuggingFace上的各种大语言模型

预先安装&#xff1a; apt install aria2 # sudo apt install aria2apt install git-lfs # sudo apt install git-lfs下载hfd wget https://hf-mirror.com/hfd/hfd.shchmod ax hfd.sh设置环境变量 Linux export HF_ENDPOINThttps://hf-mirror.comWindows $env:HF_ENDPOINT…...

[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解

突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 ​安全措施依赖问题​ GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql

智慧工地管理云平台系统&#xff0c;智慧工地全套源码&#xff0c;java版智慧工地源码&#xff0c;支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求&#xff0c;提供“平台网络终端”的整体解决方案&#xff0c;提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台

🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...

LeetCode - 199. 二叉树的右视图

题目 199. 二叉树的右视图 - 力扣&#xff08;LeetCode&#xff09; 思路 右视图是指从树的右侧看&#xff0c;对于每一层&#xff0c;只能看到该层最右边的节点。实现思路是&#xff1a; 使用深度优先搜索(DFS)按照"根-右-左"的顺序遍历树记录每个节点的深度对于…...

IP如何挑?2025年海外专线IP如何购买?

你花了时间和预算买了IP&#xff0c;结果IP质量不佳&#xff0c;项目效率低下不说&#xff0c;还可能带来莫名的网络问题&#xff0c;是不是太闹心了&#xff1f;尤其是在面对海外专线IP时&#xff0c;到底怎么才能买到适合自己的呢&#xff1f;所以&#xff0c;挑IP绝对是个技…...

基于IDIG-GAN的小样本电机轴承故障诊断

目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) ​梯度归一化(Gradient Normalization)​​ (2) ​判别器梯度间隙正则化(Discriminator Gradient Gap Regularization)​​ (3) ​自注意力机制(Self-Attention)​​ 3. 完整损失函数 二…...