【C++】内存管理

【C++】内存管理

在C语言中,我们可以通过 malloc/calloc/realloc/free 等函数进行动态内存管理,而到了C++中就有了新的内存管理方式: new/delete

C/C++ 内存分布

C/C++ 中程序内存划分区域是这样的:

在语言层,我们通常把数据段称为静态区 ,把代码段称为常量区。以下几道题可以帮我们更好地理解内存区域划分

cpp 复制代码
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);
}

以下数据分别存在哪里?选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

  • globalVar ->静态区,全局变量存在静态区
  • staticGlobalVar ->静态区,静态变量存在静态区
  • staticVar ->静态区,静态变量存在静态区
  • localVar ->栈,普通变量存在栈区
  • num1 ->栈,数组存放于栈区
  • char2 ->栈,拷贝常量字符串给字符数组,数组存放于栈区
  • *char2 ->栈,数组中的内容存在栈区,如上图
  • pChar3 ->栈,虽然指向的内容 存放于常量区,但是指针本身存放于栈区
  • *pChar3 ->常量区,指向的内容是常量字符串,所以存放于常量区,如上图
  • ptr1 ->栈,虽然指向的内容 存放于堆,但是指针本身存放于栈区
  • *ptr1 ->堆,指向的内容是在堆区开辟的空间,如上图

C语言的内存管理

C语言的内存管理在本篇不是重点,所以只稍微提一下

malloc/calloc/realloc的区别

  • mallco,参数是需要开辟的空间总大小
  • calloc,更加精细一些,参数是要开辟多少个 空间,每个空间有多大 。而且还会将空间内的值初始化为0
  • realloc,对已有空间进行扩容。如果原空间后面有足够的可使用空间,就会原地扩容 ;如果不够,就会异地扩容 :将原空间的数据拷贝至新空间+扩容,销毁原空间,返回新空间地址

mallco 系列函数使用时都需要强制类型转换 ,且需要对返回值进行检查,如果开辟空间失败需要进行相应的处理。而且数据初始化很不方便

cpp 复制代码
int* p1 = (int*) malloc(sizeof(int));
if (p1 == NULL)
{
    perror("malloc fail\n");
    exit(-1);
}

C++的内存管理

为了解决以上痛点,C++提出了新的内存管理方式:使用newdelete操作符进行内存管理

new/delete 内置类型

使用格式如下:

cpp 复制代码
// 申请空间:内置类型指针 指针名 = new 内置类型
int* p1 = new int;
// 释放空间:delete 指针名
delete p1;

使用 new/delete 管理空间,不用强转 指针,也不用指定空间大小 ,使用起来简便许多。不仅如此,new还支持初始化,如下

cpp 复制代码
int *p1 = new int(10) // 申请一个 int 空间,并初始化为10

如果想一次申请多个空间,要在类型后面加[数量],格式如下:

cpp 复制代码
int* p1 = new int[10]; // 申请10个 int 类型的空间 

一次申请多个空间也可以初始化,格式如下:

cpp 复制代码
int* p1 = new int[3] {1, 2, 3};

释放多个空间 时,一定要在delete后加[]

cpp 复制代码
int* p1 = new int[3] {1, 2, 3};
delete[] p1;
  • 单个空间的申请和释放要使用new和delete操作符
  • 多个空间的申请和释放要使用new[]和delete[]操作符
  • 以上操作符一定要匹配使用

new/delete 自定义类型

在C语言中,使用 malloc 函数创建一个自定义对象有些麻烦,例如链表节点

cpp 复制代码
struct ListNode
{
        int _val;
        ListNode* _next;
};
// 创建新节点
struct ListNode* CreateNode(int x)
{
        ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
        if (newnode == NULL)
        {
                perror("malloc fail");
                exit(-1);
        }

        newnode->_val = x;
        newnode->_next = NULL;
        return newnode;
}

而使用new就可以直接创建自定义类型对象,会自动为自定义类型开辟空间 ,并且调用构造函数

cpp 复制代码
class ListNode
{
public:
        ListNode(int val)
                :_val(val)
                , _next(nullptr)
        {}
private:
        int _val;
        ListNode* _next;
};

ListNode* ln1 = new ListNode(10); // 开辟空间+调用构造

