C++入门自学Day5-- C/C++内存管理(续)

发布于:2025-08-04 ⋅ 阅读:(19) ⋅ 点赞:(0)

 往期内容回顾     

        C/C++内存管理(初识)        

        c++类与对象(面试题)

        c++类与对象(友元)
        c++类与对象(类的初始化和静态成员)
        c++类与对象(赋值运算符与拷贝构造)
        c++类与对象(拷贝构造)
        c++类与对象(构造和析构函数)
        c++类与对象(初识2)


 一、 C++内存管理

        在 C++ 中,new 和 delete 是用于 动态内存管理 的运算符,它们之所以存在,是为了满足 C++ 更灵活、更高效的内存控制需求,尤其在对象管理方面相对于 C 的 malloc/free 可支持自定义类对象的构造与析构

                1. new 不仅分配内存,还会调用构造函数,完成对象初始化。

  •         2. delete 不仅释放内存,还会调用析构函数,完成资源清理。


        示例:链遍的构建以及内存释放

c语言实现:

       使用malloc进行链表内存分配和构建,利用free进行链表的销毁。

//c语言链表的创建 --> malloc,free
typedef struct ListNode_c{
    int _val;
    struct ListNode_c* _next;
    struct ListNode_c* _prev;
}ListNode_c;

ListNode_c* Buy_NewNode(){
    ListNode_c* node  =(ListNode_c*)malloc(sizeof(ListNode_c));
    node->_next = NULL;node->_prev = NULL;
    node->_val = 0;
    return node;
}

void ListDestroy(ListNode_c* phead){
    assert(phead);
    ListNode_c* cur = phead->_next;
    while (cur)
    {
        ListNode_c* tmp = cur->_next;
        free(cur);
        cur = tmp;
    };
    free(phead);
    phead = NULL;  
}

C++实现:

        利用new,delete进行链表的动态内存分配,构建以及销毁。new-->调用构造函数

delete -->调用析构函数。

// c++如何使用new,delete进行动态内存管理

struct ListNode_cpp
{
    ListNode_cpp(int val = 6)
    :_val(val)
    ,_prev(nullptr)
    ,_next(nullptr)
    {};
    void ListDestroy_cpp(ListNode_cpp* phead) {
        ListNode_cpp* cur = phead;
        while (cur) {
            ListNode_cpp* tmp = cur->_next;
            delete cur;
            cur = tmp;
        }
    };

    int _val;
    ListNode_cpp* _prev;
    ListNode_cpp* _next;
};

总结

特性

malloc/free(C)

new/delete(C++)

是否调用构造函数

❌ 否

✅ 是

是否类型安全

❌ 否(返回 void* 需强转)

✅ 是(自动返回对象指针)

是否支持对象数组初始化

❌ 否

✅ 是(需配合 new[] 和 delete[])

是否能被重载

❌ 否

✅ 支持 operator new/delete 重载

容易内存泄漏

✅ 容易

✅ 仍然可能,建议配合智能指针使用


二、operator new 和 delete

        new 和 delete 是 C++ 的 运算符,不仅仅是关键字,它们调用的是底层的函数:operator new 和 operator delete,你可以重载这些函数来自定义对象的内存分配方式。

operator new 和 malloc 

class A{
    public:
    int _val;
};
int main(){
    size_t size = 2*1024*1024*1024;
    A* a1 =(A*) malloc(size*sizeof(A));
    cout<< a1 <<endl;
    // A* a2 = new A;
    //A* a3 = (A*) operator new(size*sizeof(A));
}

当使用malloc开辟一个很大的内存时,malloc开辟失败,则开辟的地址a1为0地址:

输出描述:
0x0 

并不会程序崩溃,只是开辟的0地址


当使用operator new去开辟大内存时,报错如下:

libc++abi: terminating due to uncaught exception of type std::bad_alloc: std::bad_alloc
zsh: abort      "/Users/junye/Desktop/cplusplus/"memory

说明你的程序因为 内存分配失败(std::bad_alloc) 而异常终止。

