C++动态内存管理new,delete
- 1.C/C++内存分布
- 2.C语言中的内存管理方式
- 3.C++中的内存管理方式new,delete
- 3.1C++中的内置类型new,delete
- 3.2new,delete操作自定义类型
- 3.3 new和delete匹配
- 4. operator new与operator delete函数
- 4.1new和delete底层实现
- 4.2new和delete匹配,new []和delete []匹配
- 5. 定位new表达式(placement-new)
- 5.1定义
- 5.2使用场景
所属专栏:C“嘎嘎" 系统学习❤️
🚀 >博主首页:初阳785❤️
🚀 >代码托管:chuyang785❤️
🚀 >感谢大家的支持,您的点赞和关注是对我最大的支持!!!❤️
🚀 >博主也会更加的努力,创作出更优质的博文!!❤️
1.C/C++内存分布
在学习C语言的时候我们有讲到过动态内存够管理部分C语言当中的动态内存管理在这一篇文章中我们也有详细了解到什么是动态内存,为什么要存在动态内存。
这里我们也做简单的回顾一下:
- 在内存中为什么要存在内存区域划分呢?因为在我们向内存输入数据的时候,
不同的数据有不同存储需求
,而不同的内存区域就是为了满足对不同数据的存储需求而设计的。- 比如一些数据可能就是一个
临时的数据
,我们就可以存储到栈区。 - 有些数据可能是
动态使用的,需要时就创建,不需要是就销毁
,就可以存到堆区。 - 有些数据可能是要
长期使用,整个程序期间使用,随时都可能需要使用的
,这个时候就可以存放在静态区。 - 也有一些数据是
只读不修改的
,比如常量,可执行代码,这些数据就可以存放在常量区。
- 比如一些数据可能就是一个
我们可以用一张内存区域分布图来概括我们的内存:
说明:
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,
栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享内存,做进程间通信。 - 堆用于程序运行时动态内存分配,
堆是可以上增长的。
- 数据段–存储全局数据和静态数据。
- 代码段–可执行的代码/只读常量。
下面我们来做一些判断内存区域的题型:
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在哪里?____ ;2.staticGlobalVar在哪里?____;3.staticVar在哪里?____ ;4.localVar在哪里?____;
5.num1 在哪里?____;
6.char2在哪里?____ ;7.* char2在哪里?___;
8.pChar3在哪里?____ ;9.* pChar3在哪里?____;
10.ptr1在哪里?____ ;11.* ptr1在哪里?____;
-
填空题:
1.sizeof(num1) = ____;2.sizeof(char2) = ____;3. strlen(char2) = ____;
4.sizeof(pChar3) = ____; 5.strlen(pChar3) = ____;
6.sizeof(ptr1) = ____;
正确答案:
一:1-5:CCCAA,6-11:AAADAB
二:40,5,4,4/8,4,4/8
第一题的前5个因该都没什么问题,问题可能出现在6-11题,我们讲解一下;
2.C语言中的内存管理方式
在学习C语言的时候我们有讲到过动态内存够管理部分C语言当中的动态内存管理在这一篇文章中我们也有详细了解到什么是动态内存,为什么要存在动态内存,如果有对知识点模糊不清的可以回顾一下。
3.C++中的内存管理方式new,delete
- C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因
此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1C++中的内置类型new,delete
使用语法:
new + 类型(初始值)
delete + 类型
new + 类型[大小]
delete[] + 类型
int main()
{
//开辟一个int类型的空间
int* ptr1 = new int;
//开辟一个int类型的空间并初始化为2
int* ptr2 = new int(2);
delete ptr1;
delete ptr2;
//开辟一个int类型的数组,大小为3
int* ptr3 = new int[3];
//开辟一个int类型的数组,大小为10,并初始化
int* ptr4 = new int[10]{1,2,3,4};//没有被初始化的编译器会默认初始化为0
delete[] ptr3;
delete[] ptr4;
return 0;
}
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用
new[]和delete[],注意:匹配起来使用。
3.2new,delete操作自定义类型
class text
{
public:
text(int val)
:_a(val)
{
}
private:
int _a;
};
int main()
{
//C语言中的malloc
text* t1 = (text*)malloc(sizeof(text));
//C++中的new
text* t2 = new text(1);
free(t1);
delete t2;
return 0;
}
从上面malloc和new出来用法不一样之外没什么区别,唯一的区别就是malloc无法对自定义类型进行初始化,而new可以对自定义类型进行初始化,也就是说new做了两件事:开辟空间+构造函数。delete也做了两件事情:调用析构函数+释放空间。
3.3 new和delete匹配
class stack
{
public:
stack(int capacity = 4)
{
cout << "stack()" << endl;
_a = new int[capacity];
_size = 0;
_capacity = capacity;
}
~stack()
{
cout << "~stack()" << endl;
delete[] _a;
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
stack* p1 = new stack(4);
delete p1;
return 0;
}
我们看一下上面一段代码,他的执行顺序什么呢?
- 那如果我们把delete p1 换成 C语言中的free(p1)会怎么样呢?
是不是free直接释放了对象的空间,而_a没有被释放啊,就会有内存泄漏。
所以这里new一定要和delete匹配着使用。
4. operator new与operator delete函数
4.1new和delete底层实现
- new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是
系统提供的全局函数
,new在底层调用operator new全局函数来申请空间,delete在底层通过
operator delete全局函数来释放空间。要注意的是operator new和operator delete不是重载 底层其实是封装了malloc和free。
- 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申 请空间失败时会抛异常,malloc会返回NULL。
- 自定义类型
- new的原理
- 调用operator new函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造
- delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用operator delete函数释放对象的空间
- new T[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对
象空间的申请 - 在申请的空间上执行N次构造函数
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对
- delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释
放空间
- new的原理
官方库给定的封装:
/*
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) //最要注意的是这里,malloc开辟失败是返回0,而operate delete开辟失败不是返回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来释放空间的。 - 而且我么也可以发现 operator new(size_t size)的参数是size_t size是一个大小值,operator delete(void* pUserData)的参数是一个对象。
我们也可以从汇编的角度来看一看:
class text
{
public:
text(int a = 0)
{
_a = a;
cout << "text()" << endl;
}
~text()
{
cout << "~text()" << endl;
}
private:
int _a;
};
int main()
{
text* p1 = new text;
delete p1;
text* p2 = (text*)operator new(sizeof(text));
operator delete(p2);
return 0;
}
这里只调用了一次构造和一次析构。
- text* p1 = new text;
- delete p1;
- 这里从汇编的角度我们就很清楚的可以看到,为什么new会做两件事情:
开空间+调用构造
,delete会调用析构+释放空间。
底层其实是调用了malloc和free,只不过是被封装过的maloc和free。
我们也可以这样理解:
new = operator new + 构造函数
delete = 析构函数 + operator delete
4.2new和delete匹配,new []和delete []匹配
class text
{
public:
text(int a = 0)
{
_a = a;
cout << "text()" << endl;
}
~text()
{
cout << "~text()" << endl;
}
private:
int _a;
};
int main()
{
text* p1 = new text[10];
delete[] p1;
return 0;
- 我们看一下new[]是怎么开辟空间的。首先text类的大小是4个字节,开辟10个就是40个字节。这里可以类比一下开辟数组,数组是一个连续的内存,这里同样也是,10个类的物理内存是连续的,也就是说底层只会调用一次malloc,但是会调用10次析构函数。
- 如果按照我么正常的计算,这里内存因该会开辟40个字节的空间。
我们进入汇编查看一下:
- 这里从监视窗口中我们看到的却是44个字节,对开了4个字节。
我们在到内存中看一看:
- 至于为什么要多开一个个数的空间呢?
是为了delete[]准备的,我们知道,new[10]是调用一次底层的malloc加上调用了10次构造函数,而之所以会调用10次构造函数是因为我们告诉编译器我们要开辟10个类内存大小。但是delete[ ]我们是没有告诉他有多少个的,它也就不知道要析构多少次,所以这块多出来的空间是为了提供给delete[]告诉它我们开辟了多少空间,让它析构多少次。
- 所以如果我们把delete[]换成delete或者free会怎么样呢?
程序会崩掉,因为如果直接用delete或者free,编译器会认为你要释放的是一个对象,也就是从上面的p1开始释放,没有从多开的那块空间开始释放,也就只释放了部分的空间,而我们知道free是不能释放部分内存的
,所以程序会崩。
这里就说明了,我们要养成好匹配的习惯。
new和delete匹配。
new[] 和delete[]匹配。
5. 定位new表达式(placement-new)
5.1定义
-
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表。 -
问一个问题,构造函数能显示调用吗?
答案是不能显示调用的,构造函数是创建对象时自动调用的。
但是析构函数可以显示调用。
所以C++中可以用定位new来显示调用构造函数
class text
{
public:
text(int a = 0)
{
_a = a;
cout << "text()" << endl;
}
~text()
{
cout << "~text()" << endl;
}
private:
int _a;
};
int main()
{
text* p1 =(text*)operator new(sizeof(text));
new(p1)text(1);
//上面两个加起来就等于text* p1 = new text;
p1->~text();
operator delete(p1);
//上面两个加起来就等于delete p1;
delete[] p1;
return 0;
}
5.2使用场景
- 使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如
果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化