使用 delete 释放自定义类型时,会先自动调用相应的析构函数,再释放空间。如果先释放对象的空间,就找不到自定义对象了,自然调用不了析构函数,如果对象申请了空间,就会造成内存泄漏

operator new与operator delete函数

operator new与operator delete不是 new 和 delete 的重载,而是独立的函数。

  • new 和 delete 是操作符 ,而operator new与operator delete是全局函数
  • new 在底层调用 operator new 来申请空间;delete 在底层调用 operator delete 来释放空间
  • new[] 在底层调用 operator new[] 来申请空间;delete[] 在底层调用 operator delete[] 来释放空间

而 operator new 是通过 malloc 申请空间 的,并且封装了其他功能;operator delete 也类似,是通过 free 来释放空间

new 和 delete 的原理

内置类型

对于内置类型,malloc/free 和 new/delete 基本类似,但不同的是:

  • malloc 失败会返回 NULL,需要手动检查
  • new 失败会抛异常,需要捕获

自定义类型

如下自定义类型用来测试

cpp 复制代码
class A
{
public:
        A(int val = 0)
                :_a(val)
        {
                cout << "A(int val = 0)" << endl;
        }

        ~A()
        {
                cout << "~A()" << endl;
        }
        
private:
        int _a;
};

new 和 delete

  • new一个自定义类型的对象,会先调用 operator new 为对象在堆上申请空间 ,然后在空间上调用对象的构造函数,完成对象的构造

反汇编:

  • delete对象,会先在空间上调用对象的析构函数 ,释放对象申请的空间,然后调用 operator delete 销毁对象,释放对象的空间

new[] 和 delete[]

  • new A[10] :会先调用 operator new[],为 10 个A类对象申请空间,之后在申请的空间上调用 10 次构造函数,完成构造
  • delete[] A :先在申请的空间上调用 10 次析构函数,清理掉对象申请的资源,然后调用 operator delete 释放这 10 个A类对象的资源

new/delete 和 new[]/delete[] 要匹配使用

new A[10]表明了有10个空间,但是delete[] A 中没有数字,delete是如何得知要调用10次析构函数 ?那是因为在 new 时,我们已经给过数字,那么在申请空间时,会多申请一个 int 大小的空间,来存放申请了多少个空间


这样 delete[] 就会知道多开了一个 int 大小的空间,会先调用 10 次析构,然后调用 operator delete 从多开的空间开始释放空间

如果new[]没有配套使用delete[],而是使用delete,那么就不会从额外申请的空间开始释放 ,这样就会出错,因为释放空间只能从申请的空间开头释放

定位new(placement-new)

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

使用格式:

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

使用场景:

定位new表达式在实际中一般是配合内存池 使用。因为内存池分配出的内存没有初始化,所以如

果是自定义类型的对象 ,需要使用new的定义表达式进行显式调用构造函数进行初始化。

cpp 复制代码
A* pa = (A*)malloc(sizeof(A)); // pa只是指向一块大小和A相同的空间,未初始化,不算做一个对象
new(pa)A(10); // 显式调用构造,初始化

总结:malloc/free 和 new/delete 的区别

  1. malloc 是函数,new 是操作符
  2. malloc 返回的是 void* 指针,需要强转才可以使用;new 返回的是对应的类型的指针
  3. malloc 需要手动计算开辟空间的大小并传递;new 只需要在后面跟上空间的类型即可
  4. malloc 不可以初始化空间数据;new 可以初始化
  5. malloc 失败时会返回 NULL,需要手动判空;new 失败时会抛异常,需要捕获异常
  6. molloc/free 申请自定义对象时,不会调用构造和析构;new 会在申请空间后调用对象的构造函数完成构造,delete 会在释放对象空间之前,调用对象的析构函数清理对象申请的资源
相关推荐
axxy200021 分钟前
leetcode之hot100---24两两交换链表中的节点(C++)
c++·leetcode·链表
_oP_i1 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx1 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
武子康1 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
若亦_Royi1 小时前
C++ 的大括号的用法合集
开发语言·c++
豪宇刘2 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意2 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
东阳马生架构3 小时前
JVM实战—1.Java代码的运行原理
jvm
FF在路上3 小时前
Knife4j调试实体类传参扁平化模式修改:default-flat-param-object: true
java·开发语言
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html