C++动态内存管理

目录

[1. C/C++内存分布图](#1. C/C++内存分布图)

相关说明:

相关应用:

C中动态内存方式

[2. C++ 内存管理方式](#2. C++ 内存管理方式)

[new 和delete操作自定义类型](#new 和delete操作自定义类型)

3种初始化数组

[3. operator new与operator delete函数](#3. operator new与operator delete函数)

[4. new和delete实现原理](#4. new和delete实现原理)

[一.new 的实现原理](#一.new 的实现原理)

二.delete的实现原理

[5. malloc/free和new/delete的区别](#5. malloc/free和new/delete的区别)

6.内存泄漏

[1. 手动分配内存后,忘记释放](#1. 手动分配内存后,忘记释放)

[2. 指针重赋值,丢失内存引用](#2. 指针重赋值,丢失内存引用)

[3. 异常导致释放代码未执行](#3. 异常导致释放代码未执行)

[4. 全局 / 静态指针指向的堆内存未释放](#4. 全局 / 静态指针指向的堆内存未释放)


1. C/C++内存分布图

相关说明:

  1. 又叫堆栈------非静态局部变量/函数参数/返回值等,是向下增长。
  2. 内存映射段是高效的 I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口,创建共享共享内存,做进程间通信。
  3. 用于程序运行是进行动态内存分配,堆是可以向上增长的。
  4. 数据段 -- 存储全局数据和静态数据。
  5. 代码段 --可执行的代码/只读常量。

相关应用:

选择:
选项 : A . 栈 B . 堆 C . 数据段 ( 静态区 ) D . 代码段 ( 常量区 )
globalVar 在哪里? C__
staticGlobalVar 在哪里? C__
staticVar 在哪里? C__
localVar 在哪里? A
num1 在哪里? A
char2 在哪里? A
* char2 在哪里?A_ char2[]是把字符串 "复制" 到了栈上,相当于自己在栈上存了一份。
pChar3 在哪里? A__
* pChar3 在哪里?D const char* pChar3 只是在栈上存了一个 "地址",这个地址指向常量区里 已经存在的字符串。
ptr1 在哪里? A__
* ptr1 在哪里? B__

C中动态内存方式

以前C语言部分有: C语言动态内存/管理

cpp 复制代码
#include<iostream>

using namespace std;

int main()
{

	int* p1 = (int*)malloc(4 * sizeof(int));
	free(p1);

	int* p2 = (int*)calloc(4, sizeof(int));

	int* p3 = (int*)realloc(p2, sizeof(int)*10);

	cout << p2 << endl;//这里需要释放p2吗?
	cout << p3 << endl;

	free(p3);

}

问题1这里P2 需要释放吗?

不需要,手动 free(p2) 反而会导致程序出现未定义行为(比如崩溃、内存错乱)。

  1. realloc 原地扩容(原内存块后方有足够空间):这时指向同一地址
  2. realloc 分配新内存块(原内存块后方空间不足):此时 realloc 会在堆中找一块足够大的新空闲内存块,把 p2 指向的原数据完整复制到新块中,自动释放 p2 指向的旧内存块 ,然后返回新块的地址给 p3p2 变成了「野指针」(指向的内存已被系统回收),再手动 free(p2),就是对「已经释放的无效内存」进行释放,同样是未定义行为。

问题2:malloc/calloc/realloc 区别

  1. malloc:内存不初始化,分配 size 字节的连续堆内存,返回首地址;分配失败返回 NULL,适用于仅需分配内存、无需初始化为 0 的场景
  2. calloc:初始化为 0,分配 num 个大小为 size 的连续内存(总大小 num*size),并将所有字节初始化为 0
  3. realloc:不初始化新扩展部分,调整已分配内存块的大小:若原块后方有足够空间则原地扩容;否则分配新块、复制原数据并释放原块,返回新地址;ptrNULL时等价于mallocnew_size为 0 时等价于free。(void* realloc(void* ptr, size_t new_size))

关键注意事项

  1. 指针类型处理malloc/calloc/realloc 均返回 void* 指针,C++ 中需强制转换为目标类型指针(C 可隐式转换)。
  2. 地址有效性realloc 不保证内存地址不变,必须用返回的新指针接收,原指针可能失效(尤其是异地扩容后)。
  3. 内存安全
    • 使用 malloc 时,建议手动初始化内存,避免旧数据引发的问题。
  • realloc 扩容后,原指针可能被自动释放,不能重复 free 原指针。

2. C++ 内存管理方式

cpp 复制代码
void  Test(){
//动态申请一个int类型的空间
int* ptr4 = new int;

//动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);

//动态申请10个int类型的空间
int* ptr6 = new int[3];
int* ptr7 = new int[10] {1, 2, 3, 4};
delete ptr4;
delete ptr5;
delete[]ptr6;
delete[] ptr7;
}

总结:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[ ]和delete[ ]

new 和delete操作自定义类型

cpp 复制代码
#include<iostream>

using namespace std;
class A
{
public:
	A(int a)
		:_a(a)
	{
		cout << "A()" << this << endl;
	}
	~A()
	{
		cout << "~A()" <<this<< endl;
	}


private:
	int _a;

};


int main()
{
	// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
	//还会调用构造函数和析构函数
	A* p1 = (A*)malloc(sizeof(A));
	A* p2 = new A(1);
	free(p1);
	delete p2;
	// 内置类型是几乎是一样的
	int* p3 = (int*)malloc(sizeof(int)); // C
	int* p4 = new int;
	free(p3);
	delete p4;
	A* p5 = (A*)malloc(sizeof(A) * 10);
	A a1(1), a2(2), a3(3);
	A* p6 = new A[3]{a1,a2,a3};
	free(p5);
	delete[] p6;
	return 0;
}


可以直观的看出在申请自定义类型的空间时, new 会调用构造函数, delete 会调用析构函数,而 malloc free 不会
最后那6个析构是:「栈上 3 个对象」+「堆上 3 个对象」

3种初始化数组

这是3种初始化堆数组的写,第二种效率最高,但依靠A类支持int类型构造,若是有explicit,则不支持第二种隐式类型转换,这3种:

  1. 堆数组 3 次拷贝构造 + 栈对象 3 次构造 + 堆数组 3 次析构 + 栈对象 3 次析构
  2. 堆数组 3 次构造 + 堆数组 3 次析构
  3. 临时对象 3 次构造 + 堆数组 3 次拷贝构造 + 临时对象 3 次析构 + 堆数组 3 次析构

3. operator newoperator delete****函数

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

cpp 复制代码
/*
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);
}
/*
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 就是 operator new。→ 错误。new 是操作符,包含 "分配内存 + 构造对象" 两步;operator new 只负责分配内存。
  • 误区operator new 只能用 malloc 实现。→ 错误。它可以用任何内存分配方式实现,比如内存池、mmap 等。
  • 误区 :类内重载的 operator new 会影响全局。→ 错误。类内重载的仅对该类生效,全局的 operator new 依然存在

4. new和delete实现原理

一.new 的实现原理

new 是 C++ 中动态创建对象的关键字,其执行过程分为内存分配对象初始化 两个核心阶段,底层依赖 operator new 函数和构造函数。

  1. 单个对象的 newT* ptr = new T(args);

执行流程:

(1) 调用 operator new 分配内存

底层默认通过 malloc 分配指定大小(sizeof(T))的堆内存。

若分配失败,默认会抛出 std::bad_alloc 异常;若使用 nothrow 版本 (new(std::nothrow)T),则返回 nullptr

(2) 调用构造函数初始化对象

在已分配的内存上,调用 T 的构造函数(传入 args),完成对象的初始化。

(3)返回对象指针

返回指向初始化完成的对象的指针。

2.数组的 new[]T* ptr = new T[N]{...};

  • 调用 operator new[] 分配内存
    • 分配的内存大小 = N * sizeof(T) + 额外开销(通常是 4 字节,用来存储数组的元素数量 N)。
    • 额外开销的作用是:后续 delete[] 时,需要知道要调用多少次析构函数。
  • 调用 N 次构造函数
    • 对数组的每个元素调用构造函数,完成初始化。
  • 返回数组首元素指针
    • 注意:返回的指针是数组首元素的地址,而实际分配的内存起始地址会比返回指针靠前(因为前面存了元素数量 N)。
二.delete的实现原理

delete 是销毁动态对象并释放内存的关键字,执行过程分为对象清理内存释放 两个核心阶段,底层依赖析构函数和 operator delete 函数。


  1. 单个对象的 deletedelete ptr;

执行流程:

  • 调用析构函数清理对象
    • 调用 T 的析构函数,释放对象持有的资源(如堆内存、文件句柄等)。
  • 调用 operator delete 释放内存
    • 底层默认通过 free 释放之前分配的内存。
  1. 数组的 delete[]delete[] ptr;

执行流程:

  • 获取数组元素数量
    • 从指针 ptr 向前偏移,读取 new[] 时存储的元素数量 N
  • 调用 N 次析构函数
    • 对数组的每个元素调用析构函数,清理所有对象。
  • 调用 operator delete[] 释放内存
    • 释放整个内 存块(包括存储元素数量的额外开销部分)

5. malloc/freenew/delete****的区别

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

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

6.内存泄漏

内存泄漏的核心定义:在堆上分配的内存,使用完毕后既没有被释放,又失去了对这块内存的所有有效引用,导致这块内存永远无法被程序回收和复用,直到程序进程结束,操作系统才会回收它。

  • 内存泄漏不会导致程序立即崩溃,它是一种 "慢性疾病"。
  • 对于短期运行的小程序(如控制台测试程序),内存泄漏的影响很小(程序结束后系统会回收所有内存)。
  • 对于长期运行的程序(如服务器、后台服务、游戏引擎),内存泄漏会持续占用内存,最终导致内存耗尽、程序运行缓慢甚至崩溃,这是必须严格避免的。

1. 手动分配内存后,忘记释放

cpp 复制代码
#include<iostream>

using namespace std;

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


private:
	int _a;

};

// 场景1:new 后忘记 delete
void test1() {
    int* ptr = new int(10); // 堆上分配内存
    // 业务逻辑...
    return; // 忘记执行 delete ptr; ,内存泄漏
}

// 场景2:new[] 后忘记 delete[](自定义类型更危险,还会漏析构)
void test2() {

	A* arr = new A[3]{1,2,3}; // 堆上分配数组
    // 业务逻辑...
    return; // 忘记执行 delete[] arr; ,内存泄漏+对象未析构
}

// 场景3:malloc 后忘记 free
void test3() {
    char* buf = (char*)malloc(1024); // 堆上分配内存
    // 业务逻辑...
    return; // 忘记执行 free(buf); ,内存泄漏
}

2. 指针重赋值,丢失内存引用

指针被重新赋值后,原来指向的堆内存失去了唯一引用,变成 "无人认领" 的内存,无法释放。

cpp 复制代码
void test4() {
    int* ptr = new int(10); // 第一次分配内存,地址为 0x1000
    ptr = new int(20); // 指针重赋值,指向新地址 0x2000
    // 此时,原地址 0x1000 的内存既没有被释放,也无法通过 ptr 访问,内存泄漏
    delete ptr; // 仅释放了 0x2000 的内存,0x1000 的内存永远丢失
}

3. 异常导致释放代码未执行

cpp 复制代码
void func() {
    throw std::runtime_error("未知异常"); // 抛出异常
}

void test5() {
    int* ptr = new int(10);
    func(); // 抛出异常,后续代码无法执行
    delete ptr; // 这行代码永远不会被执行,内存泄漏
}

4. 全局 / 静态指针指向的堆内存未释放

cpp 复制代码
int* g_ptr = nullptr;

void init() {
    g_ptr = new int(100); // 全局指针分配堆内存
}

// 程序结束前未调用该函数,内存泄漏
void destroy() {
    delete g_ptr;
    g_ptr = nullptr;
}

规避最基础就是遵循 "谁分配,谁释放" 原则,统一内存管理策略

相关推荐
2401_838472512 小时前
用Python和Twilio构建短信通知系统
jvm·数据库·python
weixin_452159552 小时前
如何从Python初学者进阶为专家?
jvm·数据库·python
2301_790300963 小时前
用Python读取和处理NASA公开API数据
jvm·数据库·python
hello 早上好3 小时前
03_JVM(Java Virtual Machine)的生命周期
java·开发语言·jvm
2301_790300964 小时前
数据分析与科学计算
jvm·数据库·python
yufuu985 小时前
使用Scikit-learn进行机器学习模型评估
jvm·数据库·python
小旭95276 小时前
Java 反射详解
java·开发语言·jvm·面试·intellij-idea
HalvmånEver6 小时前
Linux:线程创建与终止上(线程五)
java·linux·jvm
m0_706653237 小时前
Python入门:从零到一的第一个程序
jvm·数据库·python