二次修订于date:2024:3:13
C语言中malloc、calloc、realloc、和free这几个函数用来负责动态开辟内存,在C++中也可以使用这些函数,但是C++推出了新的操作符用来动态内存的开辟,就是new、delete
内存分布
了解一下C/C++的虚拟内存的分段
如图:从上到下依次是栈,堆,数据段,代码段。
栈一般是用来存放局部变量,返回值,维护函数栈帧等
内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
堆就是负责动态内存开辟,也就是new开辟内存的地方。
数据段存放的是静态变量,全局数据,全局数据又会分为已初始化全局数据区和未初始化全局数据区。
代码段中存放的就是可以执行代码编译成的指令(一般是在汇编阶段完成后存放的二进制机器指令,同时常量也是存放在这块区间)。
栈是向下增长的,堆是向上增长的。也即是堆从低地址到高地址,但是注意在malloc的时候并不一定后malloc出的空间地址一定比前面的地址高,因为如果前面有空间释放,那后面malloc出的空间就可以在释放的空间处再次占用。
C语言的malloc、calloc、realloc
这几个函数他们的作用和区别是什么呢?
malloc就是在堆上动态开辟一块空间不初始化,calloc就是开辟空间然后会将开辟的空间全部初始化为0,realloc是在一块开辟好的空间上进行扩容,这里有两种扩容方式:原地扩容,异地扩容,当这段空间后面的连续空间满足扩容需求的时候就会原地扩容,当后面的连续空间不满足扩容要求的时候就会在另一个地方新开辟一段空间然后将原空间的数据拷贝过去。
如果realloc的第一个参数是NULL的话,此时realloc等同于malloc
void Test ()
{
int* p1 = (int*) malloc(sizeof(int));
free(p1);
// 1.malloc/calloc/realloc的区别是什么?
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
free(p3 );
}
这里p2是不需要free的,因为p2和p3,如果原地扩容那么指向同一块空间,free一次就够了,如果是异地扩容,那么p2指向的原来的空间就会在拷贝完成后被free掉。
如果realloc 异地扩容,那么就会在新的地址处开辟空间,将内容拷贝到新空间,然后将旧地址的内存空间释放掉,返回新的地址。
C++的内存管理方式
new和delete不是函数而是操作符也是C++的关键字。
new、delete在内置类型上的使用
void Test1()
{
int* p1 = new int;
int* p2 = new int(3);
int* p3 = new int[5];
int* p4 = new int[5]{ 1,2,3 };
delete p1;
delete p2;
delete[] p3;
delete[] p4;
}
int main()
{
Test1();
return 0;
}
对于单个内置类型对象,可以直接写new int,要初始化,就在int后面加圆括号和初始化的值,想要new一个数组,就直接加方框里面写数组的大小。如果想要对数组里面的值初始化,可以在方框后直接接一个大括号里面放上初始化的值,注意这里是类似数组,如果是不完全初始化那么剩下的值会被自动初始化成为0。但是这种语法是C++11里面才有的。
delete释放内存的时候对于数组要加上一个方括号,表示要释放的是多个对象。
要注意区分new的方括号和圆括号,方括号里面的数字代表的是申请的对象个数,圆括号里面的数字代表的是初始化的内容(对于char类型可以用单个字符初始化)
new、delete在自定义类型上的使用
new和delete在自定义类型上使用的时候,new创建对象会自动调用构造函数,delete释放对象的时候会先调用析构函数清理资源。而malloc和free只是负责开辟空间和释放空间。
struct ListNode
{
ListNode(int val = 0)
:_next(nullptr)
,_prev(nullptr)
,_val(val)
{}
~ListNode()
{
_next = nullptr;
_prev = nullptr;
_val = 0;
}
ListNode* _next;
ListNode* _prev;
int _val;
};
int main()
{
ListNode* p1 = new ListNode;
ListNode* p2 = new ListNode(9);
ListNode* p3 = new ListNode[5];
ListNode* p4 = new ListNode[5]{ 1,2,3,4 };
delete p1;
delete p2;
delete[] p3;
delete[] p4;
return 0;
}
new和delete对于自定义类型的用法基本接近于对于内置类型的用法,区别就是会调用构造函数和析构函数,这也符合C++面向对象的特性,所有对象在创建的时候就已经完成了初始化。
operator new函数和operator delete函数
new和delete是两个操作符也是关键字,而operator new函数和operator delete函数就是c++系统库提供的全局函数,可以用来完成对象的申请和销毁,new底层是调用operator new + 构造函数来完成的。delete底层是调用析构函数 + operator delete函数来实现的。
这里的operator new和operator delete函数于malloc和free有什么区别呢?
结论:operator new实际是封装了malloc + 申请失败抛异常,所以这个函数申请失败会抛异常,malloc申请失败会返回空指针,而operator delete和free就差不多了,operator delete底层实际上也是调用free来实现的。
下面这是operator new和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);//这里实际就是调用了free函数
__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、free是一样的。
int main()
{
ListNode* p1 = (struct ListNode*)operator new(sizeof(ListNode));
operator delete(p1);
return 0;
}
operator new 和operator delete的类专属重载
这是在某个类中重载了这两个函数,之后申请这个类的空间或者释放就会调用类里面重载的operator new和operator delete。
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1);//调用STL里面的简单内存管理器
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
};
class List
{
public:
List()
{
_head = new ListNode;
_head->_next = _head;
_head->_prev = _head;
}
~List()
{
ListNode* cur = _head->_next;
while (cur != _head)
{
ListNode* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
private:
ListNode* _head;
};
int main()
{
List l1;
return 0;
}
通过运行后的控制台我们可以看到这时候的new和delete调用的是重载后的operator new和operator delete。
那么重载这两个函数有什么用呢?首先通过重载了类专属的operator new和operator delete实现的是在内存池中为链表开辟节点和释放节点。提高了开辟和释放的效率。
这就是池化技术,这里如何理解内存池能够提高效率呢?简单举例:比如一日三餐都需要用水,可是唯一的水井在两公里外,你每次用水都要跑到两公里外去打水,效率很低,然后你想办法,在家里建了一个蓄水池,只需要每天早上将蓄水池灌满,后面使用水的时候效率就变得很高了。
所以内存池里面的内存依然是从操作系统中申请出来的,存放在内存池中,要使用的时候就可以用很高的效率完成内存的申请和释放。
new和delete的实现原理
1.new的实现原理是,先调用operator new 申请空间,然后调用构造函数对这个对象进行初始化。
2.delete的实现原理,先调用析构函数清理资源,然后调用operator delete函数释放对象空间。
3.new [N]的实现原理,调用operator new[N]函数,这个函数是调用了N次operator new函数申请出来N个对象,然后调用N次构造函数对这N个对象进行初始化。
4.delete []先在要释放的对象空间上调用N次析构函数完成N个对象的资源清理,然后调用operator delete[]来释放空间,operator delete[]实际也是多次调用operator delete来释放空间的。
new和delete使用不匹配问题
如果使用new int[10]{1},申请了10个空间,使用delete释放。这就是使用不匹配。
当不匹配的时候程序可能会崩溃也可能不会崩溃,如果显示的写了析构函数,那程序就会崩溃,测试环境为VS2013,申请的10块空间的时候实际申请了11个空间,在第一个空间之前编译器多申请了一个空间用来存放10,方便后续调用delete []释放的时候能知道调用多少次析构函数清理。如果此时使用delete释放空间,会忽略第一个存放10的空间,从中间释放整块空间就会造成程序崩溃。
如果没有显示的写出构造函数程序不会崩溃,因为编译器生成的构造函数什么也不干,编译器就不会调用析构函数,所以一开始开辟空间的时候也不会多余开辟一个空间,因此此时使用delete释放空间程序就不会崩溃。
定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式: **
new (place_address) type或者new (place_address) type(initializer-list) **
**place_address必须是一个指针,initializer-list是类型的初始化列表 **
**使用场景: **
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,需要使用new的定位表达式,显示调构造函数进行初始化
class Test
{
public:
Test(int val = 0,int te = 0)
:_val(val)
,_te(te)
{}
private:
int _val;
int _te;
};
int main()
{
Test* p1 = (Test*)malloc(sizeof(Test));
Test* p2 = (Test*)malloc(sizeof(Test));
new(p1)Test(20,30);
new(p2)Test;
return 0;
}
内存管理常见问题
malloc/free 和 new/delete的区别
- malloc和free是函数,new和delete是操作符
- malloc出的空间不会初始化,new会初始化(自定义类型)
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常(try、catch)
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间
后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
内存泄漏
内存泄漏:是指申请的空间在不使用了之后并没有还给系统,而是失去了对这块内存的控制,这并不是指的物理空间上面内存的消失而是在虚拟内存分段后,因为设计错误,导致对某块内存失去控制因而造成了内存的浪费。
危害:内存泄漏会使得服务器的内存越来越小,也会变得越来越卡,直到最后服务器崩溃。运行时间很短的程序内存泄漏影响并不大,因为在进程结束后所有的内存都会被操作系统回收。
void MemoryLeaks()
{
// 1.内存申请了忘记释放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.异常安全问题
int* p3 = new int[10];
Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
delete[] p3;
}
这里要注意一旦捕获到异常,就会直接跳转到捕获处,申请失败后面的语句就不会被执行到了。
注意:内存泄漏是指指针丢了还是内存丢了?
实际是指针丢了,因为指针保存着这块内存的地址,有这个地址就能找到这块内存,指针没了,这块内存就失控了,就浪费了,形成了内存泄漏。内存是不会丢的,只会失去控制。
内存泄露的分类
1.堆内存泄露(Heap Leak)
堆内存指的是程序执行中通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
2.系统资源泄漏
指程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统
资源的浪费,严重可导致系统效能减少,系统执行不稳定。
如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。因为会直接跳到捕捉异常的地方,之前的语句有些不会被执行。需要智能指针来管理才有保证。 - 采用RAII思想或者智能指针来管理资源。
- 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
如何一次性申请4G的内存空间?
在32位机器上,内存总共只有4GB而分配给堆的实质只有2GB左右,所以要申请4GB我们必须要换成64位机,这时候64位的地址线最多可以管理171亿多GB的内存空间。
下面这段代码就是可以申请4GB的空间,只要打开任务管理器查找到进程就可以看到
// 将程序编译成x64(x86_64)的进程,运行下面的程序试试?
#include <iostream>
using namespace std;
int main()
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
return 0;
}