C++动态内存管理 - new和delete

news2024/11/27 2:48:30

目录

开胃菜 - 浅析C/C++的内存分段

内存分段

各段说明

new和delete的基础用法

深度剖析new

定位new

浅析delete

malloc/free和new/delete的异同


开胃菜 - 浅析C/C++的内存分段

这部分是计算机系统相关的知识,这里只是先浅谈一下,可能有些内容会有冲突,但重在理解。

内存分段

从狭义上讲内存的分段可以分为堆、栈、数据段以及代码段(内存映射区比较复杂,暂不涉及),大致内容可以参考下图:

但其实细说的话其实并没有这么简单。在编译阶段,编译器生成的​目标文件(.o或.obj)中至少包括编译后的机器指令代码、数据,以及链接时所需要的的一些信息(比如符号表、调试信息、字符串等)。其中,目标文件将这些信息按不同的属性以“ 段 ”(segment)的形式存储。如下是一些我们经常会提到的段:

这里我们主要分析表格中常见的段信息,虽然还有其他段,例如注释信息段( .comment )等,但它并不是我们关注的重点。

​下图是一个目标文件的图例分析,主要分析代码和数据分别存放在哪些段中。图中并没有给出堆栈的信息,因为堆栈只有在程序被装载运行时才会分配内存,而在目标文件这个阶段还没有分配栈段的内存。而且堆栈相对来说比较熟悉,所以也就无伤大雅了。

图片来源: 实例说明代码段(.text)、数据段(.data)、bss段、只读数据段……

各段说明

堆和栈我们经常使用,这里就不多说了。堆区就是相当于直接在内存中进行操作,但实际上是在虚拟内存中操作的,并不会直接干扰物理内存。而栈则是程序运行时创建的栈帧。下面我们来介绍剩下数据区和代码区。

代码段:

程序的源代码(包括函数等)编译后的机器指令就会放在代码段,即 .text 段中。在经典的x86体系结构中,代码段通常包含了程序的函数、方法和一些固定的指令集等信息。

数据段:

数据段包括 .data 、 .bss 、 .rodata 三个部分,暂且认为程序中的全局变量和局部变量就是数据段。那么为什么数据段要分成 .data 、 .bss 、 .rodata 三个部分呢?

其主要因素有两个:是否占用内存空间、读写权限如何。

已初始化的全局变量和局部静态变量保存在 .data 段,未初始化的全局变量和局部静态变量一般放在 .bss 段。理论上讲,未初始化的全局变量也是应该放在.data段的,但由于它们都是0,所以为它们在.data段分配空间并且存放数据0也就没有必要了。其中,.bss 段并不会占用磁盘空间,只会在程序加载到内存时分配相应的空间。这种懒加载的特性可以节省可执行文件的大小,特别是当程序中包含大量未初始化的全局或静态变量时。
 .data段和 .bss 段中的都是可读写的数据,而 .rodata 存放的是只读数据,主要是一些const变量、字符串常量等。单独设立 .radata 段的好处是:在程序加载的时候可以将 .rodata 段的属性映射成只读,这样对这个段的任何修改操作都作为非法操作处理。另外在某些平台还可以将 .rodata 段存放在只读存储器,例如ROM,通过硬件保证只读。

那么为什么又要把 “代码段” 和 “数据段” 分开存放呢?

当程序被装载后,数据和指令分别被映射到两个虚拟内存区域。数据段对进程来讲是可读写的,而代码段对进程来说是只读的,所以这两个虚拟内存区域的权限可以被分别设置为可读写和只读,防止程序的指令被有意和无意地改写。
现代CPU的缓存一般被设计成数据缓存和指令缓存分离,程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
当系统中运行着多个该程序的副本时,例如多个线程同时都运行同一个程序,它们的代码段指令都是一样的,所以内存中只需要保存一份该程序的代码段,然后将每个副本进程的数据段区域分来,这样可以节省大量空间。

new和delete的基础用法

在C语言的时候我们在堆区申请内存时往往采用如下这种写法:

char* p = (char*)malloc(100 * sizeof(char));
// ...
free(p);

而在C++中往往采用如下的写法:

char* p = new char[100];
// ...
delete[] p;

这就是C++中new和delete关键字,具体用法格式如下:

// new的用法
new 类型名;
new 类型名(构造函数参数);
new 类型名{构造函数参数}; // C++11支持
// delete的用法
delete 指针;
delete[] 指针; // 对于数组的释放使用 delete[]

用法示例

