C++内存管理(2)new、delete详解

2023-11-12

目录

new operator(new操作)

new类对象时加不加括号的差别

new工作任务

delete工作任务

new和delete 堆区空间操作(对比malloc和free)

new和delete操作基本类型的空间

new和delete操作基本类型的数组

new和delete操作类的空间

new和delete操作对象数组

new内存分配细节探秘

为什么要尽可能少的调用malloc?

new和delete的重载

为什么要重载 new

监测内存创建销毁,统计和监控泄漏

内存对齐的处理

特定应用:多进程内存共享

重载全局的 new 和 delete

在全局new和delete中添加定制行为

重载类的操作符 new 和 delete

类new和delete操作符重载基础

对齐的内存分配

共享内存的分配

定位new(placement new)

功能

使用placement new

placement new对象的销毁

placement new的应用

硬件编程

实现基础库

多种版本的operator new重载


new operator(new操作)

new类对象时加不加括号的差别

  • A *pa = new A;//有无构造函数初始化为垃圾值
  • A *pa2 = new A();//无构造函数初始化为0,有构造函数为垃圾值

在g++中默认初始化成员变量为0,而A *pa2 = new A(5)初始化成员变量为5

#include <iostream>
using namespace std;

class A
{
public:
    int m_num;

public:
    A(){};
    A(int num) : m_num(num)
    {
        cout << "construct" << endl;
    }
    ~A()
    {
        cout << "disconstruct" << endl;
    };
};

int main(int argc, char const *argv[])
{
    A *pa = new A;
    A *pa2 = new A();
    A *pa3 = new A(5);

    cout << "pa->m_num = " << pa->m_num << endl;
    cout << "pa2->m_num = " << pa2->m_num << endl;
    cout << "pa3->m_num = " << pa3->m_num << endl;

    delete pa;
    delete pa2;
    delete pa3;
    return 0;
}

运行结果:

new工作任务

调用operator new()--malloc

调用了分配对象的构造函数

delete工作任务

调用了分配对象的析构函数

调用operator delete()--free

new和delete 堆区空间操作(对比malloc和free)

new和delete操作基本类型的空间

new和malloc delete和free 没有区别

区别:

new 不用强制类型转换

new在申请空间的时候可以 初始化空间内容

new和delete操作基本类型的数组

new和delete操作类的空间

malloc不会调用构造函数 free不会调用析构函数

new 会调用构造函数 delete调用析构函数

new调用有参构造

new和delete操作对象数组

new内存分配细节探秘

  • new分配内存实际是调用malloc函数进行内存分配;
  • 思考:delete/free是如何知道要释放多大的内存?
    • 分配内存时,为了记录和管理分配出去的内存,额外多分配了不少内存,造成了浪费;尤其是你频繁的申请小块内存时,造成的浪费更明显,更严重
    • 实际分配情况

为什么要尽可能少的调用malloc?

  • 内存开销: 每次调用 malloc 都会引入额外的内存开销,包括内存分配表、堆管理等数据结构,这些开销可能会在大量小型分配时累积并消耗大量内存。
  • 内存泄漏风险: 使用 malloc 分配内存后,需要负责在不再使用内存时释放它。如果你频繁地调用 malloc,则需要管理和追踪许多不同的内存分配,容易出现内存泄漏问题,导致程序在运行时逐渐耗尽内存。
  • 性能开销: 内存分配和释放是相对较慢的操作,涉及到内部数据结构的维护、内存搜索等操作。频繁调用 malloc 可能会导致性能下降,特别是在大规模数据处理或高性能计算应用中。
  • 碎片化: 频繁分配和释放小块内存可能导致内存碎片化,即使系统总内存充足,也可能由于碎片化问题无法满足大块内存分配的需求。

为了减少 malloc 调用的次数,可以考虑以下方法:

  • 使用栈内存: 对于小型临时变量,可以使用栈内存而不是堆内存,因为栈内存的分配和释放非常快速。但要注意栈内存的生命周期通常较短。
  • 池化: 如果需要频繁创建和销毁对象,可以使用内存池技术,通过一次性分配一大块内存并自行管理对象的分配和释放。
  • 缓存: 对于某些可复用对象,可以使用缓存来避免频繁分配和释放内存。这在对象池等场景中很有用。
  • 避免不必要的动态分配: 如果可以在编译时确定数组或数据结构的大小,可以使用栈数组或静态分配来避免动态分配。

