当前位置: 首页 > news >正文

【C++】内存管理,深入解析new、delete

【C++】内存管理

文章目录

  • 【C++】内存管理
    • 一、C/C++内存分布
    • 二、C语言中动态内存管理方式:
      • 关键区别总结
    • 三、C++内存管理方式
      • 3.1new和delete操作内置类型:
      • 3.2 new和delete操作自定义类型
      • 3.2 new申请空间失败
    • 四、new 和 delete 的实现原理
      • 4.1 operator new 与 operator delete函数
      • 4.2 new 和 delete 的实现原理
        • 4.2.1 内置类型
        • 4.2.2 自定义类型
      • 4.3 不匹配使用的后果
        • 4.3.1 内置类型
        • 4.3.2 自定义类型
        • 4.3.3 数组
        • 4.3.4 总结
    • 五、内存泄漏
      • 5.1 内存泄漏的定义
      • 5.2 C++ 的内存泄漏问题
      • 5.3 对比其他语言
      • 5.4 内存泄漏的常见场景对比
      • 5.5 如何避免内存泄漏
        • C++ 的解决方案
        • 其他语言的解决方案
      • 5.6 总结
    • 六、定位new表达式(placement-new)
    • 七、总结:malloc/free和new/delete的区别

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

选择题:

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar在哪里?C

staticGlobalVar在哪里?C

staticVar在哪里?C

localVar在哪里?A

num1 在哪里?A

char2在哪里?A

*char2在哪里?A

pChar3在哪里?A

*pChar3在哪里?D

ptr1在哪里?A

*ptr1在哪里?B

在这里插入图片描述

  1. 又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
  3. 用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段–存储全局数据和静态数据

二、C语言中动态内存管理方式:

在C语言中,我们会用到malloc/calloc/realloc/free这些函数来动态管理内存

简单做道题,回顾一下malloc、calloc、realloc的特性:

void Test ()
{int* p1 = (int*)malloc(sizeof(int));int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);// 这里需要free(p2)吗?free(p1 );free(p3 );
}

ans:

如果是原地扩容,那p2和p3指向的就是同一块空间,p2、p3释放其中一个即可。若是异地扩容,返回p3指向的新空间之前就会自动把原先p2的空间释放掉,所以这里两种情况,我们只释放p3就可以了

面试题:

  1. malloc/calloc/realloc的区别?

    在 C 语言中,malloccallocrealloc 均用于动态内存分配,但功能不同:malloc 分配指定字节的未初始化内存,适用于通用场景;calloc 按元素数量和大小分配内存并初始化为全零,适合需要默认清零的结构或数组;realloc 用于调整已分配内存块的大小(扩展或缩小),可能复制数据到新地址。区别在于 malloc 不初始化且参数为总字节数,calloc 自动初始化但效率略低,而 realloc 可动态伸缩内存,需注意其可能返回新指针,且未扩展区域内容不确定。三者均需配合 free 释放内存,避免泄漏。

关键区别总结

特性malloccallocrealloc
初始化未初始化初始化为零保留原数据(新区域未初始化)
参数形式总字节数元素个数 × 元素大小原指针 + 新字节数
适用场景通用动态分配需要零初始化的结构/数组动态调整内存大小
性能开销低(无初始化)较高(需清零)可能涉及内存复制
内存连续性保证连续保证连续可能改变地址(需重新赋值指针)
//函数原型:
void* malloc (size_t size);//参数:size:内存块的大小(以字节为单位)
//返回值
//成功后,指向函数分配的内存块的指针。
//此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
//如果函数无法分配请求的内存块,则返回 null 指针。void*void* calloc (size_t num, size_t size);//参数:num:需要分配元素的个数,size:每个元素的大小
//返回值
//成功后,指向函数分配的内存块的指针。
//此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
//如果函数无法分配请求的内存块,则返回 null 指针。void*void* realloc (void* ptr, size_t size);//参数ptr:指向先前使用或分配的内存块的指针。或者这可以是 null 指针,在这种情况下,将分配一块新的空间(就像被调用一样)。//返回值:
//指向重新分配的内存块的指针,该块可能与 相同或新位置。
//此指针的类型为 ,可以将其转换为所需的数据指针类型,以便可取消引用。ptrvoid*

