动态内存的开辟C语言我们可以通过malloc、calloc、realloc、free等函数来完成我们的需求,但是在C++中我们通过new、delete关键字来完成。本章我们将详细讲解new、delete。
平时我们编写的程序有:全局对象、局部对象、static对象、常量、函数体等,那他们存储在哪里呢,如下图:
tip:
栈区(stack)
:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的非静态局部变量、函数参数、返回数据、返回地址等,栈是向下增长的。内存映射段
:内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(在Linux详细讲解)堆区(heap)
:堆用于程序运行时动态内存分配,堆是可以向上增长的。一般由程序员分配释放,生存期由我们决定,非常灵活,但是问题也最多。若程序员自己不释放,程序结束时可能由OS回收。数据段(静态区)
:存储全局数据和static数据。内存在程序编译时就已经分配好了,这块内存在程序的整个运行期间都存在,程序结束后由系统释放。代码段(常量区)
:存放可执行的代码和只读常量,可执行的代码——函数体(类成员函数和全局函数)的二进制代码。混淆点:
代码示例:
int main()
{
//1、使用malloc向堆区申请10个int型的连续空间
int* p1 = (int*)malloc(sizeof(int) * 10);
//判断是否malloc是否开辟成功
if (nullptr == p1)
{
//开辟失败打印错误信息并退出
perror("malloc");
return -1;
}
//malloc使用之前,必须自己初始化,因为malloc申请完空间,没有初始化,直接返回起始地址
free(p1);
p1 = nullptr;//free释放之后p2没有改变,仍然能找到那块空间,所以p2为野指针,有危险,建议free之后都将其置为空
//2、使用calloc向堆区申请10个int型的连续空间
int* p2 = (int*)calloc(10, sizeof(int));
//判断是否calloc是否开辟成功
if (nullptr == p2)
{
//开辟失败打印错误信息并退出
perror("calloc");
return -1;
}
//calloc可以直接使用,因为calloc申请好空间后,会把空间初始化为0,然后返回起始地址
//3、使用realloc可以调整动态开辟内存空间,例如将p2所指的动态内存扩大到20个int
//realloc调整内存空间时存在三种情况:
// ①原地扩容:原有空间之后有足够大的空间,直接在后面追加空间,返回旧地址
// ②异地扩容:原有空间之后没有足够大的空间,在堆区另找一块合适的空间,并把旧空间free,同时返回新空间的起始地址
// ③开辟失败:找不到合适的空间,开辟失败返回null
int* p3 = (int*)realloc(p2, 20 * sizeof(int));//防止开辟空间失败,使用一个新的指针变量来接收realloc的返回值
//判断是否扩容成功,扩容成功仍用p2来指向这块空间
if (p3 != nullptr)
{
//扩容成功
p2 = p3;
p3 = nullptr;
}
else
{
//扩容失败,打印错误信息,并退出
perror("realloc");
return -1;
}
//使用realloc扩容的空间之前,需要初始化
free(p2);
p2 = nullptr;//free释放之后p2没有改变,仍然能找到那块空间,所以p2为野指针,有危险,建议free之后都将其置为空
return 0;
}
tip:
malloc函数
:C语言提供的一个动态内存开辟的函数
calloc函数
:也是C语言提供用来动态开辟内存的函数
realloc函数
:调整动态开辟内存空间的大小的函数
free函数
:C语言提供来专门用来做动态内存的释放和回收的函数
动态内存的两种回收方式
:
void*指针类型
:当用户不确定指针变量是什么类型时,设计成void*,void*是无具体类型的指针
C++兼容C,所以C语言中的内存管理方式在C++中可以继续使用,但是有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:**通过new和delete操作符进行动态内存管理。
代码示例:
int main()
{
//动态申请一个int类型的空间
//C
int* ptr1 = (int*)malloc(sizeof(int));
free(ptr1);
ptr1 = nullptr;
//CPP
int* ptr2 = new int;
delete ptr2;
ptr2 = nullptr;
//动态申请一个int类型的空间,并将其初始化为10
//C
int* ptr3 = (int*)malloc(sizeof(int));
*ptr3 = 10;//malloc不能初始化,realloc只能将其初始化为0
free(ptr3);
ptr3 = nullptr;
//CPP
int* ptr4 = new int(10);//单个对象可以使用()初始化
delete ptr4;
ptr4 = nullptr;
//动态申请10个int类型的空间
//C
int* ptr5 = (int*)malloc(sizeof(int) * 10);
free(ptr5);
//CPP
int* ptr6 = new int[10];
delete[] ptr6;
//动态申请3个int类型的空间,并将其初始化为1,2,3
int* ptr7 = new int[3]{ 1,2,3 };//多个对象可以使用C++11的初始化列表{}初始化
delete[] ptr7;
return 0;
}
tip:
malloc/free和new/delete的区别
:
总结: 动态申请内置类型的数据,new和malloc除了用法上面,其他方面没有什么区别。
祖师爷不会因为用法麻烦就搞出来了new和delete,最重要的是因为malloc和free无法解决自定义类型的动态申请。
代码示例:
//注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// new/delete 和 malloc/free最大区别是 new/delete对于自定义类型除了开空间还会调用构造函数和析构函数
//malloc单纯开空间
A* p1 = (A*)malloc(sizeof(A));
//free单纯释放空间
free(p1);
//new:开空间+调用构造函数
A* p2 = new A(1);
//delete:调用析构函数+释放空间
delete p2;
//new多个自定义对象也会调用多个构造函数
A* p6 = new A[10]{A(1), A(2)};//没有显式初始化的自定义对象调用默认构造初始化
//delete多个自定义对象也会调用多个析构函数
delete[] p6;
return 0;
}
tip:
malloc/free和new/free最大的区别
:在动态申请自定义类型的数据时,malloc/new除了用法上不同,还有一个最大的区别,new会调用构造函数初始化,delete会调用析构函数清理,而malloc与free不会。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)
tip:
代码示例:抛异常的情况
int main()
{
int* p = nullptr;
try
{
do
{
//p = (int*)malloc(1024 * 1024);//malloc开辟失败,返回空
p = new int[1024 * 1024];//new开辟失败,抛异常直接跳到catch
cout << p << endl;
} while (p);
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
异常的相关知识点,后续会详细讲解,现在只需要知道new开辟空间失败后会抛异常。
如果申请的是内置类型的空间,new和malloc,delete和free基本类似。
不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请和释放的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
class Stack
{
public:
Stack(int capacity = 4)
:_top(0)//栈顶指针初始化为0,指向栈顶指针的下一个元素
, _capacity(capacity)
{
_arr = (int*)malloc(sizeof(int) * capacity);
//检查……
}
~Stack()
{
free(_arr);
_arr = nullptr;
_top = 0;
_capacity = 0;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
try
{
//动态申请一个堆区的Stack对象
//使用malloc不会调用构造函数初始化,free也不会调用析构函数清理
//所以使用new和delete
//new:开空间 + 调用构造函数
Stack* p = new Stack;
//delete: 调用析构 + 释放空间
delete p;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
使用malloc动态申请自定义类型,只是单纯的开空间,不会调用构造函数。我们有什么方法可以使已开辟的自定义类型的空间调用构造函数呢》
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
即:非new操作符动态申请的自定义类型,想要显示的调用构造函数,需要使用定位new表达式
。
代码示例:
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
// 定位new/replacement new
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
//显示调用构造函数——需要使用定位new
new(p1)A;// 注意:如果A类的构造函数有参数时,此处需要传参
//显示调用析构函数——直接调用
p1->~A();
free(p1);
return 0;
}
tip:
使用格式
:
使用场景
:
补充: 显示调用析构函数——目标指针变量->析构函数。
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
总结:C++使用new/delete管理内存最大的原因不是用法上变简洁了,而是申请自定义类型new除了开空间还会调用构造函数,delete除了释放空间还会调用析构函数清理。