详细介绍:从C内存管理进阶到C++内存管理(中)-new与delete详解

目录

从C内存管理进阶到C++内存管理(中)-new与delete详解

1.C++内存管理方式

1.1 new/delete 操作内置类型

1.2 new 和 delete 操作自定义类型

2. operator new 与 operator delete 函数

2.1 operator new 与 operator delete 函数(重点)

operator new 函数

operator delete 函数

总结与关系梳理

3. new 和 delete 的实现原理

3.1 内置类型

3.2 自定义类型

new 的原理

delete 的原理

3.3 数组版本:new[] 和 delete[] 的原理

new T[N] 的原理

delete[] 的原理

4. 定位new表达式

4.1 基本概念和使用

4.2 在游戏开发中的重要意义

1. 内存池和对象池

2. 性能优化原理

4.3 使用场景总结

4.4 注意事项

5. 常见重要问题

5.1 malloc/free和new/delete的区别

5.2 内存泄漏

5.2.1 什么是内存泄漏,内存泄漏的危害

5.2.2 内存泄漏分类

5.2.3 如何检测内存泄漏

5.2.4 如何避免内存泄漏

在下篇我们将重点介绍智能指针


1.C++内存管理方式

C语言中,我们使用 malloc, calloc, reallocfree 来进行动态内存管理。这些函数在C++中依然可以继续使用,但它们并非为C++而生。在处理C++的自定义类型(如类对象) 时,它们显得力不从心,而且使用起来也比较麻烦(需要计算大小、类型转换等)。

因此,C++提出了自己的内存管理方式:通过 newdelete 操作符进行动态内存管理。这不仅更简洁,更重要的是,它能与C++的对象生命周期完美结合。


1.1 new/delete 操作内置类型

我们先来看看如何使用 newdelete 来操作 int, double 这样的内置类型。

C语言方式:

void Test() {
    // 申请一个int大小的空间
    int* p1 = (int*) malloc(sizeof(int));
    // 释放
    free(p1);
​
    // 申请4个int大小的空间,并初始化为0
    int* p2 = (int*)calloc(4, sizeof(int));
    // 将p2的空间重新调整为10个int大小
    int* p3 = (int*)realloc(p2, sizeof(int) * 10);
    
    // 问题:这里需要free(p2)吗?
    // 答案:不需要!realloc成功后,p2指向的旧空间已经被自动释放。
    // 如果realloc失败返回NULL,则p2仍然有效,但此例中我们直接使用了p3,所以只需要free(p3)。
    free(p3);
}

C++方式:

void Test() {
    // 动态申请一个int类型的空间(未初始化)
    int* ptr4 = new int;
​
    // 动态申请一个int类型的空间并初始化为10,()用来初始化
    int* ptr5 = new int(10);
​
    // 动态申请10个int类型的空间(未初始化),[]用来设定大小
    int* ptr6 = new int[10];
​
    // 释放空间
    delete ptr4;
    delete ptr5;
    delete[] ptr6; // 注意这里使用的是 delete[]
}

重要知识点与注意事项:

  1. 语法更简洁new 后面直接跟类型,不需要计算大小 sizeof(int),也不需要强制类型转换 (int*)

  2. 支持初始化:对于单个变量,可以使用 new int(10) 的方式在分配内存的同时进行初始化。这是 malloc 做不到的。

  3. 严格匹配原则

    • 申请和释放单个元素的空间,使用 newdelete

    • 申请和释放连续的空间(数组),使用 new[]delete[]

    • 必须匹配使用! 如果使用 new[] 分配,却用 delete 释放(而不是 delete[]),对于内置类型可能不会立即出错,但对于自定义类型,其行为是未定义的,通常会导致程序崩溃或内存泄漏。这是一个非常常见的错误根源。

      还有一个小知识点,比较冷门:那就是new/delete属于操作符,而malloc/realloc这些属于库函数


1.2 new 和 delete 操作自定义类型

