跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 内存管理:new/delete 操作自定义类型的构造与析构原理

C++ 内存管理中 new/delete 操作符相比 malloc/free 的优势在于自动调用构造函数和析构函数。文章详细讲解了 C/C++ 内存分布、动态内存管理方式、operator new/delete 底层实现、定位 new 表达式以及两者的区别。重点分析了 new/delete 在处理内置类型和自定义类型时的不同行为及底层原理。

292440837发布于 2026/2/8更新于 2026/10/2499 浏览
C++ 内存管理:new/delete 操作自定义类型的构造与析构原理

文章配图

一、C/C++的内存分布

1、内存分布问题

#include <iostream>
using namespace std;

// C/C++内存分布问题
int globalVar = 1;
static int staticGlobalVar = 1;

int main() {
    static int staticVar = 1;
    int localVar = 1;
    int num1[10] = { 1, 2, 3, 4 };
    char char2[] = "abcd";
    const char* pChar3 = "abcd";
    *char2 += 1;
    //(*(char*)pChar3) += 1;
    int* ptr1 = (int*)malloc(sizeof(int) * 4);
    int* ptr2 = (int*)calloc(4, sizeof(int));
    int* ptr3 = (*)(ptr2, () * );
    (ptr1);
    (ptr3);
      i = ;
     j = ;
    cout << &i << endl;
    cout << &j << endl;
    cout << (*)pChar3 << endl;
     ;
}
int
realloc
sizeof
int
4
free
free
const
int
10
int
1
void
return
0

知识点:

  • globalVar 在哪里? C (数据段)
  • staticGlobalVar 在哪里? C (数据段)
  • staticVar 在哪里? C (数据段)
  • localVar 在哪里? A (栈)
  • num1 在哪里? A (栈)

解析: 前五个都比较好区分,因为数据段是存放全局数据和静态数据的,所以 globalVar、staticGlobalVar 以及 staticVar 均在数据段,localVar 和 num1 为局部变量是存放在栈区中。

  • char2 在哪里? A (栈)
  • *char2 在哪里? A (栈)
  • pChar3 在哪里? A (栈)
  • *pChar3 在哪里? D (代码段/常量区)
  • ptr1 在哪里? A (栈)
  • *ptr1 在哪里? B (堆)

解析: 这五个就需要好好讲解了。首先我们要知道局部变量就是在栈区进行创建的,所以 char2、pChar3、ptr1 这三个局部变量都是存放在栈区。

对于 char2:char char2[] = "abcd"; 我们学习了类和对象把这个代码在语义上可以理解为 C++ 中的拷贝构造,char2 本身作为局部变量存放在栈区,编译器会把常量区的这四个字符 (a、b、c、d) 以及 \0 逐个拷贝到栈上的 char2 数组分配的内存空间。

对于 pChar3:由于 pChar3 本身就是字符指针,存放的就是字符串的地址,这里就没有拷贝一说,这样其实就能和 C++ 中的引用结合来理解,这也是为什么 const char* pChar3 = "abcd"; 要加 const 进行修饰了,引用其实也能理解为地址,本质还是因为两者地址是相同的,如果不加上 const 进行修饰也就会导致权限放大而报错。所以对 pChar3 进行解引用指向的位置就是常量区的字符串的首元素。

对于 ptr1:ptr1 就比较简单了,我们之前就讲过动态开辟的空间是存放在堆区的,由于 ptr1 本身就是整型指针,存放的就是动态开辟的空间的地址,对其进行解引用后指向的位置就是堆区中动态开辟的空间的首元素。

具体各个变量所在位置可以参考下图:

文章配图

2、概念说明

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

二、C/C++中动态内存管理方式解析

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

C 语言中的动态内存管理方式我们之前就学过,这里就不再过多赘述里面的相关知识点了。

我们直接来看一段代码并回答一下相关问题:

#include <iostream>
using namespace std;

int main() {
    // 1.malloc/calloc/realloc的区别是什么?
    int* p2 = (int*)calloc(4, sizeof(int));
    int* p3 = (int*)realloc(p2, sizeof(int) * 50);
    cout << p2 << endl;
    cout << p3 << endl;
    // 这里需要 free(p2) 吗?
    free(p3);
    return 0;
}

先回答一下第二题:是否需要 free(p2)? 是不需要 free(p2)。因为 realloc 扩容分两种情况:

  1. 原地扩容:p2 和 p3 两者本身地址就是同一块地址,free 了 p3 时 p2 也就跟着被 free 了;
  2. 异地扩容:p2 和 p3 指向不同的地址,但原来 p2 指向的内存块已经被 realloc 自动释放了,也不需要 free p2。
相关知识点

1. malloc/calloc/realloc 的区别?

  • malloc:分配指定字节内存,不初始化,数据随机。
  • calloc:分配内存并初始化为 0,需指定元素个数和单个元素字节数。
  • realloc:调整已分配内存大小,可扩容或缩容,可能复用原内存或重新分配并复制数据,有两种情况,原地扩容或者异地扩容。

