C++ 动态内存管理,new与delete

本文详细解析了C/C++中的内存分布,包括栈区、堆区、数据段和代码段的特点。深入探讨了C/C++动态内存管理机制,对比了C语言中的malloc/free与C++中的new/delete操作符,讲解了它们的实现原理及使用注意事项。

目录

C/C++内存分布

C语言动态内存管理

C++动态内存管理

语法:

总结:

new/delete 实现原理

operator new与operator delete函数

new/delete 实现原理总结

类内部实现operator new/operator delete(了解)

定位new表达式(placement-new)

malloc/free 和 new/delete 的区别


C/C++内存分布

C/C++文件编译链接形成的可执行文件,是直接运行在操作系统上的。不像Java这种语言,是运行在虚拟机上的,相比之下就显得没那么贴近操作系统了。 

而C/C++的程序运行起来之后,各种变量,对象,代码分布都是有区域划分的,了解这些区域,可以更加理解不同变量/对象的性质。

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);
}

栈区开辟函数栈帧,局部变量,参数,临时变量等存储在栈区。堆区属于动态开辟的区域,new / malloc 出的空间都属于堆区,这个区域需要我们自己管理。数据段,又名静态区,存储静态变量,全局变量和全局静态变量。 代码段又名只读常量区,存储可执行代码,常量。

不同存储区域主要决定对象/变量的生命周期。

如上代码中,globalVar staticGlobalVar staticVar 存储在静态区,也就是数据段。 下面的localVar num1 char2 pChar3 ptr1 ptr2 ptr3 都是在函数栈帧内创建的局部对象。
比较有趣的是:对于char2这个字符数组来说,实际上是把常量区的"abcd"字符串拷贝到栈区一份,而局部指针变量pChar3指向的就是常量区的"abcd" (pChar3声明为const char*是因为它指向的是一个常量字符串) 下面的ptr1 ptr2 ptr3指向的是堆区的内存。

之前有一个有些许特殊的疑问:我不太理解这个char2在内存中是一个怎样的存储方式,因为我们知道,在某些使用场景下,数组名是数组的首元素地址,那么,这个char2在内存中是一个指针吗?实际上并不是,char2只是用来标识这个数组,充当这个数组的数组名。


下面,我们重点学习堆区,因为动态内存管理就是管理堆区的空间。

C语言动态内存管理

这里简单回顾一下,malloc,calloc,realloc和free在开辟空间时的特点,主要为了理解后面的new / delete ,也方便作比较

1. void* malloc (size_t size); malloc语法非常简单,就是指定x字节,在堆区开辟空间,然后返回值是void*,需要我们强转为我们需要的指针类型。   且malloc开辟出的空间是随机值,不会初始化

2. void* calloc (size_t num, size_t size); 语法与malloc相比略有不同,num为开辟元素个数,size为开辟单个元素的字节大小。区别是开辟出的空间初始化为0

3. void* realloc (void* ptr, size_t size); size为新的空间字节大小,realloc常用于对一段已经开辟的空间进行扩容,分为原地扩和异地扩。

4. 对于malloc和calloc和realloc开辟的空间,需要free进行回收释放。

C++动态内存管理

C++中动态内存管理使用new 和 delete关键字,在堆区开辟和释放空间。

语法:

void Test1() {
    int* p = new int;   // new开辟一个int变量,不初始化
    int* p2 = new int(1); // 初始化为1
    int* p3 = new int[3]; //开辟3个int。不初始化
    int* p4 = new int[3]{1,2,3};  // 初始化3个int
    
    delete p;
    delete p2;
    delete[] p3;
    delete[] p4;
    // 自定义类型
    A* ptr = new A;  // 堆区开辟单个自定义类型对象,调用默认构造函数,若没有,报错
    A* ptr2 = new A(1);  // 指定构造方式,调用对应的构造函数
    A* ptr3 = new A[3]; // 调用三次默认构造函数
    A* ptr4 = new A[3]{1,2,3}; // 三次对应的构造函数
    A* ptr5 = new A[3]{{1,2},{3,4},{5,6}}; // 三次对应的构造函数
    A* ptr6 = new A[3]{A(1,2),A(2,3),A(3,4)};  // 同上
    cout<<endl<<endl;
    A* ptr7 = new A[3]{{1,2},1,{4,5}};   // 配对构造函数
    
    delete ptr;
    delete ptr2;
    delete[] ptr3;
    delete[] ptr4;
    delete[] ptr5;
    delete[] ptr6;
    delete[] ptr7;
}
// 控制台输出结果
A()
A(int a)
A()
A()
A()
A(int a)
A(int a)
A(int a)
A(int a, int b)
A(int a, int b)
A(int a, int b)
A(int a, int b)
A(int a, int b)
A(int a, int b)