三、C++内存管理方式

C语言内存管理方式在C++中可以继续使用,但是C++是面向对象的语言,有些地方就无能为力,而且使用起来比较麻烦,特别是自定义类型的初始化和销毁。因此C++又提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理

3.1new和delete操作内置类型:

void Test()
{// 动态申请一个int类型的空间int* ptr4 = new int;// 动态申请一个int类型的空间并初始化为10int* ptr5 = new int(10);// 动态申请10个int类型的空间int* ptr6 = new int[3];//释放空间delete ptr4;delete ptr5;delete[] ptr6;
}

在这里插入图片描述

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用
new[]和delete[],注意:匹配起来使用。

实现原理:

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


3.2 new和delete操作自定义类型

#include <iostream>
using namespace std;
class A
{
public:A(int a1 = 0, int a2 = 0): _a1(a1), _a2(a2){cout << "A(int a1 = 0, int a2 = 0)" << this << endl;}A(const A& ra): _a1(ra._a1), _a2(ra._a2){cout << "A(const A& ra)" << endl;}~A(){cout << "~A():" << this << endl;}
private:int _a1;int _a2;
};
int main()
{
// new/delete 和 malloc/free最大区别是
// new/delete 对于【自定义类型】除了开空间还会调用构造函数和析构函数A* p1 = (A*)malloc(sizeof(A));A* p2 = new A(1, 2);free(p1);delete p2;// 内置类型是几乎是一样的int* p3 = (int*)malloc(sizeof(int));int* p4 = new int;free(p3);delete p4;//数组:A* p5 = (A*)malloc(sizeof(A) * 10);A* p6 = new A[10];free(p5);delete[] p6;return 0;
}

在这里插入图片描述

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,malloc/free不会。 对于内置类型来说两者没有什么区别

换句话可以说,new/deletemalloc/free的升级版,newdelete其实是为了处理自定义类型而生的!!


3.2 new申请空间失败

失败的原因大概率为虚拟内存空间不足:

在申请失败时,会抛异常

在这里插入图片描述

64位

在这里插入图片描述


四、new 和 delete 的实现原理

4.1 operator new 与 operator delete函数

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

以下为operator new、 operator delete 的源码

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid* 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_FINALLYreturn;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

总的来说就是:operator new 实际也是通过malloc来申请空间如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。


4.2 new 和 delete 的实现原理

4.2.1 内置类型

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

4.2.2 自定义类型
  • new 原理

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

    在这里插入图片描述

  • delete的原理

    1. 在空间上执行析构函数,完成对象中资源的清理工作
    2. 调用operator delete函数释放对象的空间

    在这里插入图片描述

  • new T[N]的原理

    1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
    2. 在申请的空间上执行N次构造函数

    在这里插入图片描述

  • delete[]的原理

    1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间

    在这里插入图片描述


4.3 不匹配使用的后果

4.3.1 内置类型
int main()
{int* p1 = new int[10];free(p1); //正常释放return 0;
}

如果是内置类型,构造函数和析构函数都对内置类型不处理,所以用delete和free没什么区别

