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

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

 往期内容回顾     

        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(); //对已分配内存进行初始化-->定位newa1->~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,系统资源允许即可

http://www.lryc.cn/news/609393.html

相关文章:

  • C语言数据结构(7)贪吃蛇项目2.贪吃蛇项目实现
  • Linux 文件系统基本管理
  • python 12 install jupyter时zmq.h或libzmq报错处理
  • 基于springboot的在线考试系统/考试信息管理平台
  • 苍穹外卖项目学习——day1(项目概述、环境搭建)
  • 团队独立思考的力量
  • 机器学习——决策树(DecisionTree)
  • 波士顿房价预测工具 - XGBoost实现
  • 三、驱动篇-HDF驱动介绍1
  • 【Unity】背包系统 + 物品管理窗口 (上)
  • Python 的标准库 bisect 模块
  • 从WebShell 与 ShellCode 免杀技术 打造适合自己的免杀技术链
  • [Oracle] 获取系统当前日期
  • 使用AssemblyAI将音频数据转换成文本
  • [Oracle] TO_DATE()函数
  • gpu instancer crowd 使用自定义材质并且只修改单个物体的材质参数
  • 机器学习 决策树基本介绍
  • [2025ICCV-目标检测方向]DuET:通过无示例任务算术进行双增量对象检测
  • 数据结构:单向链表的函数创建
  • kubernetes基础知识
  • io_cancel系统调用及示例
  • 11.消息队列
  • IDEA查看源码利器XCodeMap插件
  • LangChain4J入门:使用SpringBoot-start
  • 网络规划与设计5个阶段内容
  • 项目日记---高并发内存池整体框架
  • Python中的sys.path与PYTHONPATH全解析:模块导入路径的底层机制与最佳实践
  • 进阶向:YOLOv11模型轻量化
  • 微店所有店铺内的商品数据API接口
  • AI Competitor Intelligence Agent Team