A(int a, int b)
A(int a)
A(int a, int b)
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()
~A()

上方为测试new使用方法和对应的输出结果,当调用特定的构造函数时,会输出对应语句。

总结:

1. 对于内置类型而言,new和malloc的作用差别不是很大,只是new可以在堆区创建对象的同时初始化为指定值,且语法使用上也更加便捷了。

2. 对于自定义类型而言,new和malloc有本质的区别,因为new在堆区实例化对象时,必须调用某构造函数对对象进行初始化,上方语句中,每条new语句都调用了对应的构造函数,而malloc只能开辟出没初始化的空间,是无法对对象进行初始化的。

3. 对应new,delete在执行过程中,也会对对象调用析构函数,完成资源的清理。并释放空间。而free仅仅是释放空间,不会调用析构函数。

4. 我们知道,对象的构造函数和析构函数是无比,至关重要的。所以,在C++中,动态开辟对象不能使用malloc/free,必须使用new/delete (当然也有可能有其他方法)。

5. new/delete   new[]/delete[] 要配对使用,开辟多个对象使用new[],清理时就要使用delete[]

6. new和malloc还有一个区别是:malloc在申请空间失败时,返回0。 而new申请空间失败时,抛异常,需要try catch语句处理异常,这也是C++的风格。

7. new/delete是关键字,也是操作符,需要操作数。而malloc/free是函数。

new/delete 实现原理

上图为A* p2 = new A(10); 形成的汇编指令。 可以看出,new完成工作实际上是调用operator new 和 构造函数来完成的。 对应的,delete 实际上是调用operator delete 和 析构函数。

operator new与operator delete函数

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

/*
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);
}
/*
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)

上方代码为operator new 和 operator delete的底层实现,我们不需要看懂,只需要知道,operator new实际上就是封装了malloc函数,通过malloc来申请空间,当申请空间失败时,抛出异常。而 operator delete 就是封装了free函数,通过free来释放空间。(其中的_free_dbg 就是 free)

operator new不是重载的new操作符,而是一个函数,它的函数名就是operator new,他是一个全局库函数。

我们可以理解为 new  =  operator new(malloc+抛异常机制)   +  类的构造函数
                        delete = operator delete(free)  + 类的析构函数

new/delete 实现原理总结

内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

自定义类型

new的原理

        1. 调用operator new(malloc)函数申请空间

        2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

        1. 在空间上执行析构函数,完成对象中资源的清理工作

        2. 调用operator delete(free)函数释放对象的空间

new T[N]的原理

        1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请

        2. 在申请的空间上执行N次构造函数

delete[]的原理

        1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

        2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

类内部实现operator new/operator delete(了解)

#include<iostream>
struct ListNode
{
    int _val;
    ListNode* _next;

    // 内存池
    static std::allocator<ListNode> alloc;

    void* operator new(size_t n) {
        std::cout << "operator new -> STL内存池allocator申请" << std::endl;
        void* obj = alloc.allocate(1);
        return obj;  // 开辟出一个空间,返回地址  实际上全局operator new函数也是这个功能
    }
    void operator delete(void* ptr) {
        std::cout << "operator delete -> STL内存池allocator申请" << std::endl;
        alloc.deallocate((ListNode*)ptr, 1);
    }

    ListNode(int val): _val(val), _next(nullptr){

    }
};

std::allocator<ListNode> ListNode::alloc;

int main() {
    ListNode* node1 = new ListNode(1);
    ListNode* node2 = new ListNode(2);
    ListNode* node3 = new ListNode(3);
    delete node1;
    delete node2;
    delete node3;
}

这里是在类内实现operator new 和 operator delete,则在外部new类对象时,不再调用全局库的operator new,而是调用类内的。这里不属于函数重载,因为作用域不同,也不属于运算符重载。这里仅仅是实现了类的operator new / operator delete函数,用于new和delete这个类的对象时使用此函数。

作用:因为标准库中的operator new 使用malloc在堆区申请空间,如果频繁的话,效率会比较低。在我们自己实现的函数内部,可以在某内存池(或者STL的内存池)内申请空间,会提高效率。

operator new / operator delete 的函数格式是一定的,返回值和参数一定。不能改变

定位new表达式(placement-new)

用于申请好的类对象空间,但是对象没有进行初始化。比如malloc出的空间。则使用定位new即可调用类的构造函数初始化对象。

void Test2() {
    A* p = (A*)malloc(sizeof(A));
    new(p)A;
    p->~A();
    free(p);

    A* p2 = (A*)operator new(sizeof(A));  // 调用全局库中的operator new
    new(p2)A(10);
    p2->~A();
    operator delete(p2);
}


int main()
{
    Test2();
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值