这才是 new/deletemalloc/free 最根本、最重要的区别。让我们通过一个类 A 来理解。

#include 
using namespace std;
​
class A {
public:
    A(int a = 0) : 
    _a(a) {
        cout << "A():" << this << " a=" << _a << endl;
    }
    ~A() {
        cout << "~A():" << this << endl;
    }//利用打印,使析构和构造函数被调用时可以显示的被我们看到
private:
    int _a;
};
​
int main() {
    cout << "malloc vs new for A" << endl;
​
    // 1. 使用malloc:只分配内存,不会调用构造函数
    A* p1 = (A*)malloc(sizeof(A));
    cout << "p1 points to: " << p1 << endl; // 对象未构造,_a是随机值
    //以及使用malloc,我们还要强制转换,和使用sizeof计算大小
    
    
    // 2. 使用new:分配内存,并调用构造函数进行初始化
    A* p2 = new A(1); // 会输出 "A():... a=1"
​
    free(p1);  // 只释放内存,不会调用析构函数
    delete p2; // 先调用析构函数,再释放内存
///////////////////////////////////////////////
//////////再往下是两者对于开辟连续空间存储自定义类型的比较/////////
​
​
    cout << "\n malloc vs new for A[10] " << endl;
​
    // 3. 使用malloc分配数组:同样不调用构造函数
    A* p5 = (A*)malloc(sizeof(A) * 10);
​
    // 4. 使用new[]分配数组:为每个元素调用构造函数
    A* p6 = new A[10]; // 会输出10次 "A():... a=0" (使用默认参数)
​
    free(p5);   // 不调用任何析构函数
    delete[] p6; // 为每个元素调用析构函数,会输出10次 "~A():..."
​
    return 0;
}

运行上述代码,你会看到类似以下的输出:

malloc vs new for A 
p1 points to: 0x... (某个地址)
A():0x... a=1
~A():0x...
​
 malloc vs new for A[10] 
A():0x... a=0
A():0x... a=0
... (共10次)
~A():0x...
~A():0x...
... (共10次)

核心区别与重要知识点:

  1. 构造与析构的自动调用

    • new:在堆上分配内存后,会自动调用该类型的构造函数来初始化对象。

    • delete:在释放堆内存之前,会自动调用该对象的析构函数来清理资源(如关闭文件、释放其他内存等)。

    • mallocfree仅仅负责内存的分配和释放,与构造、析构函数无关。

  2. 为什么这个区别如此重要? 想象一下,如果你的类在构造函数中打开了一个文件,或者在析构函数中关闭了它。使用 malloc 创建的对象,它的构造函数根本没被调用,文件自然没有打开。后续操作可能会失败。更糟糕的是,使用 free 释放这个对象,析构函数也不会被调用,导致文件句柄等资源泄漏。

    因此,在C++中,对于自定义类型,必须使用 newdelete这是防止出现任何意外错误的必须手段。

2. operator new 与 operator delete 函数

2.1 operator new 与 operator delete 函数(重点)

在理解C++内存管理时,有一个重要的概念需要厘清:new/delete 是操作符,而 operator new/operator delete 是函数

  • newdelete:是C++的关键字,用户直接使用的操作符。(在上文有简单提到)

  • operator newoperator delete:是系统提供的全局函数

它们之间的关系是:new 在底层调用 operator new 函数来申请空间,delete 在底层通过 operator delete 函数来释放空间。

让我们通过分析源码(简化版,不必强求理解,重点关注注释与流程)来理解这两个全局函数的工作原理。

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) {
        // 如果malloc失败,尝试调用_new_handler(用户可能设置的内存不足处理函数)
        if (_callnewh(size) == 0) {
            // 如果用户没有设置处理函数,或者处理函数无法解决内存不足问题,则抛出bad_alloc异常
            static const std::bad_alloc nomem;
            _RAISE(nomem);
        }
    }
    return (p);
}