2. malloc 的实现原理? 基于 brk(调整数据段地址)和 mmap(映射新内存区域)系统调用,结合内存池管理(按大小分类维护空闲块,分配时快速匹配,减少系统调用开销)。

2、C++中内存管理方式

C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。

2.1 new/delete操作内置类型
int main() {
    int* p1 = new int;      // 单个对象 (未初始化)
    int* p2 = new int[10];  // 数组 (未初始化)
    delete p1;
    delete[] p2;
    return 0;
}
int main() {
    // 申请对象 + 初始化
    int* p3 = new int(5);           // 单个对象 (初始化为 5)
    int* p4 = new int[10] {1, 2, 3, 10}; // 数组 (初始化为 {1, 2, 3, 10},后面默认初始化为 0)
    delete p3;
    delete[] p4;
    return 0;
}

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

大家可能会觉得这些操作我们之前 C 语言中使用的 malloc 等操作也可以实现,虽然没这么方便,但也不是完全不行,那 C++ 到底为什么要引入新的 new/delete 操作符呢? 只针对于上面的内置类型的确没有意义,但对于 C++ 中的类类型而言就非常有意义了。

2.2 new/delete操作自定义类型 (重点掌握)
2.2.1 new 创建单个对象
class A {
public:
    A(int a = 0) : _a(a) { cout << "A():" << this << endl; }
    ~A() { cout << "~A():" << this << endl; }
private:
    int _a;
};

int main() {
    // malloc 只开空间,不会去自动调用构造进行初始化,不太好使
    A* p1 = (A*)malloc(sizeof(A)); 
    // new 操作符在动态开辟空间的同时,对于类类型而言还会自动调用对应的构造进行初始化
    A* pa1 = new A;
    A* pa2 = new A(1); 
    // delete 操作符会自动调用析构函数
    delete pa1;
    delete pa2;
    return 0;
}

并且对于之前数据结构学过的创建链表而言,new/delete 操作符的作用相比 malloc 会显得尤为突出:

class ListNode {
public:
    ListNode(int val = 0)//链表的构造函数
        :_val(val), _next(nullptr) { }
    int _val;
    ListNode* _next;
};

int main() {
    // 创建链表
    ListNode* n1 = new ListNode(1); // 用 new 在动态开辟空间的同时还能自动调用构造函数进行初始化
    ListNode* n2 = new ListNode(2);
    ListNode* n3 = new ListNode(3);
    ListNode* n4 = new ListNode(4);
    n1->_next = n2;
    n2->_next = n3;
    n3->_next = n4;
    // 相较于之前创建链表既要写一个开辟空间的函数还要写一个初始化的函数 new 操作符进行了极大的优化
    return 0;
}
2.2.2 创建对象数组

第一种方法:

class A {
public:
    A(int a = 0) : _a(a) { cout << "A(int a)" << this << endl; }
    ~A() { cout << "~A()" << this << endl; }
    A(const A& ra) { cout << "A(const A& ra)" << this << endl; }
private:
    int _a;
};

int main() {
    A a1 = A(1);
    A a2 = A(2);
    A a3 = A(3);
    // 第一种写法 (比较繁琐而且不能进行优化,一般不会用)
    A* p1 = new A[3]{ a1, a2, a3 }; 
    // new A[3] 会先创建 3 个数组元素 (A 对象),通过 a1、a2、a3 分别给这 3 个元素进行初始化
    // 此时就会调用拷贝构造函数 (形参为类类型)
    delete p1;
    return 0;
}

第二种写法 (匿名对象):

class A {
public:
    A(int a = 0) : _a(a) { cout << "A(int a)" << this << endl; }
    ~A() { cout << "~A()" << this << endl; }
    A(const A& ra) { cout << "A(const A& ra)" << this << endl; }
private:
    int _a;
};

int main() {
    // 第二种写法
    A* p2 = new A[3]{ A(1), A(2), A(3) }; 
    // 通过匿名对象可实现连续构造 + 拷贝构造 -> 编译器会优化为直接构造
    delete []p2;
    return 0;
}

第三种写法 (隐式类型转换):

class A {
public:
    A(int a = 0) : _a(a) { cout << "A(int a)" << this << endl; }
    ~A() { cout << "~A()" << this << endl; }
    A(const A& ra) { cout << "A(const A& ra)" << this << endl; }
private:
    int _a;
};

int main() {
    // 第三种写法
    // A aa1 = 1;//隐式类型转换
    A* p3 = new A[3]{ 1, 2, 3 }; 
    // 先创建 3 个数组元素 (A 对象),通过 1, 2, 3 分别给这 3 个元素进行初始化
    // 此时就会进行隐式类型转换
    // 和第二种方法一样,会进行连续构造 + 拷贝构造 -> 编译器会优化为直接构造
    delete []p3;
    return 0;
}
2.3 new 申请空间失败时的处理

