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

learn C++ NO.7——C/C++内存管理

引言

现在是5月30日的正午,图书馆里空空的,也许是大家都在午休,也许是现在37摄氏度的气温。穿着球衣的我已经汗流浃背,今天热火战胜了凯尔特人,闯入决赛。以下克上的勇气也激励着我,在省内垫底的大学中,我不觉得气馁,我要更加努力学习,让自己能够越来越好,以后肯定也会”晋级决赛”。

1.C/C++程序的内存分布

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

2.变量在内存中存储的位置

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{static int staticVar = 1;int localVar = 1;int num1[10] = { 1, 2, 3, 4 };char char2[] = "abcd";const char* pChar3 = "abcd";int* ptr1 = (int*)malloc(sizeof(int) * 4);int* ptr2 = (int*)calloc(4, sizeof(int));int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);free(ptr1);free(ptr3);
}

在这里插入图片描述
下面我按照从左往右从上到下的顺序依次分析上面的题目。globalVar和staticGlobalVar都是定义在全局域中,所以是存储在静态区中的。staticVar虽然是在局部域内定义的,但是它是static修饰的变量所以依旧是存储在静态区中的。localVar和num1都是在局部域内定义的局部变量,都是存储在栈区空间的。上面第一部分比较简单,下面我以画图加分析的来看下面部分。
为什么char2和*char2都在局部域呢?
在这里插入图片描述
pChar3是一个定义在栈区的const char类型指针,它保存的是常量字符串"abcd"的首元素地址。*pChar3是对常量字符串的首元素地址进行解引用操作,访问的是常量区的空间。ptr1是一个在栈区上创建的指针变量,存放的是动态开辟空间首字节地址。解引用访问ptr1访问的是堆区空间。

数组名单独放在sizeof内部表示整个数组,所以是4*10字节。"abcd"其实是隐含了’\0’字符,所以是4+1=5字节。无论什么指针都是4 or 8字节,指针的大小取决于平台,64位平台8字节,32位平台4字节。

3.C语言的动态内存管理

常见的C语言动态内存管理如下:malloc/calloc/realloc/free。具体细节可以移步c语言动态内存管理,查看这里不做赘述。

void Test ()
{int* p1 = (int*) malloc(sizeof(int));free(p1);// 1.malloc/calloc/realloc的区别是什么?int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);// 这里需要free(p2)吗?free(p3 );
}

1.malloc就是单纯地开辟堆区空间。calloc是可以指定初始化内容开辟堆区空间。realloc就是动态调整堆区申请的空间。当调整后的空间无法在原空间后扩容,则会将原空间的内容拷贝到新的空间上再申请到连续空间。
在这里插入图片描述

4.C++的动态内存管理

因为C++是兼容C语言的,所以C语言的动态内存管理依旧是可以在C++中使用的。由于C语言的动态管理方式存在缺陷,所以C++也提供了两个操作符来进行动态内存管理。分别是new 和 delete。

4.1.new和delete管理内置类型

int main()
{//cint* p1 = (int*)malloc(sizeof(int));free(p1);//cpp//new后面跟的是类型int* p2 = new int;delete p2;//malloc单纯开辟空间//new支持初始化int* p3 = new int(10);delete p3;int* p4 = (int*)malloc(sizeof(int) * 10);free(p4);int* p5 = new int[10];delete[] p5;//一定要带[]//开辟连续空间也是支持初始化的int* p6 = new int[10]{1,2,3,4};delete[] p6;}

需要注意的是使用malloc申请的内存就用free来释放,使用new申请的内存,就用delete来清理。不可以free来释放new的空间,或者用delete来释放malloc的空间。虽然在内置类型中,可能不会出问题,但是,这样使用是不规范也不正确的。这就好比吃面要用筷子,吃炸鸡要用手套,吃炒饭用勺子。你用free去释放new的内存,就好比用勺子去吃炸鸡,这样是不合适的。在某些场景下,你用free去释放new的内存就会好比用手套去吃火锅,那肯定是不行的。

4.1.new和delete操作自定义类型

struct ListNode
{//c++写法ListNode(int x):_next(nullptr), _val(x){}int _val;struct ListNode* _next;
};//c语言写法
struct ListNode* BuyNode(int x)
{struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->_next = NULL;newnode->_val = x;return newnode;
}int main()
{struct ListNode* n1 = BuyNode(1);struct ListNode* n2 = BuyNode(2);struct ListNode* n3 = BuyNode(3);free(n1);free(n2);free(n3);//struct升级成了类ListNode* nn1 = new ListNode(1);ListNode* nn2 = new ListNode(2);ListNode* nn3 = new ListNode(3);delete nn1;delete nn2;delete nn3;return 0;
}

new可以去调用自定义类型的构造函数来初始化对象,这样写比c语言的写法香多了。

class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}A(const A& aa){cout << "A(const A& aa)" << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};
int main()
{A* p1 = (A*)malloc(sizeof(A) * 4);A* p2 = new A[4]{1,2,3,4};free(p1);delete[] p2;return 0;
}

