【C++ 学习 ⑥】- C++ 动态内存管理详解

news2024/11/24 5:50:04

目录

一、new 表达式和 delete 表达式的工作机理

二、operator new 和 operator delete 函数

2.1 - 标准库定义

2.2 - 重载

三、定位 new 表达式

四、常见面试题

4.1 - malloc/free 和 new/delete 的区别

4.2 - 内存泄漏


 

在 C++ 中,new 和 delete 既是关键字,也是一种特殊的操作符。C++ 程序中的动态内存管理主要就是通过操作符 new/delete 和 new[]/delete[] 实现的


一、new 表达式和 delete 表达式的工作机理

假设有一个类 A

class A
{
public:
    A(int x = 0) : _i(x) 
    {
        cout << "A(int x = 0), _i: " << _i << endl;
    }
    ~A()
    {
        cout << "~A(), _i: " << _i << endl;
    }
private:
    int _i;
};

当我们使用一条 new 表达式时

A* p1 = new A;  // 分配一个默认初始化的 A 类对象
A* p2 = new A(10);  // 分配并初始化一个 A 类对象
// A(int x = 0), _i: 0
// A(int x = 0), _i: 10
​
A* arr1 = new A[5];  // 分配五个默认初始化的 A 类对象
A* arr2 = new A[5]{ 1, 2, 3, 4, 5 };  // 分配并初始化五个 A 类对象
A* arr3 = new A[5]{ A(1), A(2), A(3), A(4), A(5) };  // 分配并初始化五个 A 类对象
// A(int x = 0), _i: 0 --> 输出 5 次
// A(int x = 0), _i: 1 ~ 5
// A(int x = 0), _i: 1 ~ 5

实际上执行了以下两步操作

  1. 调用一个名为 operator new(或者 operator new[])的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或对象的数组)。

    operator new[] 实际上通过调用 operator new 完成 N 个对象空间的申请

  2. 编译器运行相应的构造函数以构造这些对象,并为其传入初始值

当我们使用一条 delete 表达式时

delete p1;  // 销毁 *p1,然后释放 p1 指向的内存空间
delete p2;  // 销毁 *p2,然后释放 p2 指向的内存空间
// ~A(), _i: 0
// ~A(), _i: 10
​
delete[] arr1;  // 销毁数组中的元素,然后释放对应的内存空间
delete[] arr2;  // 销毁数组中的元素,然后释放对应的内存空间
delete[] arr3;  // 销毁数组中的元素,然后释放对应的内存空间
// ~A(), _i: 0  --> 输出 5 次
// ~A(), _i: 5 ~ 1
// ~A(), _i: 5 ~ 1

实际上执行了以下两步操作

  1. 对 p1/p2 所指的对象或者 arr1/arr2/arr3 所指的数组中的元素执行对应的析构函数

  2. 编译器调用名为 operator delete(或者 operator delete[])的标准库函数释放内存空间

    operator delete[] 实际上通过调用 operator delete 完成内存空间的释放

 

 


二、operator new 和 operator delete 函数

2.1 - 标准库定义

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

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
    // try to allocate size bytes
    void* 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);
}
  1. 抛出异常

    int main()
    {
        int* p = nullptr;
        do
        {
            p = new int[1024 * 1024];
        } while (p);
        return 0;
    }

  2. 捕获异常

    #include <iostream>
    using namespace std;
    ​
    int main()
    {
        int* p = nullptr;
        try
        {
            do
            {
                p = new int[1024 * 1024];
            } while (p);
        }
        catch (exception& e)
        {
            cout << e.what() << endl;  // bad allocation
        }
        return 0;
    }

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_FINALLY
​
    return;
}
​
/* free 的实现 */
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

2.2 - 重载

一般情况下不需要自定义 operator new 和 operator delete,除非在申请和释放空间时有某些特殊的需求,例如:在使用 new 和 delete 申请和释放空间时,打印一些日志信息,可以简单帮助用户来检测是否存在内存泄漏

#include <iostream>
using namespace std;
​
// 重载 operator delete,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个字节
void* operator new(size_t size, const char* fileName, const char* funcName,
    size_t lineNo)
{
    void* p = ::operator new(size);
    cout << fileName << "-" << funcName << "-" << lineNo << "-" << p << "-"
        << size << endl;
    return p;
}
​
// 重载 operator delete,在释放空间时:打印再那个文件、哪个函数、第多少行释放
void operator delete(void* p, const char* fileName, const char* funcName,
    size_t lineNo)
{
    cout << fileName << "-" << funcName << "-" << lineNo << "-" << p <<
        endl;
    ::operator delete(p);
}
​
int main()
{
    // 对重载的operator new 和 operator delete进行调用
    int* p = new(__FILE__, __FUNCTION__, __LINE__) int;
    operator delete(p, __FILE__, __FUNCTION__, __LINE__);
    return 0;
}

上述调用显然太麻烦了,可以使用宏对调用进行简化(只有在 Debug 模式下,才调用用户重载的 operator new 和 operator delete)