// 隐式调用构造函数
MyClass* c1_x = new MyClass;
// 显示调用无参构造1
MyClass* c2_0 = new MyClass();
// 显示调用无参构造2
MyClass* c2_1 = new MyClass{};
// 显示调用单参构造1
MyClass* c3_0 = new MyClass(10);
// 显示调用单参构造2
MyClass* c3_1 = new MyClass{ 10 };
// 显示调有单参构造1
MyClass* c4_0 = new MyClass(10, 20);
// 显示调有单参构造2
MyClass* c4_1 = new MyClass{ 10,20 };
// 显示调用有参构造 - 数组
MyClass* c = new MyClass[5]{ {10,20},{20,30},{30,40} }; //部分初始化
// 数组无法这样初始化,只能上面这样初始化
//MyClass* c = new MyClass[5]( {10,20},{20,30},{30,40} ); //error

注意事项

  1. C++之所以要引入new和delete,一个好处是可以简化写法。但更重要的原因是为了适配C++中的自定义类型。
  2. 在C++中malloc和free等是不会自动调用构造和析构的,而new和delete可以自动调用构造和析构。
  3. malloc等函数申请空间失败时返回NULL,而new的申请空间失败会抛异常(抛异常更符合C++的胃口)
  4. delete释放单个数据,delete[]释放数组,要注意匹配使用。因为delete和delete[]在底层的实现机制是不一样的,如果不匹配使用会导致很危险的未定义行为。

深度剖析new

在使用new运算符时,它会执行以下步骤:

  1. 调用operator newnew运算符首先会调用operator new函数,用于在堆上分配内存。operator new函数返回一个指向未初始化内存块的指针。这一步只涉及内存的分配,还没有创建对象。

  2. 构造对象:接下来,new运算符会调用相应类型的构造函数来在已分配的内存上创建对象。这一步才是创建对象的过程。构造函数会对对象进行初始化,根据构造函数的逻辑,可能会分配额外的资源或设置对象的初始状态。

  3. 返回指针:最后,new运算符返回指向已创建对象的指针,使得我们可以在代码中使用这个指针来访问和操作新创建的对象。

简言之,new运算符的操作包含内存分配和对象构造两个步骤,确保对象在正确初始化的情况下在堆上动态创建。特别的,在使用new运算符创建数组时,步骤中的第二步会重复调用相应类型的构造函数来创建数组中的每个元素。

new和operator new

也许看到new会调用operator new的时候感到很懵逼,但实际上new和operator new是两回事:new是一个语法糖运算符,它将申请空间与调用构造一并完成;而operator new是一个用于动态申请内存空间的函数,它的内部实际上就是封装了malloc等函数,只不过operator new在申请内存空间失败时会抛异常,这更符合C++的胃口。

(new的相关源代码:gcc/libstdc++-v3/libsupc++/new_op.cc (github.com))

下面是operator new的相关的介绍:

在C++中,operator new 函数是用于动态分配内存的重要函数。它用于申请一块内存来存储对象或数据。而且,new运算符在背后会调用operator new函数来完成内存分配,并在构造对象时调用构造函数。

operator new 的函数定义如下:

void* operator new (std::size_t size);

其中size参数是所需内存块的字节数。该函数返回一个指向已分配内存的指针,或者在无法满足分配请求时,抛出std::bad_alloc异常。

在使用new运算符分配对象时,例如:

MyClass* ptr = new MyClass;

那么在底层,它被转换为:

MyClass* ptr = static_cast<MyClass*>(operator new(sizeof(MyClass)));
/* 编译器为我们隐式调用构造函数,并构造对象 */

如果需要自定义内存分配行为,可以重载全局的或类的operator new函数。

定位new

我们知道,C++可以显示调用析构,但是并不允许显示调用构造函数。但其实还有一种可以看作是显示调用构造的语法:定位new

定位new(placement new)是new运算符的变体,定位new的作用是在已分配空间的内存块上构造对象。定位new的语法格式如下:

// 隐式调用构造函数 
new (place_address) type 
// 显示调用构造函数 
new (place_address) type(initializer-list) 
// 列表初始化 C++11 
new (place_address) type{initializer-list} 

/* 注释:
    place_address - 指针或是一块合法的内存地址
    initializer-list - 初始化列表
*/

定位new的使用场景

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

用法示例:
        我们知道,malloc等函数只是单纯的动态分配空间,所以我们可以将malloc分配的内存用定位new显示的构造一个对象。可以说,某种意义上定位new可以理解成显示的调用构造函数进而构造出一个对象。demo如下:

#include <iostream>
using namespace std;

class value
{
public:
    value(int val = 0) : _val(val) {}
    void show_val()
    {
        cout << _val << endl;
    }
    ~value()
    {
        cout << _val << ": 析构" << endl;
    }
private:
    int _val;
};