4.3.2 自定义类型
  • 不需要资源清理的自定义类型

    class A
    {
    private:int _a1 = 0;
    };
    int main()
    {A* p2 = new A;free(p2);//只释放了空间,没有调析构函数return 0;
    }
    

    自定义类型,构造函数和析构函数都会处理,但如果我们用free,就少调了一次析构函数,但是该类的析构函数并不需要清理资源,所不调用也是无所谓的。

  • 需要资源清理的自定义类型

    class Stack
    {
    public:Stack(){cout << "Stack()" << endl;_a = new int[4];_top = 0;_capacity = 4;}~Stack(){cout << "~Stack()" << endl;delete[] _a;_top = _capacity = 0;}private:int _top;int _capacity;int* _a;
    }int main()
    {Stack* st = new Stack;//delete st;free(st);//没有调析构函数return 0;
    }

    在这里插入图片描述

    这里的Stack类型是有向堆另外申请空间的,当我们使用free来释放空间时,free只会将st所指向的空间释放,而成员变量_a所指向的空间就会泄漏

    但是这个地方编译器并没有报错,这说明编译器对于内存泄露不是很敏感,我们作为程序员要尽量自己去避免这种情况!!

    在这里插入图片描述

    在这里插入图片描述

4.3.3 数组

我们运行一下下面的代码:

class A
{
public:A(char a1 = 0)//默认构造函数: _a1(a1){cout << "A(char a1 = 0)" << this << endl;}~A()//析构函数{cout << "~A():" << this << endl;}
private:char _a1;
};class B
{
public:B(char b1 = 2): _b1(b1){cout << "B(char b1 = 2)" << this << endl;}
private:char _b1;
};int main()
{//int* p1 = new int[10];A* p1 = new A[5];B* p2 = new B[5];//没有匹配使用//free(p1);//直接崩溃//delete p1;//调用了一次析构,然后崩溃//匹配使用delete[] p1;//正常运行//没有匹配使用//free(p2);//正常运行//delete p2;//正常运行//匹配使用delete[] p2;//正常运行return 0;
}

在这里插入图片描述

通过上面的代码,我们发现当我们用new开了一个元素个数为10的A类型数组,开的时候没问题,但当程序走到free的时候却崩溃了?我们知道这是因为没有匹配使用new/delete,但本质上是为什么呢?

问题主要在于new的机制:

在new数组时,编译器会在开数组前,会在数组前面多开四个字节的空间来记录开辟元素的个数,这个记录主要是当我们在用delete[][]中并没有指明有多少个元素,也就不知道要析构多少次。所以就是给后面delete[]用来调用析构函数的

我们分别看一下A、B类型的空间创建和销毁的过程

  • 有析构函数的A类型的数组:

    在这里插入图片描述

    在这里插入图片描述

    对比上面的Stack类型,虽然Stack类型内存泄漏,但他却没有直接程序崩溃。而这里就直接崩溃了。检查机制是由编译器决定的,所以在不同的编译器下,对于空间泄漏的检查都会有所差别,所以我们为了减少这种偶然性的问题,我们就必须匹配使用new/delete,有空间资源申请的自定义类型,就必须要写析构函数,从而更加好的管理内存。

  • 没有析构函数的B类型的数组:

    在这里插入图片描述

    因为没有析构函数,在销毁空间时也不会调用,所以就不许要多开4个字节

    在这里插入图片描述

4.3.4 总结
  1. 对于内存泄漏问题,不同编译器的检查机制是不一样的。
  2. 不匹配使用new/delete的风险是不可控制的,我们必须要做到匹配使用
  3. 对于有空间申请,需要用到资源清理的自定义类型,必须要写好析构函数,否则会导致内存泄漏。

五、内存泄漏

