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

往期内容回顾

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

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

c++类与对象(友元)

c++类与对象(类的初始化和静态成员)

c++类与对象(赋值运算符与拷贝构造)

c++类与对象(拷贝构造)

c++类与对象(构造和析构函数)

c++类与对象(初识2)


一、 C++内存管理

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

  1. new 不仅分配内存,还会调用构造函数,完成对象初始化。
    1. delete 不仅释放内存,还会调用析构函数,完成资源清理。

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

c语言实现:

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

cpp 复制代码
//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 -->调用析构函数。

cpp 复制代码
// 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

cpp 复制代码
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 的完整过程:

cpp 复制代码
MyClass* p = new MyClass(); 

相当于两步操作:

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

bash 复制代码
void* mem = operator new(sizeof(MyClass));

调用构造函数构造对象:

bash 复制代码
MyClass* p = new (mem) MyClass();

new = operator new + 构造函数


delete的完整过程:

bash 复制代码
delete p;

相当于两步操作:

调用析构函数:

bash 复制代码
p->~MyClass();

调用 operator delete 释放内存:

bash 复制代码
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、基本语法

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

​

或常见更直观的形式:

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

或者

cpp 复制代码
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、持续性泄露(最常见)
cpp 复制代码
#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、间歇性泄露(条件分支未覆盖)
cpp 复制代码
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,系统资源允许即可
相关推荐
叉烧钵钵鸡27 分钟前
Java ++i 与 i++ 底层原理
java·开发语言·后端
gnawkhhkwang33 分钟前
io_submit系统调用及示例
linux·c语言
gnawkhhkwang36 分钟前
io_cancel系统调用及示例
linux·c语言
御水流红叶42 分钟前
安卓加固脱壳
android·开发语言·python
hqxstudying44 分钟前
SpringAI的使用
java·开发语言·人工智能·springai
狐小粟同学44 分钟前
JAVAEE--4.多线程案例
java·开发语言
the beard1 小时前
RabbitMQ:基于SpringAMQP声明队列与交换机并配置消息转换器(三)
java·开发语言·rabbitmq·intellij idea
橙小花1 小时前
C语言:函数指针、二级指针、常量指针常量、野指针
c语言·数据结构·算法
清朝牢弟2 小时前
Ubuntu系统VScode实现opencv(c++)视频的处理与保存
c++·人工智能·vscode·opencv·ubuntu
oioihoii2 小时前
在macOS上使用VS Code和Clang配置C++开发环境
c++·macos·策略模式