int main() 
{
    // 先分配内存(堆区栈区都可以)得到指针
    value* p1 = (value*)malloc(sizeof(value));
    value* p2 = (value*)malloc(sizeof(value));
    value* p3 = (value*)malloc(sizeof(value));

    new(p1)value;         // 隐式调用无参构造
    new(p2)value(10);     // 显示调用有参构造
    new(p3)value{ 20 };   // 列表初始化,C++11支持

    p1->show_val();
    p2->show_val();
    p3->show_val();
    cout << endl;

    delete p1;
    delete p2;
    delete p3;
    cout << endl;

    return 0;
}

浅析delete

delete和new一样,也是一个被封装好的运算符。不同的是,new运算符先通过operator new函数申请空间,然后调用构造函数。而delete则是先调用析构函数然后再调用operator delete函数。

我们知道operator new运算符是封装了malloc函数,而operator delete同理也是封装了free函数。

(delete的相关源代码:gcc/libstdc++-v3/libsupc++/del_op.cc (github.com))

malloc/free和new/delete的异同

共同点是:

都是从堆上申请空间,并且需要用户手动释放。

不同的地方是:

  1. malloc和free是函数,new和delete是运算符。
  2. new相较malloc写法上更简便。
  3. malloc申请的空间不会初始化,new申请空间的同时也会构造对象。
  4. 同理,free不会调用析构,但delete运算符是先调用析构再free当前对象。
  5. malloc的返回值为void*, 在使用时必须强转,new则不需要。
  6. malloc申请空间失败时,返回的是NULL,因此使用时需要判空。new申请空间失败时并不返回NULL,而是抛异常,因此new需要捕获异常。
  7. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数。而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

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

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

相关文章

初学绘画就上手!7款AI绘图免费软件精选推荐!

本篇文章将分享7款个人非常喜爱的AI绘图免费软件&#xff0c;一起来看看吧。 即时灵感 即时灵感 " 是一款全新升级的 AI 绘画工具&#xff0c;为创作者们提供了丰富多样的创作体验和强大功能。这个创新工具旨在让创作过程更加简单、便捷&#xff0c;同时提供高质量的作品…

Jenkins+Nginx+vue

安装nodejs 在这里插入图片描述 echo off xcopy C:\ProgramData\Jenkins\.jenkins\workspace\super_manage_vue\dist F:\java\www\super_manage_vue\ /s /e /y echo 复制文件完成 exit安装niginx 配置文件如下 #user nobody; worker_processes 1;#error_log logs/error.lo…

反射调用private方法的坑

使用反射调用私有方法时&#xff0c;发现空指针异常&#xff0c;无法直接注入导致空指针异常 加入如下代码后&#xff0c;恢复正常 if (AopUtils.isCglibProxy(marketSmsTaskService)) {// 如果是cglib代理对象&#xff0c;则转为原始对象marketSmsTaskService (MarketSmsTas…

与身边的人有差距,我选择只跟自己比并发展自己的长处!

文章目录 方向一&#xff1a;简述自己的感受方向二&#xff1a;聊聊你想怎么做方向三&#xff1a;如何调整自己的心态 虽然清楚知识需要靠时间沉淀&#xff0c;但在看到自己做不出来的题别人会做&#xff0c;自己写不出的代码别人会写时还是会感到焦虑怎么办&#xff1f; 你是…

链表OJ题

题目一&#xff1a;206反转链表 题目要求&#xff1a; 思路分析&#xff1a; 代码实现&#xff1a; struct ListNode* reverseList(struct ListNode* head){ //n1,n2反转指针 n3链接下一个结点的指针struct ListNode* n1,*n2,*n3;n1 NULL;n2 head;if(n2)//有可能head本身…

CentOS7 启动谷歌浏览器 java+Selenium+chrome+chromedriver

前言&#xff1a;自己想使用该技术实现自动化抓取音乐&#xff0c;目前在window上运行成功&#xff0c;需要在Linux Centos服务上跑&#xff0c;配置上出现了许多问题&#xff0c;特此记录。 参考文档&#xff1a;CentOS7 安装Seleniumchromechromedriverjava_远方丿的博客-CSD…

PLC拉格朗日插值(SCL、ST计算源代码)

插值是对函数进行近似的基本方法,这篇博客主要介绍常用的拉格朗日插值法, Lagrange插值法不太清楚的同学,可以看看数值计算和分析类书籍,网上有很多C语言的拉格朗日插值算法,这里我们主要给出在PLC里利用ST,SCL语言完成拉格朗日插值计算。 1、拉格朗日插值FC 插值法可以…

TSINGSEE青犀视频安防监控视频平台加密机的详细使用与操作步骤