关键点解析:

  1. 底层调用mallocoperator new 的最终目的仍然是向操作系统申请内存,它通过调用 C 语言的 malloc 函数来实现。

  2. 失败处理机制:这是与 malloc 最大的不同!

    • malloc:申请失败时返回 NULL 指针。

    • operator new:申请失败时,它会先尝试调用一个名为 _callnewh 的函数(这对应着用户可能通过 set_new_handler 设置的函数)。如果这个处理函数能够释放出一些内存,它就会重试 malloc。如果无法解决,则抛出 std::bad_alloc 异常

  3. 返回值:成功时返回 void* 指针,与 malloc 一致。

这意味着,使用 new 时你不需要检查返回值是否为 NULL,而应该使用 try-catch 来捕获异常。

不过在学习过程中我们会比较少见抛异常的场景,这有多种原因如现代C++更推荐使用智能指针和容器,内存分配失败通常难以恢复(连基本内存分配也失败,程序寸步难行),现代操作系统内存管理更健壮(现代系统有虚拟内存,内存压缩等机制,抛异常的情况很少)等等

// C风格:检查返回值
int* p1 = (int*)malloc(sizeof(int));
if (p1 == NULL) {
    // 处理错误
}
​
// C++风格:捕获异常
try {
    int* p2 = new int;
    // 使用 p2
} catch (const std::bad_alloc& e) {
    std::cerr << "Memory allocation failed: " << e.what() << std::endl;
    // 处理错误
}
operator delete 函数
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData) {
    // ... (一些调试和线程安全代码)
    if (pUserData == NULL)
        return;//如果传了空指针,他会直接返回
    
    // 最终调用free释放内存
    _free_dbg(pUserData, _NORMAL_BLOCK); // 在调试模式下,_free_dbg 最终会调用 free
}

关键点解析:

  1. 底层调用freeoperator delete 的最终目的是释放内存,它通过调用 C 语言的 free 函数来实现。

  2. 空指针安全:和 free 一样,传递一个 NULL 指针给 operator delete 是安全的,它会直接返回。

总结与关系梳理

让我们用一个流程图来理清 newdelete 与这些底层函数、操作符的关系:

// new 的分解动作
new Type; 
↓ (编译器转化为)
1. void* memory = operator new(sizeof(Type)); // 分配内存,可能抛异常
2. Type* obj = static_cast(memory);
3. obj->Type::Type();                         // 调用构造函数
​
// delete 的分解动作
delete ptr;
↓ (编译器转化为)
1. ptr->~Type();                             // 调用析构函数
2. operator delete(ptr);                     // 释放内存

重要结论:

  1. operator new/operator delete 只负责内存的分配和释放,它们本身不会调用构造函数和析构函数。调用构造和析构是 new/delete 表达式本身的职责。

  2. operator new 失败时抛异常,malloc 失败时返回 NULL。这是C++面向对象错误处理机制(异常)的体现。

  3. 你可以重载 operator newoperator delete(无论是全局的还是针对特定类的),来自定义内存分配策略,这是实现内存池、性能优化等高级技巧的基础。而 malloc/free 的行为是固定的。

  4. 我们可以知道new/delete在底层也会调用malloc/free,这正是我们在上篇学习它们的意义

记住这个核心关系,你就掌握了C++动态内存管理的基石。

3. new 和 delete 的实现原理

理解了 operator newoperator delete 之后,我们就能更深入地分析 newdelete 表达式本身的实现原理了。

3.1 内置类型

对于内置类型(如 int, double, char 等),new/delete 的行为相对简单:

// 对于内置类型
int* p1 = new int;        // 分配一个int大小的空间
int* p2 = new int(42);    // 分配空间并初始化为42
int* p3 = new int[10];    // 分配10个int的连续空间
​
delete p1;                // 释放单个int
delete p2;
delete[] p3;              // 释放int数组

与 malloc/free 的对比:

特性malloc/freenew/delete
内存分配malloc(sizeof(int))new int
初始化不能直接初始化new int(42) 可以初始化
失败处理返回 NULL抛出 std::bad_alloc 异常
语法简洁性需要计算大小和类型转换直接使用类型名

关键点:

  • 对于内置类型,new/delete 主要提供了更好的语法和初始化支持

  • 失败时的异常机制是主要区别

  • 仍然要严格匹配 new/deletenew[]/delete[]

3.2 自定义类型

这才是 new/delete 真正发挥价值的地方。让我们通过分解动作来理解其原理。

new 的原理
// 当我们写:
A* p = new A(10);
​
// 编译器实际上会将其转换为:
void* memory = operator new(sizeof(A));  // 1. 分配内存
A* p = static_cast(memory);          // 转换指针类型
p->A::A(10);                             // 2. 调用构造函数
delete 的原理
// 当我们写:
delete p;
​
// 编译器实际上会将其转换为:
p->~A();                   // 1. 调用析构函数
operator delete(p);         // 2. 释放内存
3.3 数组版本:new[] 和 delete[] 的原理

对于对象数组,过程更加复杂,因为需要跟踪数组大小。

new T[N] 的原理
// 当我们写:
A* arr = new A[3];
​
// 编译器实际上会:
// 1. 调用 operator new[] 分配额外空间存储数组大小 + N个对象空间
// 2. 在分配的空间上构造N个对象

实际的内存布局可能是:

[数组大小信息][对象0][对象1][对象2]...
delete[] 的原理
// 当我们写:
delete[] arr;
​
// 编译器实际上会:
// 1. 获取数组大小(从存储数组时额外存储的信息中获取)
// 2. 对每个元素调用析构函数(逆序)
// 3. 调用 operator delete[] 释放整个内存块

演示数组操作的内部机制:

#include 
​
class Tracked {
public:
    Tracked(int id) : id_(id) {
        std::cout << "Constructing Tracked #" << id_ << std::endl;
    }
    
    ~Tracked() {
        std::cout << "Destructing Tracked #" << id_ << std::endl;
    }
    
private:
    int id_;
};
​
int main() {
    std::cout << "=== Creating array of 3 objects ===" << std::endl;
    Tracked* arr = new Tracked[3]{1, 2, 3};//注意我们构造的顺序
    
    std::cout << "\n=== Deleting array ===" << std::endl;
    delete[] arr;  // 注意:必须使用 delete[] 而不是 delete
    
    return 0;
}

输出:

=== Creating array of 3 objects ===
Constructing Tracked #1
Constructing Tracked #2
Constructing Tracked #3
​
=== Deleting array ===
Destructing Tracked #3
Destructing Tracked #2
Destructing Tracked #1//可以看见析构顺序和构造顺序恰好相反

重要注意事项:

  1. 严格匹配原则newdeletenew[]delete[]

  2. 不匹配的后果

    • 对于内置类型:可能不会立即崩溃,但行为未定义

    • 对于自定义类型:一定会出现问题,因为析构函数调用次数不对

      析构是逆序的,因为如果我们先构造的先析构,那么如果后构造的对象是依赖于先构造的对象创建的,就有可能出错。因此我们是后构造的先析构,这不仅仅是因为这里的delete的特性,而是在类和对象中就是如此

4. 定位new表达式

4.1 基本概念和使用

定位new允许我们在已分配的原始内存空间中调用构造函数初始化对象。

基本语法:

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

使用示例:

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(p1) A;           // 定位new调用构造函数,在指定p1这块空间调用A对象
    p1->~A();            // 手动调用析构函数
    free(p1);
​
    // 使用operator new分配内存  
    A* p2 = (A*)operator new(sizeof(A));
    new(p2) A(10);       // 带参数的定位new,相当于在p2这块空间构造一个A对象,并用10来初始化
    p2->~A();
    operator delete(p2);
​
    return 0;
}

