前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【C++内存管理】:new与delete,operator new与operator delete

【C++内存管理】:new与delete,operator new与operator delete

作者头像
用户11029137
发布2025-03-12 08:35:07
发布2025-03-12 08:35:07
13800
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

📝前言: 上篇文章【C++高潮:类与对象】我们对C++的类与对象的知识点进行了讲解。 这篇文章我们在C语言内存管理的基础上探讨一下C++内存的管理: 1,C/C++内存分布 2,C语言内存管理 3,C++内存管理方式 4,operator new与operator delete 5,new和delete的实现原理 6,定位new表达式 7,malloc/free和new/delete的区别

一、C/C++内存分布

先来看一段代码(思考下面的变量都放在什么区域):

代码语言:javascript
代码运行次数:0
运行
复制
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);
}

在C/C++中,程序内存区域主要分为以下几个部分:

  1. :存放非静态局部变量、函数参数、返回值等,它的生长方向是向下的。比如代码中的localVarnum1char2pchar3ptr1ptr2ptr3都存放在栈中。
  2. :用于程序运行时动态内存分配,向上增长。通过malloccallocrealloc分配的内存以及new操作符分配的内存都在堆上,像ptr1ptr2ptr3指向的内存就是在堆上分配的。
  3. 数据段(静态区):存储全局数据和静态数据,如globalVarstaticGlobalVarstaticVar
  4. 代码段(常量区):存放可执行的代码和只读常量,例如const char* pchar3 = "abcd";中的字符串常量"abcd"就在代码段。
  5. 内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。

这里有个很有意思的问题:* char2* pchar3分别储存在哪里?

答案:* char2在栈,* pchar3在代码段。 因为,char2代表数组首元素的地址,* char2代表的是数组char2的首元素,这个首元素储存在char2中,所以在栈;pchar3指的是字符串abcda的地址,* pchar3代表的是abcd中的a,因为abcd在代码段,所以在代码段。

二、C语言中动态内存管理方式:malloc/calloc/realloc/free

在C语言里,我们使用malloccallocrealloc来分配动态内存,用free来释放内存。

代码语言:javascript
代码运行次数:0
运行
复制
void Test()
{ 
    int* p2 = (int*)calloc(4, sizeof(int));
    int* p3 = (int*)realloc(p2, sizeof(int)*10);
    // 这里需要free(p2)吗?
    free(p3);
}

(一)malloc/calloc/realloc的区别

  1. malloc:分配我们自行指定的字节数的内存空间,不会对内存进行初始化,且需要自行判断是否分配成功
  2. calloc:会在分配内存后,将内存初始化为0。它接受两个参数,第一个是元素个数,第二个是每个元素的大小。
  3. realloc:用于调整已经分配的内存块的大小,可以扩大或缩小。如果原内存块后面有足够的空间,就直接在原内存块上进行扩展;否则,会重新分配一块新的内存,并把原内存的数据复制过去。

在使用realloc时,如果p2p3指向的地址不同,就需要释放原来的p2,否则会造成内存泄漏。

(二)malloc的实现原理(glibc中)

malloc在glibc中的实现较为复杂,它基于内存池和链表等数据结构来管理内存。简单来说,它会在堆上寻找合适的空闲内存块进行分配,如果没有足够大的空闲块,可能会向操作系统申请更多内存。

三、C++内存管理方式

C++在C语言内存管理的基础上,提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理。

(一)new 和 delete 操作符

new 操作符

new 操作符用于在堆上动态分配内存,并可以同时对对象进行初始化。它有多种使用形式:

  • 分配单个对象
代码语言:javascript
代码运行次数:0
运行
复制
int* ptr = new int(42);  // 分配一个 int 类型的对象,并初始化为 42
  • 分配数组
代码语言:javascript
代码运行次数:0
运行
复制
int* arr = new int[5];  // 分配一个包含 5 个 int 类型元素的数组
delete 操作符

delete 操作符用于释放由 new 分配的内存。同样有对应于单个对象和数组的不同使用形式:

  • 释放单个对象
代码语言:javascript
代码运行次数:0
运行
复制
delete ptr;  // 释放由 new 分配的单个对象的内存
  • 释放数组
代码语言:javascript
代码运行次数:0
运行
复制
delete[] arr;  // 释放由 new[] 分配的数组的内存

注意:newdelete搭配使用,new[]delete[]搭配使用

使用实例:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
using namespace std;