new和delete的重载

为什么要重载 new

监测内存创建销毁,统计和监控泄漏

在C++中,内存管理是开发者的一项重要责任,也是容易出错的地方。开发者可能会遗忘释放已分配的内存,导致内存泄漏。重载new和delete可以帮助开发者更好地追踪和管理内存分配。通过在重载的new和delete操作符中插入日志或者调试语句,开发者可以监测和记录所有内存分配和释放的情况,从而检测内存泄漏。

例如,以下的代码展示了如何重载new和delete操作符来监测和追踪内存分配:

void* operator new(size_t size) {
    void* p = malloc(size);
    std::cout << "Allocated " << size << " bytes at address " << p << std::endl;
    return p;
}

void operator delete(void* p) {
    std::cout << "Deallocated memory at address " << p << std::endl;
    free(p);
}

内存对齐的处理

在一些硬件平台和操作系统上,为了实现最优性能,数据需要按照某种特定的边界对齐。如果没有对齐,可能会导致性能下降,甚至运行错误。通过重载new和delete,我们可以为特定的类实现定制的内存对齐方式。

下面的代码演示了如何重载new和delete操作符来实现内存对齐:

class Aligned {
public:
    static void* operator new(std::size_t size) {
        void* p = std::aligned_alloc(alignof(Aligned), size);
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    static void operator delete(void* p) {
        std::free(p);
    }
};

特定应用:多进程内存共享

在某些情况下,多个进程可能需要访问同一块内存区域。在这种情况下,可以通过重载new和delete操作符,实现在共享内存区域中分配和释放对象。

例如,以下的代码展示了如何通过重载new和delete来在共享内存中分配和释放对象:

// 假设SharedMemoryManager是一个用于管理共享内存的类
class SharedMemoryManager {
public:
    void* allocate(size_t size);
    void deallocate(void* p);
};

class SharedMemoryObject {
public:
    void* operator new(size_t size) {
        return SharedMemoryManager::allocate(size);
    }

    void operator delete(void* p) {
        SharedMemoryManager::deallocate(p);
    }
};

在以上的例子中,SharedMemoryObject类的对象将会被分配在共享内存中,从而可以被多个进程访问。

重载全局的 new 和 delete

全局的new和delete操作符可被重载以满足特定的需求,比如定制内存管理策略,或者为内存分配和释放添加自定义行为。要注意,这些全局重载将影响到整个程序的范围,包括标准库的容器等,所以在实践中应谨慎使用。

void* operator new(size_t size) {
    // ... 实现代码
}

void operator delete(void* p) {
    // ... 实现代码
}

operator new需要返回一个足够大,可以容纳请求内存大小的指针。如果内存分配失败,需要抛出std::bad_alloc异常。operator delete需要释放传入的指针指向的内存。

在全局new和delete中添加定制行为

下面的代码将在全局的new和delete操作符中添加一些定制的行为。在分配和释放内存时,我们会打印一些信息到控制台,以便于跟踪内存的使用情况:

#include <cstdlib>
#include <iostream>

void* operator new(size_t size) {
    void* p = std::malloc(size);
    if (!p) {
        throw std::bad_alloc();
    }

    std::cout << "Allocated " << size << " bytes at address " << p << std::endl;
    return p;
}

void operator delete(void* p) {
    std::cout << "Deallocated memory at address " << p << std::endl;
    std::free(p);
}

以上代码演示了如何在全局的new和delete操作符中添加自定义的行为。这种方式在实际开发中可以帮助我们更好地理解和跟踪内存的使用情况。不过请注意,添加过多的日志可能会对性能产生影响,所以在生产环境中通常不会添加过多的日志信息。

重载类的操作符 new 和 delete

对类的new和delete操作符进行重载允许我们为该类的对象提供定制的内存管理策略。这对于需要进行特殊内存管理的类来说特别有用,例如需要在共享内存中创建的对象,或者需要进行特殊对齐的对象。

类new和delete操作符重载基础

对类的new和delete操作符进行重载的基本形式如下:

class MyClass {
public:
    static void* operator new(std::size_t size);
    static void operator delete(void* p);
};

operator new需要返回一个足够大,可以容纳请求内存大小的指针。如果内存分配失败,需要抛出std::bad_alloc异常。operator delete需要释放传入的指针指向的内存。

对齐的内存分配

假设我们有一个需要8字节对齐的类,我们可以通过重载new和delete操作符来满足这个要求:

#include <cstdlib>
#include <new>

class Aligned {
public:
    static void* operator new(std::size_t size) {
        void* p = std::aligned_alloc(8, size);
        if (!p) {
            throw std::bad_alloc();
        }
        return p;
    }

