【C++】内存管理:内存分布,new/delete的使用及细节处理,new/delete的底层,定位new表达式

🧑‍💻博主名称:鱼子星_

✅数据结构专栏:【数据结构】

✅算法竞赛专栏:【算法竞赛】

✅C++系列专栏:【C++从零开始系列】


前言

在 C/C++ 编程中,动态内存管理是构建高效、灵活程序的核心技能之一。从 C 语言的 malloc/free 到 C++ 的 new/delete,看似简单的内存申请与释放背后,隐藏着语言设计哲学、对象生命周期管理以及底层内存机制的深刻差异。

本文旨在系统性地剖析 C++ 动态内存管理的全貌。我们将从内存分布的基础知识出发,逐步对比 C 与 C++ 的动态内存开辟方式,深入探讨 new/delete 对内置类型与自定义类型的处理机制、其底层实现原理(operator new/operator delete),并拓展至定位 new 表达式等高级话题。通过清晰的代码示例、内存布局图解和面试题解析,帮助你不仅掌握语法,更理解其设计意图与适用场景,从而在项目中做出更优雅、更安全的内存管理决策。

无论你是正在巩固基础的初学者,还是希望深入理解 C++ 对象模型的中级开发者,相信本文都能为你提供清晰的指引和实用的参考。

一. 内存分布

在对C++内存分布了解前,先来看如下的题目

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
 static int staticVar = 1;
 int localVar = 1;
 int num1[10] = { 1, 2, 3, 4 };
 char char2[] = "abcd";
 const char* pChar3 = "abcd";
 int* ptr1 = (int*)malloc(sizeof(int) * 4);
 int* ptr2 = (int*)calloc(4, sizeof(int));
 int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
 free(ptr1);
 free(ptr3);
}
复制代码
一.选择题:
   选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
   1.globalVar在哪里?__C__   
   2.staticGlobalVar在哪里?__C__
   3.staticVar在哪里?__C__   
   4.localVar在哪里?__A__
   5.num1 在哪里?__A__
   6.char2在哪里?__A__   
   7.*char2在哪里?__A__
   8.pChar3在哪里?__A__      
   9.*pChar3在哪里?__D__
   10.ptr1在哪里?__A__        
   11.*ptr1在哪里?__B__

题目解析

  1. globalVar是全局变量,生命周期持续到程序结束,所以内存在静态区
  2. staticGlobalVar本身就是全局变量,再加上static也还是在静态区
  3. staticVar被static修饰,内存在静态区
  4. localVar局部变量的内存都在栈上
  5. num1也属于局部变量
  6. char2是局部字符数组的变量名,内存在栈上
  7. *char2看似是静态的字符串"abcd",但是它的内存还是在栈上,有如下 2 种理解方式:
  • *char2是常量区中的"abcd"的拷贝
  • "abcd"是局部域的数组的元素,当函数结束时,函数内部的数据会随着栈帧销毁,所以*char2在栈上
  1. pChar3也在栈上,因为 const 修饰的并不是pChar3,而是*pChar3
  2. *pChar3被 const 修饰在常量区
  3. ptr1知识局部域的一个指针变量,在栈上
  4. *ptr1是动态申请的内存,内存在堆上

测试验证


内存空间

在C++中有如下几种内存空间,存储着不同属性的数据

  • 栈(堆栈):存储着非静态局部变量/函数参数/返回值。栈的空间是自顶向下延伸的
  • :动态申请的空间都在堆上
  • 数据段(静态区):存储全局数据和静态数据
  • 代码段(常量区):存储只读常量

二. C语言中动态内存开辟的方法

动态内存开辟malloc/calloc/realloc

cpp 复制代码
//1.malloc/calloc/realloc的区别是什么?
int* p1 = (int*)malloc(sizeof(int) * 10);
int* p2 = (int*)realloc(p1, sizeof(int) * 20);
free(p2);