#ifdef _DEBUG
#define new new(__FILE__, __FUNCTION__, __LINE__)
#define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__)
#endif
​
int main()
{
    int* p = new int;
    delete(p);
    return 0;
}


三、定位 new 表达式

定位 new(placement new)表达式是在已分配的原始内存空间中调用构造函数初始化一个对象

使用格式

new(place_address) type
// 或者
new(place_address) type(initializer-list)
// place_address 必须是一个指针,initializer-list 是初始化列表

使用场景

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

#include <iostream>
using namespace std;
​
class A
{
public:
    A(int x = 0) : _i(x)
    {
        cout << "A(int x = 0), _i: " << _i << endl;
    }
    ~A()
    {
        cout << "~A(), _i: " << _i << endl;
    }
private:
    int _i;
};
​
int main()
{
    // A* p = (A*)malloc(sizeof(A));
    // 或者
    A* p = (A*)operator new(sizeof(A));
    new(p) A(10);
    // A(int x = 0), _i: 10
​
    p->~A();
    // ~A(), _i: 10
    free(p);
    return 0;
}


四、常见面试题

4.1 - 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 在释放空间前会调用析构函数完成空间中资源的清理。

4.2 - 内存泄漏

内存泄漏指因为疏忽或者错误造成程序未能释放已经不能使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等,出现内存泄漏会导致响应越来越慢,最终卡死。

C/C++ 程序中我们一般关心两种方面的内存泄漏:

  1. 堆内存泄漏(Heap Leak)

    堆内存指的是程序执行中依据需要通过 malloc/ calloc/ realloc/ new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak。

  2. 系统资源泄漏

    指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

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

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

相关文章

Linux系统c语言socket实现TCP通信

socket通信用到的函数 int socket( int af, int type, int protocol); af&#xff1a;一个地址描述。仅支持AF_INET格式&#xff0c;也就是说ARPA Internet地址格式。 type&#xff1a;指定socket类型。新套接口的类型描述类型&#xff0c;如TCP&#xff08;SOCK_STREAM&#…

IMX6ULL裸机篇之DDR3参数配置分析

一. DDR3L 初始化简介 上一篇博文进行了 DDR参数的初始化&#xff0c;通过一个 execl表进行配置&#xff0c;生成脚本文件。文章网址如下&#xff1a; IMX6ULL裸机篇之DDR3初始化_凌雪舞的博客-CSDN博客 本文对 DDR的参数配置进行详细的说明。即对 "Register Configur…

前端026_菜单模块_新增功能

菜单模块_新增功能 1、需求分析2、新增组件实现3、列表引用新增组件4、关闭弹出窗口5、校验表单数据6、提交表单数据6.1、Mock 添加新增模拟接口6.2、Api 调用接口6.3、测试新增功能1、需求分析 菜单管理中有两处有 新增 按钮: 条件区域的是新增一级菜单,传递的参数是0。列表…

java多线程同步技术基础

说明 当程序中出现多个进程对同一资源进行操作时&#xff0c;因为对数据的操作非常密集&#xff0c;可能会对资源过度操作&#xff0c;这时就需要用到线程的同步技术。 以一个抢红包程序为例&#xff0c;红包数量为3个&#xff0c;开启5个线程来模拟抢红包行为&#xff0c;红…

[MAUI]在.NET MAUI中复刻苹果Cover Flow

文章目录 原理3D旋转平行变换 创建3D变换控件绘制封面图片应用3D旋转应用平行变换绘制倒影创建绑定属性 创建绑定数据创建布局计算位置计算3D旋转 创建动效项目地址 Cover Flow是iTunes和Finder中的一个视图选项&#xff0c;允许用户使用水平滚动的图像查看他们的音乐库或文件。…

使用Qt Creator编写窗体程序并打包发布

1、设置编辑器显示语言为中文(个人习惯) 2、新建窗体应用程序 3、简单修改一下代码 3.1 双击ui文件进入设计模式 3.2 从左侧组件中直接将需要使用的组件拖拽到窗体中 3.3 添加槽函数 选中按钮&#xff0c;右键菜单中 点击 转到槽&#xff0c;头文件和.cpp文件会自动添加对…

中间件_RabbitMQ五种消息模型

文章目录 1.简单消息队列模型2.Work工作队列模型3.发布订阅模型3.1.Fanout广播3.2.Direct路由3.3.Topics通配符 RabbitMQ官方文档 RabbitMQ 提供了5种常用消息模型。但是其实3、4、5这三种都属于订阅模型&#xff0c;只不过进行路由的方式不同。 1.简单消息队列模型 简单消息队…

C语言入门篇——编译篇

目录 1、程序环境 1.1 ANSI C 标准 1.2程序的翻译环境和执行环境 1.3运行环境 2、预处理详解 2.1、预定义符号 2.2、#define 2.2.1#define定义表示符 2.2.2#define定义宏 2.2.3#define替换规则 2.4#和## 2.2.5带副作用的宏参数 2.2.6宏和函数对比 3、#undef 4、…