// 内存管理
class A {
public:
    // 构造函数,初始化 _n 和动态分配数组
    A(int n = 1)
        :_n(n)
        , _arr(new int[5] {1, 2, 3, 4, 5})
    {
        cout << "A 类对象构造,_n = " << _n << endl;
    }

    // 析构函数,释放动态分配的内存
    ~A() {
        delete[] _arr;
        cout << "A 类对象析构,_n = " << _n << endl;
    }

    // 打印数组的第 5 个元素
    void Print() {
        cout << *(_arr + 4) << endl;
    }
private:
    int _n;
    int* _arr;
};

int main() {
    // 使用 new 创建 A 类对象的数组
    A* arr = new A[3]{ 1, 2, 3 }; // 分别将1,2,3依次传入3个A对象的构造函数

    // 调用每个对象的 Print 函数
    for (int i = 0; i < 3; ++i) {
        arr[i].Print();
    }

    // 释放动态分配的数组内存
    delete[] arr;

    return 0;
}

输出结果:

代码语言:javascript
代码运行次数:0
运行
复制
A 类对象构造,_n = 1
A 类对象构造,_n = 2
A 类对象构造,_n = 3
5
5
5
A 类对象析构,_n = 3
A 类对象析构,_n = 2
A 类对象析构,_n = 1

(二)new/delete 相比 malloc/free 的改进

1. 类型安全性
  • malloc/freemalloc 返回的是 void* 类型的指针,需要手动进行类型转换才能赋值给其他类型的指针。例如:
代码语言:javascript
代码运行次数:0
运行
复制
int* ptr = (int*)malloc(sizeof(int));

在 C++ 中,如果忘记进行类型转换,可能会导致编译错误。而且,malloc 本身并不关心所分配内存的类型,只是简单地分配指定大小的字节数。

  • new/deletenew 操作符会根据指定的类型自动计算所需的内存大小,并且返回的指针类型是正确的,无需手动进行类型转换。例如:
代码语言:javascript
代码运行次数:0
运行
复制
int* ptr = new int;
2. 初始化
  • malloc/freemalloc 只是分配一块未初始化的内存区域,分配后的内存中包含的是之前存储的任意值(即垃圾数据)。如果需要对分配的内存进行初始化,需要额外的代码来完成。例如:
代码语言:javascript
代码运行次数:0
运行
复制
int* ptr = (int*)malloc(sizeof(int));
if (ptr != NULL) {
    *ptr = 0;  // 手动初始化
}
  • new/deletenew 操作符可以在分配内存的同时对对象进行初始化。对于内置类型,可以使用括号指定初始值;对于自定义类型,会调用相应的构造函数进行初始化。例如:
代码语言:javascript
代码运行次数:0
运行
复制
int* ptr = new int(0);  // 初始化 int 类型对象为 0

class MyClass {
public:
    MyClass() { std::cout << "Constructor called" << std::endl; }
};

MyClass* obj = new MyClass;  // 调用构造函数初始化对象
3. 自定义类型的处理
  • malloc/free:当处理自定义类型时,mallocfree 只是简单地分配和释放内存,不会调用对象的构造函数和析构函数。这意味着对象的资源管理和初始化工作需要程序员手动完成,否则可能会导致资源泄漏或未定义行为。例如:
代码语言:javascript
代码运行次数:0
运行
复制
#include <stdlib.h>
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "Constructor called" << std::endl; }
    ~MyClass() { std::cout << "Destructor called" << std::endl; }
};

int main() {
    MyClass* obj = (MyClass*)malloc(sizeof(MyClass));  // 仅分配内存,不调用构造函数
    // 手动释放内存,不调用析构函数
    free(obj);
    return 0;
}
  • new/delete:对于自定义类型,new自动调用对象的构造函数进行初始化delete自动调用对象的析构函数进行资源清理。这确保了对象的生命周期管理是正确的,减少了因忘记调用构造函数或析构函数而导致的错误。例如:
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "Constructor called" << std::endl; }
    ~MyClass() { std::cout << "Destructor called" << std::endl; }
};

int main() {
    MyClass* obj = new MyClass;  // 分配内存并调用构造函数
    delete obj;  // 调用析构函数并释放内存
    return 0;
}
4. 异常处理
  • malloc/freemalloc 在内存分配失败时会返回 NULL,因此在使用 malloc 分配内存后,需要手动检查返回值是否为 NULL,以处理内存分配失败的情况。例如:
代码语言:javascript
代码运行次数:0
运行
复制
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
    // 处理内存分配失败的情况
}
  • new/deletenew 操作符在内存分配失败时会抛出 std::bad_alloc 异常。可以使用 try-catch 块来捕获并处理这个异常,使代码的错误处理更加结构化和清晰。例如:
代码语言:javascript
代码运行次数:0
运行
复制
try {
    int* ptr = new int;
} catch (const std::bad_alloc& e) {
    std::cerr << "Memory allocation failed: " << e.what() << std::endl;
}

四、operator new与operator delete函数

newdelete是用户进行动态内存申请和释放的操作符,operator newoperator delete是系统提供的全局函数。

实际上,operator new是一个加强版的可抛异常的malloc,底层也是用malloc分配内存,operator delete是一个加强版的free,底层也是free来释放内存

new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

代码语言:javascript
代码运行次数:0
运行
复制
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{ 
    void *p;
    while ((p = malloc(size)) == 0)
    {
        if (_callnewh(size) == 0)
        { 
            static const std::bad_alloc nomem;
            _RAISE(nomem);
        }
    }
    return (p);
}

void operator delete(void *pUserData)
{ 
    _CrtMemBlockHeader * pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
        return;
    _mlock(_HEAP_LOCK); 
    __TRY
    { 
        pHead = pHdr(pUserData);
        _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
        _free_dbg( pUserData, pHead->nBlockUse );
    }
    __FINALLY
    { 
        _munlock(_HEAP_LOCK); 
    }
    __END_TRY_FINALLY
    return;
}

// free的实现
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

五、new和delete的实现原理

(一)内置类型

对于内置类型,newmallocdeletefree基本类似,不同的是new/delete申请和释放单个元素空间,new[]delete[]申请连续空间,并且new申请失败时抛异常,malloc返回NULL

(二)自定义类型

  1. new的原理:先调用operator new函数申请空间,然后在申请的空间上执行构造函数,完成对象的构造。
  2. delete的原理:先在空间上执行析构函数,完成对象中资源的清理工作,然后调用operator delete函数释放对象的空间。
  3. new T[N]的原理:调用operator new[]函数(实际在operator new[]中调用operator new函数完成N个对象空间的申请),然后在申请的空间上执行N次构造函数
  4. delete[]的原理:在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理,然后调用operator delete[]释放空间(实际在operator delete[]中调用operator delete来释放空间)。

六、定位new表达式

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。定位 new 表达式会自动调用相应的构造函数,但需要手动调用析构函数来正确销毁对象。

代码语言:javascript
代码运行次数:0
运行
复制
class A
{
public:
    A(int a = 0): _a(a)
    {
        cout << "A():" << this << endl;
    }
    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};

int main()
{
    A* p1 = (A*)malloc(sizeof(A));
    new(p1)A;  // 在 p1 所指向的内存位置上调用 A 类的默认构造函数来构造一个 A 类的对象(如果要给构造函数传参可以写成:new(p1)A(1))
    p1->~A(); // 手动调用 A 类的析构函数,因为使用定位 new 构造的对象不会自动调用析构函数
    free(p1); // 使用 free 函数释放之前 malloc 分配的内存

    A* p2 = (A*)operator new(sizeof(A)); // 使用 operator new 分配内存并使用定位 new 构造对象
    new(p2)A(10);
    p2->~A(); 
    operator delete(p2);
    return 0;
}

它的使用格式是new (place_address) type或者new (place_address) type(initializer - list)place_address必须是一个指针,initializer - list是类型的初始化列表。 定位new表达式一般配合内存池使用,因为内存池分配出的内存没有初始化,对于自定义类型的对象,需要用定位new表达式显式调用构造函数进行初始化

七、malloc/free和new/delete的区别

  1. mallocfree是函数,newdelete是操作符。
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,在[]中指定对象个数。
  4. malloc的返回值为void*,使用时必须强转,new不需要,因为new后跟的是空间的类型。
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但new需要捕获异常。
  6. 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、C/C++内存分布
  • 二、C语言中动态内存管理方式:malloc/calloc/realloc/free
    • (一)malloc/calloc/realloc的区别
    • (二)malloc的实现原理(glibc中)
  • 三、C++内存管理方式
    • (一)new 和 delete 操作符
      • new 操作符
      • delete 操作符
    • (二)new/delete 相比 malloc/free 的改进
      • 1. 类型安全性
      • 2. 初始化
      • 3. 自定义类型的处理
      • 4. 异常处理
  • 四、operator new与operator delete函数
  • 五、new和delete的实现原理
    • (一)内置类型
    • (二)自定义类型
  • 六、定位new表达式
  • 七、malloc/free和new/delete的区别
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档