面试题分析

  1. malloc/calloc/realloc和区别是什么?
    malloc只能用于动态内存开辟,对开辟的空间不能初始化,calloc可以对动态开辟的空间初始化,realloc用于对已有的空间扩容,扩容有两种情况,原地扩容和拷贝数据到另外的地方扩容
  2. malloc的实现原理是什么?
    感兴趣的可以点击这个链接查看【CTF】GLibc堆利用入门-机制介绍

三. C++中动态内存开辟的方法

1. new/delete的语法使用

C++中一般使用关键字 newdelete 进行动态内存开辟,对于单个元素的内存开辟,其使用的格式为:

cpp 复制代码
类型* 变量名 = new 类型(初始化的值); //初始化的值可以写也可以不写
delete 变量名;

如果要申请一块连续的动态内存,开辟时在类型之后使用[size]来定义开辟的大小(size为需要申请的元素个数),释放内存时在delete后加上[],定义格式为:

cpp 复制代码
类型* 变量名 = new 类型[size]; //初始化的方式和数组相同
delete[] 变量名;

📚️ 使用关键字 new 动态开辟内存时,返回值不需要使用类型转换,new会自行匹配返回值的类型

2. new/delete动态申请内置类型

对于内置类型的动态开辟,new/delete和malloc/free的功能并没有很大的差别,除了使用更加方便外,new主要的优势就是动态开辟内存时可以顺便初始化。

cpp 复制代码
//使用new动态开辟内存
int* p1 = new int(10);   //直接将*p1初始化成了10
int* arr1 = new int[10]{1,2,3};
delete p1;
delete[] arr1;
//使用malloc动态开辟内存
int* p2 = (int*)malloc(sizeof(int));
*p2 = 10;
int* arr2 = (int*)malloc(sizeof(int) * 10); 
free(p2);
free(arr2);

使用new直接初始化

使用malloc不能直接初始化

3. new/delete动态申请自定义类型

使用new和delete对自定义类型进行动态内存开辟对比使用malloc和free有一个绝对的优势就是使用new进行动态内存开辟会自动调用自定义类型的默认构造函数,使用delete释放内存会自动调用自定义类型的析构函数

cpp 复制代码
#include<iostream>

using namespace std;

class A
{
public:
	A(int a = 1)
		:_a(a)
	{
		cout << "A(int a = 1)" << endl;
	}
	~A()
	{
		_a = 0;
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = new A;
	delete p;
	return 0;
}

使用上代码可以进行验证,输出结果为:

如果使用new和delete进行多个元素的动态内存申请和销毁,编译器会相应的调用多次构造函数和析构函数

为什么这里又说new会调用构造函数呢?其实上面已经说了,使用new进行动态内存开辟可以直接初始化,这个方法也适用于自定义类型。那要如何进行初始化呢?

这就需要看自定义类型中写的构造函数有多少种样子了,只要有相应的构造函数,那么用什么样的初始化方式都没问题(这里针对有意义的初始化)。所以这里其实也可以得出,使用new对自定义类型进行动态内存开辟的初始化,是根据初始化的形式调用构造函数的,如果没有显式的初始化,就会调用默认构造函数,反之会调用参数最适合的构造函数。

📚️ 如果是对多个元素,每个元素都有多参数的初始化的动态内存开辟,每组参数需要使用{}括起来

cpp 复制代码
A* p = new A[3]{ {1, 2}, {3, 4}, {4, 5} };
cpp 复制代码
class A
{
public:
	A(int a = 1)
		:_a(a)
	{
		cout << "A(int a = 1)" << endl;
	}
	A(int a1, int a2)
		:_a(a1 + a2)
	{
		cout << "A(int a1, int a2)" << endl;
	}
	~A()
	{
		_a = 0;
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p1 = new A(10);
	A* p2 = new A(10, 10);
	delete p1;
	delete p2;
	return 0;
}

💻️ 输出

-

🎀小结:

  1. 使用malloc动态内存分配不能对申请的空间进行初始化,使用new可以
  2. 使用new/delete在内存申请时会自动调用构造函数,在内存释放时会调用析构函数
  3. new对自定义类型的空间初始化会根据初始化的方式选择合适的构造函数,例如:没有显式传参就调用默认构造,如果有,就会对照初始化的参数,选择最合适的构造函数

📚️ 如果自定义类型没有默认构造,则使用new一定要显式的初始化

4. new/delete的使用场景

我们使用malloc/free对链表类进行动态内存分配时,初始化需要手动写一个Init函数进行初始化,而对于链表自身申请的空间,还需要手动的调用析构函数来释放内存,这样操作很不方便

cpp 复制代码
class List
{
public:
	List(int n = 4)
	{
		_arr = (int*)malloc(sizeof(int) * n);
		_capacity = 4;
		_size = 0;
	}
	~List()
	{
		if (_arr)
		{
			free(_arr);
			_arr = nullptr;
		}
		_capacity = _size = 0;
	}
	void Init(int n = 4)
	{
		_arr = (int*)malloc(sizeof(int) * n);
		_capacity = 4;
		_size = 0;
	}
private:
	int* _arr;
	int _capacity;
	int _size;
};
int main()
{
	//无法初始化
	List* lt = (List*)malloc(sizeof(List));
	(*lt).Init();
	//手动调用析构函数
	(*lt).~List();
	free(lt);
	return 0;
}

而如果使用new/delete进行动态内存申请,它们会自动的调用 List 的构造函数和析构函数,这也就代表

  • 使用 new 不仅会申请 lt 指针的内存,更会调用 List 的构造函数将链表初始时的内存也申请了
  • 使用 delete 释放空间不仅仅是释放 lt 指针的内存,更会先调用 List 的析构函数,释放链表动态申请的空间,可谓事半功倍
cpp 复制代码
//自动调用构造函数
List* lt = new List;
//自动调用析构函数
delete lt;

📚️ 这里还有一点需要注意,new 是先申请空间再调用构造函数,delete 是先调用析构函数,再释放指针的空间

5. 【拓展】new和delete的配套使用

new和delete一定要配套使用,使用new申请内存空间,就一定要使用delete释放内存空间。为什么?来看下面一段代码

cpp 复制代码
A* p = new A[10];
delete[] p;

已知一个A类型的大小为 4 字节,所以,这里申请 10 个A类型的空间,空间大小应该为 40 个字节。那这里真的是 40 个字节吗?其实不是的,这里在x86的环境下调试会发现,这里申请了 44 个字节。

那new多申请这 4 个字节的空间用来干什么?其实,是为了保存一共这块空间一共申请了多少个A类型的对象,有的人会问保存这个干什么?那如果没有保存这个数值,编译器要怎么确定释放空间时需要调用多少次析构函数呢?

既然这多出来的 4 个字节是存储对象个数的,那么这个空间的数值应该是 10,我们可以使用内存窗口查看。

所以,这里就知道了,当使用new申请多个对象的内存时,它会在原本的空间前面多申请一个内存空间用来存储申请的对象的个数

这里回到刚才的问题,现在,你或许就已经知道为什么new和delete需要配套使用了。

🎀小结:

由于new一次申请多个对象的空间会在空间前面多申请一个内存空间,而如果使用free来释放空间,它并不会将前面多申请的内存空间释放,这样就会导致程序运行崩溃,因此,使用new申请的空间内存应该使用delete释放。

📚️ 使用new[]申请多个对象时,仅仅使用delete释放内存,程序也会崩溃,正确的是使用对应的delete[]

四. operator new和operator delete函数

1. new的底层------operator new函数

在感叹new为何如此方便之时,我们不禁问到,new这么好用,那它的底层是不是很复杂呢?这里先说结论