项目1:登录功能设计

需求 后端接口设计MySQL表常用功能模块 后端总和前端实现方案 home页面 需求 实现一个登录功能 实现的功能 注册(邮箱注册) 登录(邮箱密码) 重置密码 查看操作记录(登录, 注册, 重置密码, 登出. 都算操作) 登出 后端接口设计 1. 人机验证 只要下面出现 人机验证 的功能都需要使…

容器化:MySQL

1 缘起 开启容器化之路。 2 容器化MySQL 2.1 查看MySQL镜像 docker search mysql2.2 指定版本&#xff1a;5.7.30 通过官网查看&#xff1a;https://hub.docker.com/ docker pull mysql:5.7.302.3 路径挂载 容器路径挂载到宿主机。 新建宿主机路径 mkdir -p /home/xind…

ElasticSearch-索引和文档的创建修改删除

目录 一、创建索引 二、查看索引 三、索引是否存在 四、删除索引 五、创建文档 六、查看文档 七、更新文档 八、文档是否存在 九、删除文档 一、创建索引 # 创建一个默认的索引&#xff0c;默认是标准分词器的索引 PUT /es_db2# 创建一个默认为ik分词器的索引 PUT /e…

十万条数据,后端不分页咋办!(如何优化长列表渲染)

十万条数据&#xff0c;后端不分页咋办&#xff01;&#xff08;如何优化长列表渲染&#xff09; 长列表是什么&#xff1f; 我们通常把一组数量级很大的数据叫做长列表&#xff0c;比如渲染一组上千条的数据&#xff0c;我们以数组的形式拿到这些信息&#xff0c;然后遍历渲…

Rust快速安装

Rust依赖C编译&#xff0c;Rust官方推荐的安装方式是利用VisualSudio安装C环境&#xff0c;VisualStuidio用过的都懂&#xff0c;庞大无比、卡顿、下载还贼慢(我当时装了一上午好像)&#xff0c;因此我们通过其它方式配置C 安装C环境 下载MinGW 64 win32 seh Mingw官网&…

Java的并发集合框架

文章目录 一、并发集合框架1. 简介2. 接口Iterable2. 接口Collection3. 接口List4. 接口Set5. 接口Queue6. Deque 二、非阻塞队列1. 概念2. ConcurrentHashMap使用&#xff08;1&#xff09;简介&#xff08;2&#xff09;验证HashMap不是线程安全的&#xff08;3&#xff09;验…

ld文件中指定变量在flash中的地址定义

本文说的是在gcc环境中&#xff0c;Keil或IAR可能有自己的使用方法。 我们在定义变量时&#xff0c;有时候需要把变量定义放到flash中的固定位置或区域&#xff0c;此时需要修改工程中的链接文件&#xff08;link file&#xff0c;ld文件&#xff09;。 方法一 修改ld文件中的…

软件IIC通信以及源码解析(如何使用)

以对读取MPU6050为例&#xff0c;解析如何采用IIC通信源码。 IIC的的通信&#xff0c;通常三种用途读写。分为&#xff1a; 1&#xff1a; 2&#xff1a; 3&#xff1a; 注&#xff1a;其中最常用的就是1和3了。 对1进行讲解&#xff1a; 指定地址写&#xff0c;通常用作对状…

【网络】- TCP/IP四层(五层)协议 - 网际层(网络层) - IP地址

目录 一、概述 二、IP地址的定义 三、IP地址由网络和主机两部分标识组成 一、概述 上篇文章简单介绍了网际协议IP。网际协议 IP 大致分为三大作用模块&#xff0c; ①IP寻址、 ②路由&#xff08;最终节点为止的转发&#xff09; 、③IP分包与组包。 这篇文章主要详细介绍IP地址…

【HTTP协议详解】

目录 1.什么是http2.抓包工具2.1 抓包工具2.2 抓包原理 3.Http协议格式3.1Http请求报文3.2Http响应报文的格式 4.请求报文格式4.1 报文首行4.2 请求报文header 5. 响应报文格式6.构造Http请求7.Https协议7.1 对称密钥7.2 非对称密钥7.3 证书 1.什么是http HTTP全称为“超文本协…

《MySQL是怎么运行的》阅读分享

mysql运行的整体架构简介 Mysql是由两部分构成&#xff0c;一部分是服务器程序&#xff0c;一部分是客户端程序。 服务器程序又包括两部分&#xff1a; 第一部分server层包括连接器、查询缓存、分析器、优化器、执行器等。涵盖 MySQL 的大多数核心服务功能&#xff0c;以及所有…

LeetCode:738.单调递增的数字 714.买卖股票的最佳时机含手续费 968.监控二叉树

738.单调递增的数字 题目 当且仅当每个相邻位数上的数字 x 和 y 满足 x < y 时&#xff0c;我们称这个整数是单调递增的。 给定一个整数 n &#xff0c;返回 小于或等于 n 的最大数字&#xff0c;且数字呈 单调递增 。 贪心 class Solution {public int monotoneIncrea…