4.2 在游戏开发中的重要意义

定位new在游戏开发中的核心价值在于性能优化,主要通过以下方式实现:

1. 内存池和对象池

游戏中有大量需要频繁创建销毁的对象(子弹、粒子、特效等),使用定位new配合内存池可以:

  • 避免频繁的内存分配释放:预先分配大块内存,重复使用

  • 减少内存碎片:对象在连续内存中分配,避免堆内存碎片

  • 提高缓存命中率:连续内存布局对CPU缓存友好

2. 性能优化原理

普通new的代价:

// 每次都需要系统调用和堆管理(而系统调用在我们游戏开发中是一个昂贵的操作)
Bullet* bullet = new Bullet();  // 昂贵的操作
delete bullet;                  // 同样昂贵

定位new+内存池的优化:

// 预先分配,快速构造
void* memory = memory_pool.getMemory();
Bullet* bullet = new (memory) Bullet();  // 快速构造
// ...
bullet->~Bullet();                       // 手动析构
memory_pool.returnMemory(memory);        // 回收复用

我们可以将这个优化方式理解为,通过系统调用,调用一块大空间,然后通过定位new(指定位置)在这块空间上创建对象,避免每次创建对象都要new一块空间,然后我还可以定位delete,同样避免大量系统调用,进而优化性能

4.3 使用场景总结

定位new主要应用于:

  • 内存池系统:游戏对象池、粒子池等

  • 自定义内存管理:特殊的内存分配策略

  • 性能敏感模块:需要极致性能的核心系统

4.4 注意事项

  1. 必须手动调用析构函数:定位new只负责构造,不负责析构

  2. 内存管理责任:需要自己管理原始内存的分配和释放

  3. 异常安全:构造函数可能抛出异常,需要适当处理

5. 常见重要问题

5.1 malloc/free和new/delete的区别

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

主要区别:

特性malloc/freenew/delete
本质函数操作符
初始化不会初始化new可以初始化
空间计算手动计算大小,需要使用sizeof自动计算类型大小
返回值void*,需要强转直接返回对应类型指针
失败处理返回NULL,需要判空抛出异常,需要捕获
自定义类型只开辟空间,不调用构造/析构调用构造函数和析构函数

5.2 内存泄漏

5.2.1 什么是内存泄漏,内存泄漏的危害

什么是内存泄漏: 内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害: 长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

5.2.2 内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

  1. 堆内存泄漏 堆内存指的是程序执行中依据需要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。如果程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

  2. 系统资源泄漏 指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

5.2.3 如何检测内存泄漏

在VS下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks()函数进行简单检测:

int main() {
    int* p = new int[10];
    
    // 忘记释放 p,导致内存泄漏
    
    // 在VS中可以使用该函数检测内存泄漏
    _CrtDumpMemoryLeaks();
    return 0;
}

但这个函数只会简单给出泄露了多少字节,不会告诉你位置

5.2.4 如何避免内存泄漏
  1. 良好的设计规范:养成良好的编码规范,申请的内存空间记着匹配的去释放(碰上异常状态就算释放依然有问题,就要结合下一条)

  2. RAII思想或智能指针:采用RAII思想或者智能指针来管理资源

  3. 内存管理库:有些公司内部规范使用内部实现的私有内存管理库,这套库一般自带内存泄漏检测功能选项

  4. 检测工具:出问题时使用内存泄漏工具检测

总结: 内存泄漏非常常见,解决方案分为两种:

  • 事前预防型:如智能指针等

  • 事后查错型:如泄漏检测工具



    感谢你能看到这里,希望我的分享对解决你的问题有所帮助。

    如果觉得文章不错,欢迎关注,我将努力更新更多知识博客,谢谢大佬支持!

    你的三连是对我最大的支持!

在(下)篇我们将重点介绍智能指针。
posted @ 2025-12-08 08:50  clnchanpin  阅读(13)  评论(0)    收藏  举报