    static void operator delete(void* p) {
        std::free(p);
    }
};

在这个例子中,我们使用了std::aligned_alloc函数来进行对齐的内存分配。如果分配失败,我们抛出std::bad_alloc异常。在operator delete中,我们简单地调用std::free来释放内存。

共享内存的分配

假设我们有一个需要在共享内存中创建的对象,我们可以通过重载new和delete操作符来实现:

// 假设SharedMemoryManager是一个用于管理共享内存的类
class SharedMemoryManager {
public:
    static void* allocate(std::size_t size);
    static void deallocate(void* p);
};

class SharedMemoryObject {
public:
    static void* operator new(std::size_t size) {
        return SharedMemoryManager::allocate(size);
    }

    static void operator delete(void* p) {
        SharedMemoryManager::deallocate(p);
    }
};

在这个例子中,SharedMemoryObject类的对象将会在共享内存中创建和销毁。这允许我们在多个进程间共享这些对象。

定位new(placement new)

放置new (placement new) 是一个特殊版本的new操作符,它允许程序员将对象创建在已经分配的内存上。换句话说,它允许我们"放置"一个新的对象在我们指定的、已经存在的内存位置上。

功能

在已经分配的原始内存中初始化一个对象;

  • 已经分配,定位new并不分配内存,你需要提前将这个定位new要使用的内存分配出来
  • 初始化一个对象(初始化一个对象的内存),调用这个对象的构造函数不再分配内存;

使用placement new

在普通的new操作中,首先会申请一块足够的内存,然后在这块内存上构造对象。但是在placement new中,内存必须已经存在,它只负责在指定的内存上构造对象。以下是一个使用placement new的例子:

#include <new> // 需要包含这个头文件来使用placement new

char buffer[1024]; // 预分配的内存

int* p = new (buffer) int(123); // 在buffer上放置一个int对象

对于类,placement new最好在我们需要使用的类中重载,否则在类外重载会影响到其它类型分配空间

#include <iostream>
using namespace std;

void *operator new(size_t size)
{
    void *p = malloc(size);
    std::cout << "Allocated " << size << " bytes at address " << p << std::endl;
    return p;
}

void operator delete(void *p)
{
    std::cout << "Deallocated memory at address " << p << std::endl;
    free(p);
}

class A
{
public:
    int m_num;

public:
    A(){cout<<"default construct"<<endl;};
    A(int num) : m_num(num)
    {
        cout << "construct" << endl;
    }
    ~A()
    {
        cout << "disconstruct" << endl;
    };
    void *operator new(size_t size, void *p)
    {
        cout << "placement new" << endl;
        return p;
    }
};

int main(int argc, char const *argv[])
{
    void *p = (void *)new char[sizeof(A)];
    A *pa = new (p) A();

    pa->m_num = 5;
    cout << *((int *)p) << endl;
    delete pa;
    return 0;
}

placement new对象的销毁

由于placement new仅仅在已经分配的内存上创建对象,而不会分配内存,所以当不再需要这个对象时,我们需要手动调用该对象的析构函数来销毁对象:

p->~int(); // 手动调用析构函数

需要注意的是,这里只销毁了对象,但并没有释放内存。内存的释放需要根据实际的情况来处理。例如,如果这块内存是在栈上分配的,那么当退出作用域时会自动释放;如果是在堆上分配的,那么可能需要手动释放。

placement new的应用

placement new的一个主要应用是当我们需要在特定的位置创建对象时,比如在已分配的堆内存上,或者在栈内存上,甚至在硬件指定的特定内存地址上。

此外,placement new也常用于实现自定义的内存池,内存池可以减少动态分配和释放内存带来的开销。我们可以预先分配一大块内存,然后通过placement new在这块内存上创建对象,从而提高内存使用的效率。

硬件编程

如果知道了硬件设备的地址,想要将那个硬件设备与一个C++类直接关联,那么定位new就非常有效了

通过将placement new可以将C++的类之间关联到硬件设备上,操作该对象就相当于操作硬件

如下面程序所示,假如操作STM32的GPIOB->GPIO_Pin1,假设GPIO_Pin1的存储器映射地址为0x00005600。由于类A的对象pa的地址就是对象pa内首个字段m_num的地址,因此操作m_num就相当于操作地址0x00005600。

#include <iostream>
using namespace std;

class A
{
public:
    int m_num;

public:
    A(){};
    A(int num) : m_num(num)
    {
        cout << "construct" << endl;
    }
    ~A()
    {
        cout << "disconstruct" << endl;
    };
};

int main(int argc, char const *argv[])
{
    //访问硬件:将C++的类之间关联到硬件设备上,操作该对象就相当于操作硬件
    //单片机/STM32/ARM9:操作硬件的物理地址就相等于操作该硬件
    //GPIOB->GPIO_Pin1
    void *p = (void*)0x00005600;
    A *pa = new(p) A();
    pa->m_num = 1;//拉高电平
    pa->m_num = 0;//拉低电平

    return 0;
}

实现基础库

基础库一般为了效率要先预分配内存,然后在预分配的内存上执行构造,几乎所有的C++容器都用到了定位new

多种版本的operator new重载

优先级:内部new、全局new

可以重载很多版本的operator new,只要每个版本参数不同就行,但是第一个参数是固定的,都是size_t,表示要new对象的sizeof值

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++内存管理(2)new、delete详解 的相关文章

随机推荐