在这里插入图片描述
从上面样例可以看到,new自定义类型对象的时候,会去调用它的构造函数。当然这里是以整型值初始化自定义类型,会产生隐式类型转换,因为是在同一行编译器自动进行了优化,所以没有调用拷贝构造函数。而malloc只是单纯地开空间。delete自定义类型对象时,会去调用他的析构函数,而free只是单纯释放空间。

4.1.c++和c语言在开辟动态内存时失败处理细节

在C语言中,当使用malloc()函数开辟动态内存时,如果内存不足或者没有足够的连续空间,函数将返回NULL指针,表示内存分配失败。而在C++中,使用new操作符开辟动态内存时,如果内存不足或者没有足够的连续空间,将抛出一个std::bad_alloc异常,表示内存分配失败。

//C语言
int main()
{int* p1 = (int*)malloc(sizeof(int) * 1024);if (p1 == NULL){perror("malloc fail");exit(-1);}return 0;
}//C++
int main()
{int* p1 = nullptr;try{do{p1 = new int[1024];cout << p1 << endl;} while (p1);}catch (const exception& e){cout << e.what() << endl;}return 0;
}

在面向对象的编程语言的编程中,try-catch是一种异常处理机制。在try块中,我们编写可能引发异常的代码。如果在try块中的代码引发了异常,程序会立即跳转到与其对应的catch块。catch块定义了异常处理程序,它会处理try块中引发的异常。这里稍微了解即可,具体细节得等到后面学习后再和大家介绍了。

5.new和delete的原理

5.1.operator new与operator delete函数的介绍

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid* p;while ((p = malloc(size)) == 0){if (_callnewh(size) == 0){// report no memory// 如果申请内存失败了,这里会抛出bad_alloc 类型异常static const std::bad_alloc nomem;_RAISE(nomem);}return (p);
}

operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。

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)

operator delete:通过调用free来释放空间,而且operator delete和宏函数free的底层都是调用_free_dbg来进行释放空间的

5.2.operator new、new、operator delete和delete函数的底层实现

首先,通过下面代码的汇编代码来看看operator new、new、operator delete和delete函数究竟是怎么样的吧。

class A
{
public:A(int a = 0,int b = 0): _a(a){cout << "A():" << this << endl;}A(const A& aa){cout << "A(const A& aa)" << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};int main()
{A* p1 = (A*)operator new(sizeof(A));A* p2 = new A(1,1);operator delete(p1);delete p2;return 0;
}

在这里插入图片描述
在这里插入图片描述
通过查看汇编代码我们可以看到,new的会先调用operator new开辟空间,然后在调用构造函数初始化。delete会先调用析构函数进行清理,然后调用operator delete来释放动态内存。当然用new 自定义类型[N] 个对象,会先调用operator new开辟空间,然后在调用N次构造函数初始化。delete[] 会先调用N次析构函数,然后释放动态内存。

在这里插入图片描述

下面通过一个比较复杂的场景来看一下new和delete对于自定义类的的处理

typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){cout << "Stack(size_t capacity = 3)" << endl;_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){cout << "~Stack()" << endl;if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}private:DataType* _array;int       _capacity;int       _size;
};int main()
{Stack* p1 = new Stack;delete p1;return 0;
}

在这里插入图片描述

6.定位new

定位new是new关键字的另一种用法,用于给已经分配好的堆区空间进行调用构造函数来初始化对象。

6.1.代码样例演示

