
🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。
了解类和对象以后,读者应该明白了类和对象c++和c的最大的区别,而除此之外,c++和c在内存管理上也有一些区别,c++极大的简化了c的
malloc,创造出了new和delete,本篇文章我们就来了解一下c++的内存管理方式
首先我们来看一下虚拟内存空间的分区:

对于内存来说,有如上图的虚拟内存分区,内核空间是交给操作系统管理的,用户无法直接使用,只读数据段又叫做常量区,数据段又叫做静态区。
我们由一段代码来了解各种类型的变量是储存在哪个位置的:
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);
}那我们来看下面的问题:
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____ globalVar在哪里?
globalVar是普通类型的全局变量,储存在静态区(数据段)中
staticGlobalVar在哪里?
staticGlobalVar是static修饰的全局变量,储存在静态区(数据段)中
staticVar在哪里?
staticVar是static修饰的函数内部变量,储存在静态区(数据段)中
localVar在哪里?
localVar是普通的函数内部变量,储存在栈中
num1 在哪里?
num1是普通的函数内部的数组,储存在栈中
char2在哪里?
char2和num1一样,储存在栈中
*char2在哪里?
*char2是char2的解引用,也就是普通的函数中的数组首元素,储存在栈中
pChar3在哪里?
pChar3是普通的函数中的指针,储存在栈中
*pChar3在哪里?
*pChar3是字符串常量"abcd"的首字符,该字符串常量储存在常量区(只读数据段)中
ptr1在哪里?
ptr1是普通的函数中的指针,储存在栈中
*ptr1在哪里?
*ptr1是malloc出来的空间,属于动态申请,其内部数据储存在堆中鉴于之前不少读者已经了解过C语言,这里就不在详细说明这些动态申请内存相关的函数 如果了解比较少的可以看这个文章:精通C语言(4.四种动态内存有关函数),接下来我们就来看一段代码:
void Test ()
{
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
free(p3 );
}这里有两个问题:
malloc是申请空间但不初始化;calloc是申请空间并初始化;realloc是在原空间的基础上调整空间p3是在p2的基础上调整空间,这有三种结果:p3 和 p2 指向同一个地址 → 此时只需 free(p3)(等价于 free (p2)),若再 free (p2) 会导致重复释放(未定义行为,可能崩溃);realloc 会:① 申请新的内存块;② 把原 p2 指向的内容拷贝到新块;③ 自动释放原 p2 指向的内存;④ 返回新块地址给 p3 → 此时 p2 变成野指针,绝对不能 free (p2)(释放已被系统回收的内存,未定义行为);NULL,此时 p2 仍然有效(指向原内存块),需要手动 free (p2)。在不考虑realloc失败的情况下,p2是不需要被free的
C的内存管理我们就说到这里,接下来我们看一下c++的内存管理。
我们先来看一下他们的用法:
//new和delete是关键字,用于动态申请内存
void Test3()
{
// 动态申请一个int类型的空间
int* ptr4 = new int;
// 动态申请一个int类型的空间并初始化为10
int* ptr5 = new int(10);
// 动态申请10个int类型的空间
int* ptr6 = new int[10];
delete ptr4;
delete ptr5;
delete[] ptr6;
//申请多个对象并且初始化:
int* ptr7 = new int[10] {0};//全部初始化为0
int* ptr8 = new int[10] {1, 2, 3};//后面是0
delete[] ptr7;
delete[] ptr8;
}可以看到以上的几种用法中,初始化是 “可选项”,并且去掉C中sizeof()的方式,直接采用了类型开空间,这极大的方便了使用者。
使用中的new和delete(链表):
struct ListNode
{
ListNode(int x)
:val(x)
,next(nullptr)
{}
void Print(ListNode* head)
{
ListNode* pcur = head;
while (pcur != nullptr)
{
cout << pcur->val << "->";
pcur = pcur->next;
}
cout << endl;
}
int val;
ListNode* next;
};
int main()
{
ListNode* n1 = new ListNode(1);
ListNode* n2 = new ListNode(1);
ListNode* n3 = new ListNode(1);
ListNode* n4 = new ListNode(1);
n1->next = n2;
n2->next = n3;
n3->next = n4;
n1->Print(n1);
return 0;
}那么c++仅仅是这样为了方便就创造出了new和delete吗?其实不是的,他们更本质的区别其实还是来自c++最核心的地方:类和对象
c++内存管理最重要的点是在调用new时候会调用构造函数,调用delete时候会调用析构函数
class A
{
public:
A(int a1 = 0,int a2 = 0)
: _a1(a1)
,_a2(a2)
{
cout << "A():" <<_a1<<"and" << _a2 << endl;
}
A(const A& aa)
: _a1(aa._a1)
, _a2(aa._a2)
// 显式初始化成员变量
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A():"<<_a1 << "and" << _a2 << endl;
}
private:
int _a1;
int _a2;
};
void test4()
{
//动态申请一个A类类型的对象
A* p1 = new A;
A* p2 = new A(1);
delete p1;
delete p2;
}
int main()
{
// new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
//还会调用构造函数和析构函数,而malloc不会
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* p6 = new A[10];
free(p5);
delete[] p6;
//来看看和匿名对象配合使用:
A aa1 = { 0, 0 };
A aa2 = { 1, 1 };
A aa3 = { 2, 2 };
A* p7 = new A[3]{aa1,aa2,aa3};//调用构造
//等价于:
A* p8 = new A[3]{ A(0,0),A(1,1),A(2,2)};
//等价于:
A* p9 = new A[3]{ {0,0},{1,1},{2,2} };
//三条完全等价
return 0;
}这个特性决定了c++可以直接开空间开出一个自定义类型的对象来,还可与直接初始化。
实际上new和delete是对c中的malloc等进行了升级:
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
/*
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 和 operator delete是通过malloc和new来申请空间的,也就是说new和delete的底层是malloc和free
1. 内置类型:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[] 和 delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
2. 自定义类型:
void func()
{
//throw try/catch
int n = 0;
while (1)
{
void* p1 = new char[1024 * 1024 * 1024];
cout << p1 << "->" << n << endl;
n++;
}
}
//但是注意,申请的是虚拟内存
//总空间:32位下:分配2^32byte->大概42亿九千万->4,294,967,296
// 64位下:分配2^64byte->18,446,744,073,709,551,616空间
//栈空间:32位下:8M
//堆空间:32位下:1.8GB左右
int main()
{
//申请内存失败怎么样
try
{
func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
class A
{
public:
// 带默认参数的构造函数:初始化_a,打印对象地址
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl; // this 指向当前对象的地址
}
// 析构函数:打印对象地址
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a; // 类的成员变量
};
// 定位new/replacement new
int main()
{
// ========== 第一组:malloc + 定位new ==========
// 1. 仅分配内存(大小=A对象大小),未调用构造函数,p1指向的不是合法对象
A* p1 = (A*)malloc(sizeof(A));
// 2. 定位new:在p1指向的已分配内存上,显式调用A的构造函数(无参版)
// 此时p1才指向一个合法的A类对象
new(p1)A;
// 3. 手动调用析构函数:定位new创建的对象,编译器不会自动调用析构,必须手动调用
p1->~A();
// 4. 释放原始内存:malloc分配的内存,用free释放
free(p1);
// ========== 第二组:operator new + 定位new ==========
// 1. operator new 等价于 malloc:仅分配内存,无构造
// operator new 是C++内置函数,返回void*,无需强制类型转换(此处转换是为了统一写法)
A* p2 = (A*)operator new(sizeof(A));
// 2. 定位new:在p2指向的内存上,调用A的有参构造函数(传10)
new(p2)A(10);
// 3. 手动调用析构函数
p2->~A();
// 4. operator delete 等价于 free:释放operator new分配的内存
operator delete(p2);
// 上面两组操作完全等价:malloc/free ≈ operator new/operator delete
return 0;
}malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
一般情况下我们都会规范配套使用内存的开辟工具,但是如果不规范使用呢?
将 new 和 free 混搭 :
int main()
{
//内置类型:
int* p1 = new int;
free(p1);
//正确:delete p1;
//自定义类型:
A* p2 = new A;
//free(p2);
//这里是没有析构函数的,有内存泄露的风险
delete p2;
return 0;
}将 new[] 和 delete 搭配
class B
{
private:
int _b1 = 1;
int _b2 = 2;
};
class A
{
public:
A(int a1 = 0, int a2 = 0)
: _a1(a1)
, _a2(a2)
{
cout << "A():" << _a1 << "and" << _a2 << endl;
}
A(const A& aa)
: _a1(aa._a1)
, _a2(aa._a2)
// 显式初始化成员变量
{
cout << "A(const A& aa)" << endl;
}
~A()
{
cout << "~A():" << _a1 << "and" << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
//内置类型:
int* p1 = new int[10];
delete p1;
//不会崩溃,底层:new[] → operator new[] → 连续多次operator new → 连续多次malloc → 连续空间
//这意味着p1这个指针只要是空间的头,就能一次delete(free)掉
//自定义类型:
B* p2 = new B[10];
delete p2;
//不会崩溃
A* p3 = new A[10];
delete p3;
//会崩溃
}下面我们详细的从底层分析一下:为什么A、B同时都是自定义类型,而且两个对象都含有两个成员变量,A情况会崩溃,B情况就不会崩溃呢?
实际情况是:A在new[]时候会在p3之前,存一个int类型的值,表示对象的个数,而B没有存这个值

为什么B没有存这个值呢?
这就导致了p3指针是在整个申请空间中间的,p2是在空间开头的,直接delete就能释放掉,p3则不行,堆管理器检测到非法释放地址。