对于 C 语言中动态开辟空间的 malloc 函数在空间申请失败后会返回 NULL,而 new 在申请空间失败时会抛异常。

// 1024 * 1024 相当于就是 1 兆 (1MB)
void func() {
    int i = 1;
    int* ptr = nullptr;
    do {
        ptr = new int[1024 * 1024]; // throw
        cout << i++ << ":" << ptr << endl;
    } while (ptr);
    cout << i++ << ":" << ptr << endl;
}

int main() {
    try {
        func(); // 可以直接跳跃函数到异常
    } catch (const exception& e) {
        cout << e.what() << endl; // 打印错误信息
    }
    return 0;
}

通过打印的结果就能发现当前所能申请到的最大空间约为 9 个 G。

三、operator new 与 operator delete 函数

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

1、operator new 函数

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

2、operator delete 函数

/* 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 实际也是通过 malloc 来申请空间,如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。

四、new 和 delete 的底层实现原理

1、内置类型

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

2、自定义类型

2.1 new 的原理
  1. 先调用 operator new 函数申请空间
  2. 再在申请的空间上执行构造函数,完成对象的构造
2.2 delete 的原理
  1. 先在空间上执行析构函数,完成对象中资源的清理工作
  2. 再调用 operator delete 函数释放对象的空间
2.3 new T[N] 的原理
  1. 先调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请
  2. 再在申请的空间上执行 N 次构造函数
2.4 delete[] 的原理
  1. 先在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理
  2. 再调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间

五、定位 new 表达式 (placement-new)(了解即可)

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

使用格式:

new (place_address) type 或者 new (place_address) type(initializer-list)

place_address 必须是一个指针,initializer-list 是类型的初始化列表

使用场景:

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

class A {
public:
    A(int a = 0) : _a(a) { cout << "A(int a)" << this << endl; }
    ~A() { cout << "~A()" << this << endl; }
private:
    int _a;
};

// 定位 new 表达式 (placement-new) 的使用
int main() {
    // p1 现在指向的只不过是与 A 对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    A* p1 = (A*)operator new(sizeof(A)); 
    // 显示调用构造函数
    new(p1)A(1); // 有参数就需要传,这里其实可以传也可以不传,看你实现的构造函数
    p1->~A(); // 析构可以直接这样用,构造不行; 
    // p1->A(); // error C7624: 类型名称'A'不能出现在类成员访问表达式的右侧
    operator delete(p1);
    return 0;
}

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

1、共同点

都是从堆上申请空间,并且需要用户手动释放。

2、不同点

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

目录

  1. 一、C/C++的内存分布
  2. 1、内存分布问题
  3. 2、概念说明
  4. 二、C/C++中动态内存管理方式解析
  5. 1、C语言中动态内存管理方式:malloc/calloc/realloc/free
  6. 相关知识点
  7. 2、C++中内存管理方式
  8. 2.1 new/delete操作内置类型
  9. 2.2 new/delete操作自定义类型 (重点掌握)
  10. 2.2.1 new 创建单个对象
  11. 2.2.2 创建对象数组
  12. 2.3 new 申请空间失败时的处理
  13. 三、operator new 与 operator delete 函数
  14. 1、operator new 函数
  15. 2、operator delete 函数
  16. 四、new 和 delete 的底层实现原理
  17. 1、内置类型
  18. 2、自定义类型
  19. 2.1 new 的原理
  20. 2.2 delete 的原理
  21. 2.3 new T[N] 的原理
  22. 2.4 delete[] 的原理
  23. 五、定位 new 表达式 (placement-new)(了解即可)
  24. 六、malloc/free 和 new/delete 的区别
  25. 1、共同点
  26. 2、不同点

更多推荐文章

查看全部
  • 前端无障碍性开发指南:提升网站可访问性
  • 无人机路径规划算法详解:原理、场景与优化趋势
  • CppSharp 完全指南:5 步实现 C++ 到.NET 的自动化绑定
  • 分治归并排序核心原理及 LeetCode 实战
  • Python 核心应用场景:Web 开发、数据科学与自动化实战
  • Axure 制作 AI 自动对话机器人原型教程
  • OpenClaw Webhook 详解:配置与实战
  • 分布式文件系统 HDFS 编程实践
  • 动态规划:斐波那契数列类题目解析
  • 产品经理如何掌握 AI:实用学习指南
  • 策略模式实战:通过组合实现算法的灵活解耦
  • Windows 下 Git 常规操作:命令行与 TortoiseGit
  • 自然语言处理在金融领域的应用与实战
  • 基于 WebRTC+AI 的智能远程控制解决方案
  • C++11 关键新特性回顾与实践
  • 双指针算法实战:移动零与复写零详解
  • 互联网大厂 Android 面试题大全与核心知识点解析
  • C++ 实现:基于正倒排索引的 Boost 搜索引擎
  • AI 调参技巧:贝叶斯优化与 Optuna 实战
  • Claude Code 与 OpenSpec 环境搭建及 AI 编码提效实测

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online