一、内存区域分布
首先我们来看一段代码并尝试解决以下问题:
- 1. GlobalVar是全局变量,存储在数据段(静态区),选C。
- 2. staticGlobalVar是静态全局变量,也存储在数据段(静态区),选C。
- 3. staticVar是静态的局部变量,存储在数据段(静态区),选C。
- 4. localVar是局部变量,存储在栈区当中,选A。
- 5. num1是一个局部变量(存储着数组首元素的地址),存储在栈区中,选A。
- 6. char2与num1相同,是局部变量,选A。
- *char2表示字符数组中的字符‘a’(即访问的是数组0下标的地址),存储在栈区中,选A。
- 7. pchar3是指向常量字符串首字符的指针,是一个局部变量,选A。
- *pchar3表示常量字符串中的字符‘a’,属于常量,存储在代码段(常量区),选D。
- 8. ptr1是一个局部指针变量,存储在栈区,选A。
- *ptr1是动态开辟的内存区域中的值,存储于堆区中,选B。
这里重点关注一下*char2和*pchar3的存储位置:
- char2 所在语句的含义是将字符串"abcd"存储到字符数组中,本质是存储到了变量当中,所以解引用之后得到的字符肯定是在栈区中存储的;
- pchar3 所在语句是将常量字符串"abcd"中首字符的地址存放于指针变量当中,该指针变量的值是存储在栈区当中的,但是解引用之后得到的字符是常量,所以*pchar3肯定存储于常量区。
接下来,我们画图表示一下内存区域的分布: c++的动态内存分配与c语言相同,也是在堆区中进行操作的。
二、c++中的动态内存管理方式
之前在c语言当中,我们使用malloc/calloc/realloc/free函数来实现动态内存管理,但由于使用方式较为麻烦(例如要手动计算申请的内存大小、检查返回值等)
所以c++引入了两个操作符,便于我们更高效地实现动态内存管理:new和delete。
接下来我们从代码角度来解释这两个关键字的使用方法。
1. new与delete对内置类型的操作
int main()
{
int* p1 = new int;//动态申请一个int类型的空间
int* p2 = new int(10);//动态申请一个int类型的空间,并初始化为10
int* p3 = new int[10];//动态申请10个int类型的空间
int* p4 = new int[10] {10};//动态申请10个int类型的空间,并将第一个元素初始化为10,其余元素为0
//这里的初始化规则与数组定义时的初始化相同
//释放内存
delete p1;
delete p2;
delete[] p3;
delete[] p4;
return 0;
}
注意:当我们释放连续的空间时,delete之后要加上“ [ ] ”。
可以看到,我们使用new操作符申请内存时,不仅不用sizeof来计算申请所需的空间大小,而且还能对申请的空间进行初始化,十分方便。
2. new与delete对自定义类型的操作
new和delete申请和释放自定义类型的空间与内置类型的语法相同。
当我们使用new/delete操作自定义类型时,它们与malloc/free最大的区别是:
- new在申请内存空间之后还会调用构造函数对该空间进行初始化;
- delete会调用析构函数,然后释放内存空间。
- 而malloc/free只会开辟空间,并不会调用这两种函数。
代码示例:
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 10, int c = 20)
:_a(a)
,_c(c)
{
cout << "调用构造函数" << endl;
}
~A()
{
cout << "调用析构函数" << endl;
}
void Print() const
{
cout << _a << endl;
cout << _c << endl;
}
private:
int _a;
int _c;
};
int main()
{
A* p = new A{ 3,5 };//动态申请一个A类型的空间,并且调用构造函数初始化
p->Print();//打印内容
delete p;//调用析构函数,然后释放空间
return 0;
}
三、operator new函数和operator delete函数
operator new函数和operator delete函数是c++提供的全局函数,当我们使用new或者delete操作符时,它们就会调用这两个函数来实现相关功能。以下是这两个函数的底层实现:
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);
}
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 new函数体当中,首先调用了malloc函数来申请内存空间,如果申请成功则直接返回,否则就会启动相关应对措施;在operator delete函数体中,我们可以看到一个函数“_free_dbg”,它其实就是free的底层实现。
*/
所以说 operator new 使用了 malloc 来开辟内存,operator delete 使用了 free 来释放内存,只不过在其中添加了一些异常处理机制等,使得内存开辟更加完善。
了解了这两个函数的运行机制,我们由此总结出new和delete的实现原理:
四、new和delete的实现原理
1. 内置类型
对于内置类型而言,new/delete与malloc/free基本类似,不同的地方是:
在申请空间时可以初始化,并且new在空间申请失败时会抛出异常,而malloc会返回空指针。
2. 自定义类型
对于自定义类型,它的实现逻辑就比较复杂了,我们逐一分析:
1. new:
首先调用operator new函数申请内存空间,然后调用构造函数,完成初始化
2. delete:
首先调用析构函数,对开辟的内存进行资源清理,然后调用operator delete函数释放内存
3. new[ ]:
首先调用 operator new[ ] 函数申请多个对象的内存空间(该函数中调用了operator new),然后调用N次构造函数,完成初始化
4.delete[ ]:
首先调用N次析构函数清理资源,然后调用 operator delete[ ] 函数释放空间(该函数中调用了operator delete)
五、定位new表达式
我们都知道,当对象被创建的时候,会自动调用构造函数。那么我们能否在一块已有的内存区域上显示调用构造函数构造对象呢?语法上是不允许显示调用构造函数的,但是定位new表达式可以做到:
class A
{
public:
A(int a = 10, int c = 20)
:_a(a)
,_c(c)
{
cout << "调用构造函数" << endl;
}
~A()
{
cout << "调用析构函数" << endl;
}
void Print() const
{
cout << _a << endl;
cout << _c << endl;
}
int _a;
int _c;
};
int main()
{
A* p = (A*)malloc(sizeof(A));//申请内存空间,但不调用构造函数
new(p)A();//使用定位new表达式调用构造函数
p->Print();//打印一下成员
p->~A();//显示调用析构函数
return 0;
}
可以看到,我们成功使用定位new表达式调用了构造函数并且为成员变量设置初始值。定位new表达式在实现内存池或缓存区等高级内存管理策略时非常有用。定位new表达式的语法是:
new(ptr) Class(参数)
这里的ptr表示指向该内存区域的指针,Class是类名。
当构造函数中有非缺省参数时,需要我们在类名之后的括号中传参。
定位new表达式的注意事项:
1. 常规new表达式既负责分配内存,还负责构造对象;而定位new表达式只负责构造对象。所以在使用定位new表达式之前,要确保以及分配好足够的内存。
2. 使用定位new表达式调用构造函数后,如果我们不再使用该对象,要记得主动调用其析构函数并释放内存。
六、malloc/free和new/delete的区别总结
共同点:
都是从堆区申请空间,并且使用结束后要进行手动释放。
不同点:
1. malloc和free是函数,而new和delete是操作符。
2. malloc申请的空间不会进行初始化,而new可以初始化。
3. malloc申请空间时,需要手动计算申请的空间大小;而new申请时只需要说明类型与个数即可。
4. malloc申请空间返回的指针是void* 类型,需要进行强转,而new不需要。
5. malloc申请失败会返回NULL,所以使用时必须检查返回值;而new申请失败会抛出异常,无需检查返回值。
6. 对于自定义类型空间的开辟,malloc和free只会开辟/销毁对应的内存空间,而new和delete会调用构造函数/析构函数完成初始化/资源清理操作。