我们在此前的文章中介绍过不少TSINGSEE青犀视频安防监控视频平台关于加密机授权操作及相关疑问解答&#xff0c;感兴趣的用户可以翻阅往期的文章进行了解。由于新用户咨询该方面的问题较多&#xff0c;今天我们再来介绍一下操作步骤。 注意&#xff1a;加密机是依据IP授权&…

第三章-运输层

运输层服务 运输层协议为运行在不同主机上的进程之间提供逻辑通信&#xff0c;即从应用程序角度看两个主机好像直连一样&#xff0c;实际可能相隔万里运输层协议是在端系统上实现的&#xff0c;而不是路由器&#xff0c;为什么这么强调&#xff0c;因为运输层会将应用报文划分…

ctfshow web93-98

web93 打开环境是一个代码审计题目 简单分析就是输入一个变量num&#xff0c;其值不能等于4476与包含字母&#xff0c;但是他的值需要为4476 函数intval作用为获取变量的整数值&#xff0c;第二个参数的意思是进制&#xff0c;默认为10进制。题目参数为0&#xff0c;就根据变…

【数据结构与算法】力扣:完全二叉树的节点个数+平衡二叉树

完全二叉树的节点个数 给你一棵完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。 完全二叉树的定义如下&#xff1a;在完全二叉树中&#xff0c;除了最底层节点可能没填满外&#xff0c;其余每层节点数都达到最大值&#xff0c;并且最下面一层的节点都集中在该层最…

本地搭建web服务器、个人博客并发布公网访问

文章目录 前言1. 安装套件软件2. 创建网页运行环境 指定网页输出的端口号3. 让WordPress在所需环境中安装并运行 生成网页4. “装修”个人网站5. 将位于本地电脑上的网页发布到公共互联网上 前言 在现代社会&#xff0c;网络已经成为我们生活离不开的必需品&#xff0c;而纷繁…

YOLOv5项目调试与实战

拥有青春的时候 你就要感受它 不要浪费你的黄金时代 把宝贵的内在生命活出来 什么都别错过 一、项目介绍与环境配置 github地址 选择5.0版本的tag&#xff0c;并下载源码 使用Pycharm打开代码 选择解释器&#xff0c;我选择的是之前conda创建的pytorch环境 安装项目所需要用到…

map 比较(两个map的key,value 是否一样)

1. 用equals 比较 public static void main(String[] args) {List<Map<String,Object>> list new ArrayList<>();Map<String,Object> map1 new HashMap<>();map1.put("name","郭");map1.put("objId","1&quo…

Linux jq 命令讲解与实战操作(json字符串解析工具)

文章目录 一、概述二、jq 命令安装三、jq 命令语法与示例详解1&#xff09;基本用法2&#xff09;常用选项3&#xff09;查询和过滤1、选择字段2、过滤3、遍历数组4、组合操作 4&#xff09;修改和创建1、修改字段值&#xff1a;2、创建新字段&#xff1a;3、组合操作&#xff…

视频基础知识简介

视频 由一组图像组成&#xff0c;且图像一般为了方便传输或者减少占用空间而被压缩(如H264,265等格式)&#xff0c;最终在显示设备上进行显示。 图像 图像由像素组成&#xff0c;像素又是由RGB组成&#xff0c;每个点的颜色由红绿蓝进行组合形成新的颜色&#xff1b; 分辨率…

[Flask]SSTI1

根据题目提示&#xff0c;这关应该是基于Python flask的模版注入&#xff0c;进入靶场环境后就是一段字符串&#xff0c;而且没有任何提示&#xff0c;有点难受&#xff0c;主要是没有提示注入点 随机尝试一下咯&#xff0c;首先尝试一下guest&#xff0c;GET传参 但是没有反应…

基于Java+SpringBoot+Vue的篮球论坛系统设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

第2章 图像变换

第2章 图像变换 **写得比较粗错&#xff0c;详解后续跟上。**第2章 图像变换2.1 反色变换2.1.1 基本原理2.1.2 源码2.1.3 样例 2.2 线性变换2.2.1 基本原理2.2.2 源码2.2.3 样例加入滚动条 2.3 对数变换2.3.1 基本原理2.3.2 源码2.3.3 样例加入滚动条 2.4 gamma变换2.4.1 基本原…

锐捷VSU技术理论与实验

目录 VSU涉及的相关基础概念 VSU的2种工作模式 VSU的3种设备角色 VSU的4种设备状态 VSU的分裂与合并 VSU建立过程 双主检测 VSU报文转发原理 VSU命令配置 配置VSU 配置双主检测 VSU涉及的相关基础概念 域编号&#xff08;Domain ID&#xff09; Domain ID是VSU的标…