  • python统计字典里面value出现的次数_python 统计list中各个元素出现的次数的几种方法...

    利用字典dict来完成统计 举例 a 1 2 3 1 1 2 dict for key in a dict key dict get key 0 1 print dict 输出结果 gt gt gt 1 3 2 2 3 1 利用Python
  • 【WINDOWS / DOS 批处理】for命令详解(六)

    for命令详解 一 共十篇 for命令详解 二 共十篇 for命令详解 三 共十篇 for命令详解 四 共十篇 for命令详解 五 共十篇 for命令详解 六 共十篇 for命令详解 七 共十篇 for命令详解 八 共十篇 for命令详解
  • urlrewrite(伪静态技术运用2之outbound-rule)

    问题 页面通过 servlet detail magid 520 categoryId 205 来访问一个servlet 这个链接在IE地址栏上给人家看起来很不舒服 想把它伪装一下变成别的地址 例如想把它伪装成 servlet detail
  • 2019最新Web前端经典面试试题及答案-史上最全前端面试题(含答案)

    近期总结一一些面试题 都是企业的面试题笔记题 感觉薪资10k下的都会出笔试题 特别高的薪资都是直接技术面试或者是 现场编程 总结很多人的面试题 后期会对于单个知识点再说笔记详细讲解 部分都是百度的答案 不是特全面的 可以自己找 红色为常见面
  • vue 父子组件(个人笔记)

    目录 父组件使用子组件 父子组件之间传参 props 子获父 ref 子获父 this parent 子获父 emit 父获子 使用this emit 自定义事件名 所需要传的值 this children 父获子 父组件使用子组件 父组件
  • STM32 ADC 多通道16路电压采集

    下面介绍一种利用STM32单片机制作的16路多通道ADC采集电路图和源程序 采用USB接口与电脑连接 实则USB转串口方式 所以上位机可以用串口作为接口 电路图中利用LM324作为电压跟随器 起到保护单片机引脚的作用 直接在电脑USB取点
  • red5聊天功能--私聊的实现