C++ 的内存泄漏问题与其他语言(如 Java、Python、C#、Rust 等)有显著差异,主要源于其 手动内存管理 的特性。


5.1 内存泄漏的定义

内存泄漏(Memory Leak):程序在动态分配内存后,未能正确释放已不再使用的内存,导致系统可用内存逐渐减少,最终可能引发性能下降或崩溃。


5.2 C++ 的内存泄漏问题

原因

  • 手动管理内存:C++ 中需显式调用 new/malloc 分配内存,delete/free 释放内存。
  • 未释放的场景
    • 忘记调用 delete
    • 异常导致未执行释放代码。
    • 指针所有权不清晰(如多个指针指向同一内存,重复释放或未释放)。
    • 容器或对象内部未正确释放资源。

示例

void leak() {int* ptr = new int[100];  // 分配内存// ...使用 ptr...// 不匹配使用delete,或者忘记使用
}

后果

  • 长期运行的程序(如服务器、嵌入式系统)会逐渐耗尽内存。
  • 调试困难:泄漏点可能隐蔽,需借助工具(如 Valgrind、AddressSanitizer)检测。

5.3 对比其他语言

(1) 手动管理内存的语言(如 C)

  • 相似性:与 C++ 类似,需手动管理 malloc/free
  • 差异:C++ 的构造函数/析构函数、异常机制可能增加泄漏风险(如构造函数抛出异常导致未执行析构函数)。

(2) 半自动管理语言(如 Objective-C、Rust)

  • Objective-C(ARC)

    • 自动引用计数(ARC):编译器自动插入 retain/release 代码,减少泄漏。
    • 循环引用:仍需开发者处理(通过 weak 引用)。
  • Rust

    • 所有权系统:编译器静态检查内存生命周期,确保无泄漏(除非显式使用 unsafe)。
    • 无垃圾回收:通过所有权、借用规则实现内存安全。

(3) 自动垃圾回收语言(如 Java、Python、C#、Go)

  • 垃圾回收(GC):内存由运行时自动回收,开发者无需手动释放。
    • 优点:几乎无需担心内存泄漏。
    • 缺点
      • 不可控性:GC 触发时机不确定,可能引发短暂停顿。
      • 循环引用:某些语言(如 Python)的 GC 能处理循环引用,但需依赖算法(如分代回收)。
      • 非内存资源泄漏:如文件句柄、数据库连接仍需手动释放(需 try-with-resourcesusing 语句)。

(4) 智能指针语言(现代 C++)

  • C++11 引入的智能指针

    • std::unique_ptr:独占所有权,自动释放内存。
    • std::shared_ptr:共享所有权,引用计数为 0 时释放。
    • std::weak_ptr:解决 shared_ptr 的循环引用问题。
  • 示例

    void no_leak() {auto ptr = std::make_unique<int[]>(100); // 自动释放
    }
    
  • 优点:大幅减少泄漏风险,但需正确使用(如避免循环引用)。


5.4 内存泄漏的常见场景对比

语言泄漏风险主要原因典型场景
C++手动管理、异常、复杂对象生命周期未配对的 new/delete
C类似 C++,但无异常和析构函数malloc 后忘记 free
Java循环引用或未关闭资源静态集合持有对象、未关闭流
Python循环引用(需 GC 支持)复杂对象引用关系
Rust极低所有权系统强制安全需显式使用 unsafe 代码
C#非托管资源未释放未调用 Dispose() 的文件操作

5.5 如何避免内存泄漏

C++ 的解决方案
  1. 优先使用智能指针:替代原始指针。
  2. RAII 原则:资源获取即初始化(如通过构造函数分配,析构函数释放)。
  3. 工具检测
    • Valgrind:检测内存泄漏和越界访问。
    • AddressSanitizer(ASan):实时检测内存错误。
  4. 避免裸指针:仅在必要时使用(如与 C 库交互)。
其他语言的解决方案
  • Java/Python:依赖 GC,但需注意非内存资源(如文件、网络连接)的手动释放。
  • Rust:依赖编译器检查,几乎无需手动管理。
  • C#:使用 using 语句或 IDisposable 接口释放资源。

5.6 总结

  • C++ 的内存泄漏风险最高:源于手动管理和复杂生命周期。
  • 其他语言通过自动管理降低风险
    • GC 语言:以运行时开销为代价,简化内存管理。
    • Rust:通过编译时检查实现零成本安全性。
  • 现代 C++ 的最佳实践:智能指针 + RAII + 工具检测,可接近自动管理语言的安全性。

通过理解不同语言的内存管理机制,可以更好地规避泄漏问题,选择适合场景的工具和策略。


六、定位new表达式(placement-new)

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

使用格式:

//不带参
new(place_address)type;
//带参
new(place_address) type(initializer-list);
//       |         |           |
//      地址       类型      类型的初始化列表
#include <iostream>
using namespace std;class A
{
public:A(int a = 0): _a(a){cout << "A():" << this << endl;}~A(){cout << "~A():" << this << endl;}void Printt(){cout << _a << endl;}
private:int _a;
};
// 定位new/replacement new
int main()
{// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行A* p1 = (A*)malloc(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象cout << "p1:" << p1 << endl;new(p1)A; // 注意:如果A类的构造函数不是默认构造函数时,就必须要传参。但这里是默认构造,所以可以不用传参。p1->Printt();//资源清理p1->~A();free(p1);//delete p1;//因为定位出了对象,所以也可以通过delete释放空间,当然最好还是要匹配使用cout << endl;A* p2 = (A*)operator new(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象cout << "p2:" << p2 << endl;new(p2)A(10);//带参数构造出A对象p2->Printt();//资源清理p2->~A();operator delete(p2);//delete p2;return 0;
}

在这里插入图片描述

new就可以完成初始化这个工作,那为什么我们还要用malloc、再用定位new初始化,这不是多此一举吗??

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

在有些场景下,我们需要不断地new一块新区域,这个过程是和操作系统上的堆完成交互的,如果我们不断地交互,其实是很麻烦的,为了优化性能,有了内存池这个概念,也就是我们先在堆上一次性malloc出一大块的区域,之后如果是处理小数据,我就不需要一直跟操作系统交互,只需要在我们的内存池里面拿空间就可以了,再用定位new去初始化!!这样减少了对象之间的交互,提高了效率。

在这里插入图片描述


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

malloc/freenew/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。

不同的地方是

  1. mallocfree函数newdelete操作符
  2. malloc申请的空间不会初始化,new可以初始化
  3. malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
  4. . malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
  5. malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
    在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
http://www.xdnf.cn/news/665821.html

相关文章:

  • 【DAY30】模块和库的导入
  • Docker Volume(存储卷)
  • 动态库版本不配问题排查步骤
  • 牛客round94D
  • java使用https协议访问(自签名证书,运行时指定信任库(不修改系统证书))
  • 城市污水管网流量在线监测方案
  • VPet虚拟桌宠,一款桌宠软件,支持各种互动投喂等. 开源免费并且支持创意工坊
  • 如何搭建perfino监控(分析java服务性能)
  • 从姿势到心态:痉挛性斜颈的多维护理方案
  • old语音识别科大讯飞+deepseek api
  • SOC-ESP32S3部分:13-定时器
  • 删掉省市区的市辖区
  • 推理模型 vs 非推理模型:核心区别及优劣势解析
  • 3.微服务架构编码Base工程模块构建
  • 【stm32开发板】产品设计流程及元件选型
  • 创业团队建设与管理(一)
  • 牛客round94E
  • 「Unity3D」TextMeshPro的TMP_InputField在改变高度时,其中textComponent移动的问题解决
  • VMware Live Recovery 和 VMware Data Recovery区别
  • python 报错记录-Linux 退出python环境
  • Python Day34
  • 聚合CPA/CPS拉新分销平台开发:2025年核心功能与未来趋势解析
  • HarmonyOS运动开发:如何绘制运动速度轨迹
  • day 22 练习——泰坦尼克号幸存者预测
  • Dify中的GoogleSearch工具插件开发例子
  • 华为OD机试真题——新工号中数字的最短长度(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
  • 【AI论文】LLaDA-V:具备视觉指令微调能力的大型语言扩散模型
  • 基于 LoRA 和 GRPO 的 Qwen2.5-3B 数学推理模型微调示例
  • java学习日志——Spring Security介绍
  • 二维坐标变换、三维坐标变换、综合变换