class A
{
public:A(int a = 0,int b = 0): _a(a){cout << "A():" << this << endl;}A(const A& aa){cout << "A(const A& aa)" << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a;
};int main()
{A* p1 = (A*)malloc(sizeof(A));new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参p1->~A();//显示调用析构函数free(p1);return 0;
}

在这里插入图片描述

6.2.定位new的应用场景

在一些需要频繁申请堆区内存的程序中,通常需要提前开辟一个内存池,用于提高获取堆区内存的效率。而定位new就能将申请的内存通过调用构造函数来进行初始化。在后面学习的STL的链表中就能遇到这一场景。
在这里插入图片描述

7. malloc/free和new/delete的区别

1、malloc/free是库函数。new和delete是操作符。
2、对于内置类型来说malloc/free和new/delete的区别不是很大,对于自定义类型new/delete分别会去调用构造函数/析构函数。来进行初始化和清理工作。
3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可。
4. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常。

8.内存泄漏的概念

8.1.什么是内存泄漏

内存泄漏指的是在程序运行过程中,程序分配了一段内存空间,但在使用完这段内存空间后,没有及时释放掉这段内存,导致这段内存不能被再次使用,从而造成了内存空间的浪费。如果程序中存在内存泄漏问题,并且这种泄漏的情况不断累积,最终可能会导致程序所能使用的内存空间越来越小,甚至导致程序崩溃。内存泄漏是一种常见的编程错误,需要开发人员注意及时释放不再使用的内存空间,避免内存泄漏问题的出现。下面我通过一个样例来看看内存。

int main()
{char* p1 = (char*)malloc(1024 * 1024 * 1024);cout << p1 << endl;return 0;
}

在这里插入图片描述

8.2.内存泄漏的危害

对于客户端,内存泄漏的危害比较的小。对于服务端,内存泄漏的危害极大。因为想游戏服务、电商服务等服务端中,内存泄漏会导致服务程序宕机,进而导致软件业务的事故。所以我们程序员在操作动态内存时,一定要注意在哪里申请了就在对应的位置释放。内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

相关文章:

learn C++ NO.7——C/C++内存管理

引言 现在是5月30日的正午&#xff0c;图书馆里空空的&#xff0c;也许是大家都在午休&#xff0c;也许是现在37摄氏度的气温。穿着球衣的我已经汗流浃背&#xff0c;今天热火战胜了凯尔特人&#xff0c;闯入决赛。以下克上的勇气也激励着我&#xff0c;在省内垫底的大学中&am…...

SDUT数据库原理——第十章作业(参考答案)

1. 简述使用检查点方法进行数据恢复的一般步骤。 答: (1)使用检查点方法进行数据恢复,首先从重新开始文件(见P302页图10.3)中找到最后一个检查点记录在日志文件中的地址,由该地址在日志文件中找到最后一个检查点记录。 (2)由该检查点记录得到检查点建立时刻所有正在…...

My Note of Diffusion Models

Diffusion Models Links: https://theaisummer.com/diffusion-models/ Markovian Hierachical VAE rvs: data: x 0 x_{0} x0​,representation: x T x_{T} xT​ ( p ( x 0 , x 1 , ⋯ , x T ) , q ( x 1 , ⋯ , x T ∣ x 0 ) ) (p(x_0,x_1,\cdots,x_T),q(x_1,\cdots,x_{T…...

【P37】JMeter 仅一次控制器(Once Only Controller)

文章目录 一、仅一次控制器&#xff08;Once Only Controller&#xff09;参数说明二、测试计划设计2.1、测试计划一2.1、测试计划二 一、仅一次控制器&#xff08;Once Only Controller&#xff09;参数说明 可以让控制器内部的逻辑只执行一次&#xff1b;单次的范围是针对某…...

cleanmymac要不要下载装机?好不好用

当我们收到一台崭新的mac电脑&#xff0c;第一步肯定是找到一款帮助我们管理电脑运行的“电脑管家”&#xff0c;监控内存运行、智能清理系统垃圾、清理Mac大文件旧文件、消除恶意软件、快速卸载更新软件、隐私保护、监控系统运行状况等。基本在上mac电脑防护一款CleanMyMac就够…...

DNS风险分析及防护研究(五):常见的DNS威胁与防御(中科三方)

DNS是互联网运行重要的基础设施&#xff0c;在全球互联网运转中扮演重要作用。互联网中的每一次访问都开始于一次DNS查询&#xff0c;从而将人们更好辨识的域名转换为数字化的IP地址。随着互联网的快速发展以及网络技术的快速发展&#xff0c;DNS固有的缺陷逐步暴露出来&#x…...

使用geoserver发布shp和tiff数据

一、安装并启动geoserver服务 1.1 下载geoserver 进入官网下载 由于geoserver是使用Java语言开发的&#xff0c;所以运行需要java的环境&#xff0c;不同geoserver的版本号对java的版本要求不同&#xff0c;所以选择版本时需注意对应java的版本要求&#xff0c;由于我本地安…...

谷歌周彦祺:LLM浪潮中的女性科学家多面手丨智源大会嘉宾风采

导读 大模型研发竞赛如火如荼&#xff0c;谷歌紧随OpenAI其后推出PalM2、Gemini等系列模型。Scaling Law是否仍然适用于当下的大模型发展&#xff1f;科技巨头与初创企业在竞争中各有哪些优势和劣势&#xff1f;模型研究者应秉持哪些社会责任&#xff1f; 2023智源大会「基础模…...

Burp模块

Target模块 记录流量 1.Target按主机或域名分类记录 2.HTTP History 按时间顺序记录且会记录很多次 3.Target模块的作用 &#xff08;1&#xff09;把握网站的整体情况 &#xff08;2&#xff09;对一次工作的域进行分析 &#xff08;3&#xff09;分析网站存在的攻击面 …...

sql笔记:SQL SERVER字符串填充(标量值函数创建、标量值函数调用)

/*字符串填充 ,如果返回 -1 说明输入参数有错误*/ CREATE FUNCTION [dbo].[uf_pad_string] ( @string_unpadded VARCHAR(100), --123填充前字符串 @pad_char VARCHAR(1), --0 填充的字符串 @pad_count tinyint, --10 填充后字符串长度 @pad_p…...

python使用hTTP方法

Python中可以使用requests库来发送HTTP请求&#xff0c;其中包括GET、POST、PUT、DELETE等方法。下面是一个使用requests库发送HTTP请求的示例&#xff1a; python import requests # 发送GET请求 response requests.get(Example Domain) # 发送POST请求 data {key1: valu…...

JavaSE常用API

1. Math.round(11.5)等于多少&#xff1f;Math.round(- 11.5) 又等于多少? Math.round(11.5)的返回值是 12&#xff0c;Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5然后进行取整。 2. switch 是否能作用在 byte 上&#xff0c;是否能作用在 long 上…...

华为OD机试之模拟商场优惠打折(Java源码)

模拟商场优惠打折 题目描述 模拟商场优惠打折&#xff0c;有三种优惠券可以用&#xff0c;满减券、打折券和无门槛券。 满减券&#xff1a;满100减10&#xff0c;满200减20&#xff0c;满300减30&#xff0c;满400减40&#xff0c;以此类推不限制使用&#xff1b; 打折券&…...

5月VR大数据:Quest 2下跌超1%,其它变化不大

Hello大家好&#xff0c;每月一期的VR内容/硬件大数据统计又和大家见面了。 想了解VR软硬件行情么&#xff1f;关注这里就对了。我们会统计Steam平台的用户及内容等数据&#xff0c;每月初准时为你推送&#xff0c;不要错过喔&#xff01; 本数据报告包含&#xff1a;Steam VR硬…...

CW32系列模数转换器(ADC)

模数转换器&#xff08;ADC&#xff09;的主要功能是将模拟量转换为数字量&#xff0c;方便MCU进行处理。下面以CW32L083为例介绍CW系列的模数转换器的特点和功能&#xff0c;并提供演示实例。 一、概述 CW32L083 内部集成一个 12 位精度、最高 1M SPS 转换速度的逐次逼近型模…...

电动力学专题:电磁场规范不变性与规范自由度

对称性&#xff0c;不变性&#xff0c;相对性&#xff0c;协变形 在现代物理学中常常被认为具有相同的含义&#xff08;好拗口&#xff09; 规范与规范的自由度 保证电磁场物理量不改变的情况下&#xff0c;有多组势可供选择&#xff0c;而每组势可以称为一个规范 规范不变性…...

max delay的应用场景与常见问题

max delay与min delay用来约束start points到endpoints点对点的路径长度,set_max_delay约束最大值,set_min_delay约束最小值。 max delay的-from和-to并不局限在get_pins,get_cells和get_clocks同样可以。 set_max_delay 5 -from UFF0/Q -to UFF1/D set_max_delay -from …...

非阻塞队列

非阻塞队列 首先我们要简单的理解下什么是非阻塞队列&#xff1a; 与阻塞队列相反&#xff0c;非阻塞队列的执行并不会被阻塞&#xff0c;无论是消费者的出队&#xff0c;还是生产者的入队。 在底层&#xff0c;非阻塞队列使用的是CAS(compare and swap)来实现线程执行的非阻塞…...

动力电池管理系统(BMS)

BMS技术 目录 BMS技术 一、BMS简介 二、BMS主要功能 1、参数检测 2、剩余电量&#xff08;SOC&#xff09;估计 3、充放电控制 4、热管理 5、均衡控制 6、故障诊断 7、信息监控 8、参数标定 9、CAN总线接口 三、BMS架构组成 1、BMS的拓扑架构 1、1集中式架构的B…...

ChatGPT桌面客户端支持gpt4模型,附使用说明

#软件核心功能&#xff1a; 1、支持OpenAI官方秘钥及API2D双秘钥使用&#xff1b;如果全局魔法&#xff0c;可以自己用官方秘钥&#xff1b;没魔法国内可直接使用API2D秘钥&#xff1b; 2、内置GPT4模型选项&#xff0c;如果你的官方秘钥支持可直接使用&#xff1b;你也可以注册…...

Vivado下时序逻辑模块的仿真

文章目录 D触发器两级D触发器带异步复位的D触发器带异步复位和同步置数的D触发器移位寄存器单口RAM伪双口RAM真双口RAM单口ROM 组合逻辑电路在逻辑功能上特点是任意时刻的输出仅仅取决于当前时刻的输入&#xff0c;与电路原来的状态无关。 时序逻辑在逻辑功能上的特点是任意时刻…...

ThreadLocal的使用方式

1. ThreadLocal的使用方式 (1) 在关联数据类中创建private static ThreadLocal 在下面的类中&#xff0c;私有静态 ThreadLocal 实例&#xff08;serialNum&#xff09;为调用该类的静态 SerialNum.get() 方法的每个 线程维护了一个“序列号”&#xff0c;该方法将返回当前…...

全面理解:C++中的指针和迭代器,以及解引用操作符(*)和箭头操作符(->)的用法

指针与迭代器的基础概念 指针&#xff1a; 指针是一种变量&#xff0c;其值为另一种类型的对象在计算机内存中的地址。你可以使用指针来直接访问和操作它指向的对象。指针的使用非常强大&#xff0c;但也很危险&#xff0c;因为你有可能错误地操作内存&#xff0c;这可能会导致…...

Vite 使用学习指南

Vite 的基本概念和特点 Vite 是什么&#xff0c;它的主要特点是什么 Vite 是一个基于 ES modules 的前端构建工具&#xff0c;它的主要特点包括&#xff1a; 快速的冷启动&#xff1a;Vite 采用了基于浏览器原生 ES 模块的开发模式&#xff0c;可以在开发时快速启动应用&…...

【算法训练(day6)】双指针模板

一.双指针算法的由来和使用场景 通常情况下我们可能会遇到在某些可遍历的集合中寻找满足某种性质的字串或元素。这时候我们采取暴力的思路就会面临多重循环。我们可以利用题目中所给的集合并利用其性质将多重循环降成一重循环。光用语言描述可能不太好理解。接下来看几个双指针…...

免费常用的API接口大全

免费常用的API接口大全 OPEN AI &#xff1a; ChatGPT 能够模拟人类的语言行为&#xff0c;与用户进行自然的交互。ChatGPT 可以用于处理多种类型的对话&#xff0c;包括对话机器人、问答系统和客服机器人等。它还可以用于各种自然语言处理任务&#xff0c;比如文本摘要、情感分…...

【HTML】第 2 节 - HTML 标签

欢迎来到博主 Apeiron 的博客&#xff0c;祝您旅程愉快 &#xff01; 时止则止&#xff0c;时行则行。动静不失其时&#xff0c;其道光明。 目录 1、缘起 2、标题标签 3、段落标签 4、文本格式化标签 5、图像标签 5.1、基本作用 5.2、属性 6、超链接标签 7、音频标…...

MATLAB算法实战应用案例精讲-【数模应用】残差检验(附Java、python和MATLAB代码)

目录 几个高频面试题目 线性回归残差是否一定满足正态分布? 一般情况 特殊情况...

初学Qt(Day03)

今天概览 今天的目标是写一个动态的彩虹灯 一开始是有思路的。只是写的过程中有太多小bug了&#xff0c;真的是防不胜防 我的思路是&#xff1a; 主界面是一个开始界面&#xff0c;点击开始按钮之后&#xff0c;有一个子界面出现&#xff0c;显示出彩虹灯转动的效果。 内部的执…...

皮卡丘xss之htmlspecialchars、xss之href输出、xss之js输出

1.xss之htmlspecialchars htmlspecialchars()函数的功能如下&#xff1a; htmlspecialchars() 函数把预定义的字符转换为 HTML 实体。 预定义的字符是&#xff1a; &#xff08;1&#xff09;& &#xff08;和号&#xff09;成为 &amp; &#xff08;2&#xff09;…...