    这里我是在服务器端进行转发 遍历当前所有连接 找到目标对象 然后调用客户端函数 不知道还有没有更好的方法 如果人数多 服务器要承受较的的压力 public boolean sendPrivateMsg String target Strin
  • Centos/linux根目录扩容、分区、挂载。LVM、物理卷、逻辑卷

    一 LVM结构 二 数据盘划分 磁盘情况 需要将vda vdb剩余资源划分给 data下 开始划分 创建磁盘分区 注意 并不要求分区的大小一致 同样 分区需要使用 8e 类型来使他们可用于LVM fdisk dev vda n gt p g
  • Unity调用摄像头获取拍摄画面

    using System Collections using UnityEngine public class Test MonoBehaviour public WebCamTexture cameraTexture public str
  • 酷应用背后,低代码正在被重估

    就当下的中国TO B企业而言 尤其是中大企业而言 复杂的场景和需求是阻碍数字化的最根本原因 而低代码恰可以以一种最原子化和最细颗粒度的数字模型来解决这个问题 帮助企业构建一个完整的 连贯的前端数字化应用 作者 皮爷 出品 产业家 生产计划
  • 华为机试题80-整型数组合并

    描述 题目标题 将两个整型数组按照升序合并 并且过滤掉重复数组元素 输出时相邻两数之间没有空格 输入描述 输入说明 按下列顺序输入 1 输入第一个数组的个数 2 输入第一个数组的数值 3 输入第二个数组的个数 4 输入第二个数组的数值 输出
  • 静态储存,堆,栈的理解

    数据结构中的堆与栈 栈 可以理解为为一个瓶子 他遵循着先进后出的原则 堆 是一种经过排序的树状结构 每一个节点都有一个值 我们通常说的堆是二叉堆 堆的特点是跟节点最大或者最小 且子节点也是一个堆 所以我们在使用的堆的时候 都是随便取一个节点
  • java ssh连接远程服务器_Ubuntu开启远程连接-开启ssh服务

    ssh分为openssh client与openssh server 如果要连接某个远程就需要装openssh client 反之如果要被其他远程连接本机就需要安装openssh server 我们安装完ubuntu之后会发现无法通过外部使
  • 【软路由】旁路由使用配置教程

    软路由 旁路由使用配置教程 简介 旁路由好处 旁路由配置步骤 1 修改管理地址网段 2 关闭IPV6和DHCP服务 3 防火墙设置 关于IP动态伪装 4 终端设置 简介 我们都知道 软路由一般有两种使用方式 一种是作为承担DHCP分配的主路
  • 【JavaScript高级程序设计】重点-第五章笔记:Date、RegExp、原始值包装类、单例内置对象

    文章目录 基本引用类型 1 Date 1 1 继承的方法 1 2 日期格式化方法 1 3 日期 时间组件方法 2 RegExp正则表达式 2 1 RegExp 实例属性 2 2 RegExp 实例方法 2 3 RegExp 构造函数属性 3
  • 【Dev-C++】的dos控制台窗口大小设置

    可以使用system 接口直接修改 例 include
  • Spring 集成OpenOffice

    第一步 openoffice jar包依赖
  • 安卓scale动画fromDegrees toDegrees解释

    注意以下实验都是相对于自己 0 0f 上 1 0f 下 如果想了解安卓scale动画pivotX 请点击这里 在安卓中 fromDegrees是开始的角度 toDegrees是介绍的角度 其中X轴右面是0度 如果toDegrees比from
  • 【JS组件篇】使用拖拽组件 react-beautiful-dnd 实现工作台自定义布局功能

    项目要求 制作工作台页面 模块可根据后端配置动态展示 各模块可根据个人喜好进行拖拽布局 并保存 大致效果如下 大致实现步骤 整体布局 首先需要对设计稿进行分析 大致分为左右两部分 可根据后端配置动态展示左右两模块的展示内容 各模块相对独立
  • C++内存管理(2)new、delete详解

    目录 new operator new操作 new类对象时加不加括号的差别 new工作任务 delete工作任务 new和delete 堆区空间操作 对比malloc和free new和delete操作基本类型的空间 new和delete操