operator new 和 malloc 使用方式都一样,区别在于处理错误的方式不一致。


operator delete 和 free 区别在于调用析构函数清理

1、 new/delete行为拆解

        new 的完整过程:

MyClass* p = new MyClass(); 

相当于两步操作:

        调用 operator new 分配内存(只分配,不构造):

void* mem = operator new(sizeof(MyClass));

        调用构造函数构造对象:

MyClass* p = new (mem) MyClass();
        new = operator new + 构造函数

        delete的完整过程:

delete p;

相当于两步操作:

        调用析构函数:

p->~MyClass();

        调用 operator delete 释放内存:

operator delete(p);

        delete = operator delete + 析构函数


总结:为什么我们需要operator new/ delete呢?

        我们之所以需要 operator new / operator delete,是因为它们赋予了 C++ 开发者对 对象内存分配与释放的底层控制能力相比 new 和 delete 这对高级运算符,operator new 和 operator delete 更加底层和灵活,适用于性能优化、资源控制、调试等高阶场景


2、定位new/placement new

        定位 new(placement new)是一种特殊的 new 运算符形式,允许你在指定的内存地址上构造对象,而不是从堆上分配新内存。这在自定义内存管理(如内存池、共享内存、内存对齐控制)中非常有用。

        1、基本语法

​void* buffer = operator new(sizeof(MyClass)); // 手动分配原始内存
MyClass* obj = new (buffer) MyClass();        // 在 buffer 上构造对象(placement new)

​

或常见更直观的形式:

char buffer[sizeof(MyClass)];
MyClass* obj = new (buffer) MyClass();  // 在 buffer 上构造对象

或者

class A{
    public:
    A(){
        cout<<"A()"<<endl;
    };
    ~A(){
        cout<<"~A()"<<endl;
    };
    public:
    int _val;
};
int main(){
    // A* a1 = new A;
    // delete a1;
    A* a1 = (A*)malloc(sizeof(A));
    new(a1)A(); //对已分配内存进行初始化-->定位new
    a1->~A();
    operator delete(a1);
}

调用构造函数输出:

A()

~A()


2、与普通new的对比

功能

普通 new

定位 new(placement new)

是否分配内存

✅ 是

❌ 否,需要用户自己提供内存

是否调用构造函数

✅ 是

✅ 是

是否调用 malloc

✅ 默认是

❌ 不会

是否自动释放

✅ delete 自动释放

❌ 需要手动析构 + 手动释放内存

3、 使用场景

  1. 自定义内存池(Memory Pool)

  2. 共享内存中的对象创建

  3. 提高性能:避免频繁堆分配

  4. 构造对象数组时细粒度控制生命周期


三、常见【面试题】

      问题一、malloc/free/new/delete的相同点和区别

        1、相同点

特征

说明

都是 动态内存管理方式

都可在运行时申请内存,适合大小不确定、生命周期较长的对象或数组

都是 在堆上分配内存

内存分配来自堆区,生命周期由程序控制,不是自动释放

都必须 手动释放

需要开发者使用 free 或 delete 手动释放,否则会造成内存泄漏

        2、区别对比:malloc/free vs new/delete

比较点

malloc/free(C风格)

new/delete(C++风格)

属于语言

C(也可用于 C++)

仅适用于 C++

返回类型

void*(需要强制类型转换)

自动返回正确类型的指针

是否调用构造函数

❌ 不调用构造函数

✅ 自动调用构造函数

是否调用析构函数

❌ 不调用析构函数

✅ 自动调用析构函数

语法是否简洁

⛔ 比较繁琐:需要类型转换

✅ 简洁高效:无须类型转换

是否可重载

❌ 不可重载

✅ 可自定义 operator new/delete

是否支持数组版本

❌ 需手动计算大小,例如 malloc(n * sizeof(T))

✅ 使用 new[] 和 delete[]

异常处理

❌ 内存不足返回 NULL

✅ 抛出 std::bad_alloc 异常(也可使用 nothrow)


        问题二、什么是内存泄露(Memory Leak)

        内存泄露 是指:

程序在堆上 申请了内存,但在使用完之后 没有释放,而且也  无法再访问到 这块内存,造成内存“丢失”。
  • 内存“还存在”,但你程序再也找不到它、也不能释放它;

  • 久而久之,系统堆内存被“吃光”,导致程序变慢、崩溃、操作系统卡死。


    2、内存泄露的分类(常见 4 类)

分类类型

说明

🔹 1. 持续性泄露(Permanent Leak)

程序整个生命周期都未释放,比如 new 后忘记 delete

🔹 2. 间歇性泄露(Intermittent Leak)

在某些条件下发生,例如多次调用某函数时,部分情况忘记释放

🔹 3. 假性泄露(False Leak)

内存未释放但仍可访问,比如缓存池、单例,这些技术上不是泄露,但工具可能误报

🔹 4. 堆外泄露(Non-heap Leak)

比如系统资源泄漏:文件描述符、socket、内核对象未释放(这虽然不在 heap 上,但本质类似)

        1、持续性泄露(最常见)
#include <iostream>

void Leak1() {
    int* arr = new int[100]; // 申请内存
    // 忘记 delete[] arr;,出了函数作用域后 arr 不可达 => 内存泄露
}

int main() {
    for (int i = 0; i < 10000; ++i) {
        Leak1(); // 每次调用泄露 100 个 int
    }

    std::cout << "Done\n";
    return 0;
}
        2、间歇性泄露(条件分支未覆盖)
void Leak2(bool condition) {
    int* p = new int(10);
    if (condition) {
        // 使用后释放
        delete p;
    }
    // 如果 condition == false,就泄露了内存
}

总结:内存泄露

内容

举例 / 表现

分类

持续性 / 间歇性 / 假性 / 堆外泄露

危害

性能下降、程序崩溃、信息泄露、难维护

典型代码

new 后没 delete;条件遗漏释放逻辑

检测工具

valgrind、AddressSanitizer、VLD

预防方式

智能指针、RAII、代码规范、工具检查


问题三、为什么在32位系统下,堆无法申请4g的内存空间,而在64位下能够申请呢?

1. 地址空间只有 4GB

  • 32 位系统的地址总线宽度为 32 位,只能表示 2³² = 4GB 的虚拟地址。

  • 所以,一个进程的总虚拟地址空间只有 4GB。

2. 进程地址空间被

        操作系统划分

  • 在大多数操作系统中(例如 Linux/Windows),内核空间通常占用高地址部分 1GB 或 2GB。

  • 剩下的 2~3GB 才是用户态进程可用的空间(包括堆、栈、代码段、数据段等)。

3. 堆只能用这 2~3GB 的一部分

  • 所以你在 32 位下无法申请完整的 4GB(甚至 3GB)连续堆内存。

4.为什么 64 位可以申请远超 4GB 的堆空间

  • 在 64 位系统下,地址空间理论上可以到 16 EB(当然受限于系统实现和硬件资源)。

  • 操作系统会给每个进程分配极大虚拟空间(Linux 默认 128 TB 或更多)。

  • 因为地址空间充足,不再受限于 4GB 的虚拟内存瓶颈。

  • 只要你有足够的物理内存或 swap 资源,就能申请超大堆内存,比如几十 GB。

32 位进程地址空间(4GB):
+--------------------+  0xFFFFFFFF (4GB)
|  内核空间(1GB)    |
+--------------------+  0xC0000000 (3GB)
| 用户空间(最多3GB)|
| 代码段             |
| 数据段             |
| 堆 ← malloc        |
| ...                |
| 栈 ↓               |
+--------------------+  0x00000000

64 位进程地址空间(超大):
+-----------------------------+
|   数 TB 级的用户空间       |
|   堆、映射区、栈全在中间    |
+-----------------------------+

5、总结

问题

原因

32 位不能 malloc 4GB

因为地址空间最大就 4GB,还需留出栈、代码段、内核空间等

64 位能申请大堆空间

因为地址空间极大,只受限于物理内存或 swap,系统资源允许即可


网站公告

今日签到

点亮在社区的每一天
去签到