学习之前先做两道题,来复习一下变量的内存分布。
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";
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);
}
在c语言中我们使用malloc,relloc,calloc,来动态开辟空间。
经常会问到这三者的区别。
那么c++为什么还要继续用new与delete?
问题先不回答,只有先了解了使用,才能谈到区别
new和delete是操作符,传入对象个数,不需要强转
malloc和free是函数,传入字节数,需要强转
内置类型
//c语言
int *p = (int*)malloc(sizeof(int));
int *p1 = (int*)malloc(sizeof(int)* 10);
free(p);
free(p1);
//c++
int *p2 = new int;
//定义并初始化
int *p3 = new int(10);
//定义数组
//那么可以像上面也初始化呢,不行的
//int *p4=new int[10](1);
int *p4 = new int[10];//只能调用默认构造函数
delete p2;
delete p3;
delete[] p4;
也就是说对于内置类型,malloc/free与new/delete区别不大,就是new一个元素的时候,申请单个空间,可以初始化,new[]申请多个连续空间,delete数组的时候需要delete加[ ]。对于内置类型一般不会申请失败。
内置类型区别不大,那么对于自定义类型呢?
malloc,d1对象和只能看见数组的第一个对象没有初始化,窗口没有任何输出
new的话,调用了自定义类型的默认构造函数初始化,输出1+10次
delete的话,调用了析构函数一共1+10次。
即
opeator new 与opeator delet是c++申请空间的库函数。用法与malloc和free一样。不会调用构造函数。那它到底有什么意义。我们申请一个整形最大值的空间。(31位全为1,最高位为符号位0)
c语言处理错误的方式一般是返回错误码,申请失败的话,malloc返回0。
c++是面向对象语言,所以要有面向对象的处理模式,他错误一般是抛异常。
我们可以用try…catch语句捕获它。可以看到扩容失败
我们来读一下operator 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)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
他底层实际上是是调用了malloc,只不过在失败的时候会抛出异常。
operator delete的源码如下:
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;
}
最终operator delete也是调用了free。只不过free又调用了_free_dbg
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
所以实际上
new:
operator new + 构造函数
operator new 相当于 malloc且malloc失败抛出异常
delete :
析构函数+operator delete
一定是先析构,清理对象内部资源。然后在free对象(假如先free对象不就出问题了)
operator delete 相当于free
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1);
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 l;
return 0;
}
假如这个链表,需要频繁的使用创建节点,释放节点。频繁的去找系统申请,释放,效率低还有内存碎片等问题。就像家里给生活费,用一次给一次很不方便,一次给一个月的生活费就不用在麻烦家里。这里的场景也是一样的,系统一次性给你一块内存,你自己去折腾。也就是我们通常所叫的池化技术,内存池就是其一种机制。
在回到我们这个问题上,正常情况下,使用new,new调用operator new+构造函数,全局的operator new就会调用malloc,那么就会向系统申请,我们将operator new 重载为成员函数,这样new就会调用我们自己实现的operator new +构造函数
#include
using namespace std;
class Date
{
public:
Date(int year,int month,int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date* p1 = new Date(2000, 1, 1);
//Date* p2 = new Date[10];
return 0;
}
new一个日期类对象,并初始化空间。因为没有默认构造函数,所以对于申请10个对象的数组我们没有办法。
想要让一次性申请多个对象成功,第一个办法把构造函数变成默认构造函数。假如不允许呢?
继续想办法
首先我们知道new是operator new和构造函数。那么我们这样先调用operator new,在调用构造函数。
Date* p2 = (Date*)operator new (sizeof(Date) * 10);
//构造函数该怎么调用
我们发现构造函数是我们定义对象的时候自动调用的,怎么显示调用它呢?
现在我们想要显示的调用构造函数,来初始化一块已有的空间。
这样就完成了显示调用。当然还要清理资源,释放空间。
Date* p2 = (Date*)operator new (sizeof(Date) * 10);
//构造函数该怎么调用
int n = 10;
for (int i = 0; i < 10; i++)
{
new(p2+i)Date(2000, 1, 2);
}
//析构函数可以直接显示调用
for (int i = 0; i < 10; i ++)
{
(p2 + i)->~Date();
}
operator delete(p2);
定位new,语法就是,new(指针)类型(参数列表)。
当我们没有默认构造函数而且我们不能把他的构造函数改成默认构造函数,我们申请多个空间//Date* p2 = new Date[10];
不能用,因为它调用默认构造函数。但我们知道new是调用operator new+构造函数。所以我们先调用operator new,由于构造函数对象生成默认调用,然后在显示的调用构造函数去初始化它。
对于内置类型:
对于malloc和free区别不大。new在申请空间失败时会抛异常,malloc会返回NULL。不过对于内置类型基本不会申请失败。
对于自定义类型:
new:
delete:
new T[N]的原理
delete[]的原理
在区分一下概念
A a
是在栈上分配的内存,生命周期为当前作用域。定义时调用构造函数。
A *a
是定义了一个指向A的对象的指针,在这里它还没有指向任何对象。
A *aaa = new A;
这是在堆上分配一块内存用来存放一个A的对象,自动调用它的构造函数。然后把这块内存的首地址赋给A的指针aaa,除非运行代码delete aaa,这个对象一直存在,即使指针aaa生命周期结束(这就是传说的内存泻露);
开始测试:
//1G=1024m=1024*1024kb=1024*1024*1024byte
void* p1 = operator new( );
2g无法申请(整型溢出,记得在1024后面+u代表无符号整形)
1.5g可以
经过测试1.8g几乎是极限。
其实很容易想到,进程地址空间32位的话是232字节,就是4g,其中1g为内核空间,剩余3g是用户。堆其实很大,大概2g。
我们将进程地址空间变为64位,264,就是264字节=17179869184G=16777216T
然后进行申请就完成了2g的申请。
4g也同样可以
主要考查的是操作系统中对进程地址空间和虚拟内存的理解。