  • new的底层是 operator new 函数,operator new 并不存在于标准库中,而是一个全局函数,我们可以通过C++参考手册确定

关于 operator new 函数的具体细节这里不过多赘述,而是主要看 new 的底层实现,这里可以通过调试的反汇编观察 new 的底层

cpp 复制代码
//自动调用构造函数
List* lt = new List;
//自动调用析构函数
delete lt;

继续进入 operator new 的函数的实现,惊奇的发现,operator new 的底层居然是 malloc 函数

我们再来理清楚调用逻辑,当使用 new 动态内存申请时,new 会调用 operator new 函数,而 operator new 函数本质上又还是通过 malloc 申请动态内存,所以 new 的本质还是使用 malloc 动态申请内存

📚️ 使用 new 申请空间时,构造函数的调用并不是封装在operator new函数的,而是分开的。其实从刚刚的第一层反汇编就可以看到

2. delete的底层------operator delete函数

delete 的底层和 new 一样都是通过调用其他的函数实现的,delete 对应的函数为 operator delete ,追溯到最底层会发现,delete 释放空间的功能是通过 free 实现的。

这里也还同时验证了上面所说,delete 是先调用析构函数再调用 operator delete 释放指针的内存的。

🎀小结:

  1. new 是通过调用 operator new 申请空间的,而 operator new 又是使用malloc 申请空间的,所以本质上 new 还是使用 malloc 申请空间
  2. delete 是通过 operator delete 释放空间的,本质上它还是使用free释放空间
  3. operator new 和 operator delete 都是全局函数

五. 定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象,其使用格式如下

cpp 复制代码
new(ptr)type;
注:
ptr是指向内存空间的指针
type是要调用的拷贝构造对应的类型

定位 new 表达式一般是搭配内存池的使用,这里在之后的篇章会讲解

cpp 复制代码
class A
{
public:
	A(int a = 1)
		:_a(a)
	{
		cout << "A(int a = 1)" << endl;
	}
	~A()
	{
		_a = 0;
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	A* p = (A*)malloc(sizeof(A));
	new(p)A;
	p->~A();
	free(p);
	//使用 operator new和malloc效果一样
	A* p1 = (A*)operator new(sizeof(A));
	new(p1)A;
	p1->~A();
	operator delete(p1);
	return 0;
}

📚️ operator new 本质上也是调用malloc申请内存空间,所以它的使用方式也和 malloc一样,需要强制类型转换和以字节为单位开辟空间,否则编译器会报错

六. new/delete和malloc/free的区别

语法区别

  • new/delete不需要使用强制类型转换,malloc/free需要
  • new/delete申请空间不需要判断空指针,malloc/free需要
  • new/delete给多个元素申请空间不需要计算空间大小,malloc/free需要
  • new/delete可以在申请空间后直接初始化,malloc/free不可以

📚️ 这里说到使用new申请空间不需要判断空指针是因为new空间申请失败时会抛出异常,使用catch接收异常就知道空间申请失败了。这里new抛异常在之后的篇章会讲解

核心区别

  • new/delete对自定义类型的内存申请和内存释放,会自动调用对应的构造函数和析构函数,malloc/free则没有这个功能

本篇完结

下期预告:

  • 【C++】string(上)
相关推荐
学逆向的1 小时前
汇编——函数
开发语言·汇编·网络安全
愚昧之山绝望之谷开悟之坡1 小时前
回购逆回购
笔记
鱼子星_1 小时前
【C++】string(上):string的基本使用
c++·笔记·字符串
咸鱼翻身小阿橙1 小时前
多线程解析
java·开发语言
AOwhisky2 小时前
Python 学习笔记(第四期)——字符串:格式化、操作与文本处理——核心知识点自测与详解
开发语言·笔记·python·学习·列表·元组·字典
chnyi6_ya2 小时前
论文阅读笔记 | VIDEOPHY: Evaluating Physical Commonsense for Video Generation
论文阅读·笔记
AOwhisky2 小时前
Python 学习笔记(第六期)——函数:定义、参数传递与作用域
笔记·python·学习·云原生·运维开发·函数·参数传递
@@@@@@@@542 小时前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
神仙别闹2 小时前
编写基于C++ Huffman 算法的无损压缩程序和解压程序
java·c++·算法