C/C++内存分配
在一个程序的进程地址空间中,其内存分配如下:
栈用来存储非静态局部变量、函数参数/返回值等,栈是向下增长的。栈可以通过_alloca
进行动态内存分配,但是所分配的内存不能通过 free/delete 释放;堆用于程序的动态内存分配,堆是向上增长的;数据段用来存储全局数据和静态数据;代码段用来存储可执行指令,只读常量,字符串常量就存储在代码段中。数据段和代码段在语言层面又分别称为静态区和常量区。
C的动态内存管理
C语言中用来管理动态内存的函数有:malloc、calloc、realloc和free。malloc 用来申请一块指定大小的堆内存空间;calloc 用来申请一块指定大小的堆内存空间,并将其初始化为 0;realloc 用来扩容动态空间;free 用来释放动态内存空间。关于更详细的C语言内存管理,请参考《C语言动态内存管理》。
C++的内存管理
C 语言中的内存管理方式在 C++ 中可以继续使用,但是在有些地方会造成不可避免的麻烦,甚至无法满足需求。因此 C++ 提出了一套自己的内存管理方式。
new和delete关键字
new
用来申请一块动态内存空间,使用时不需要指定内存空间大小,只需要用类型进行标识。delete
用来释放动态内存空间。当申请和释放连续的内存空间时,需要使用new[]
和 delete[]
。new的使用支持初始化。
class A
{
private:
int _i;
public:
A(int i = 10)
:_i(i)
{
cout << "A(int i)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
//内置类型
int* pI1 = new int;
delete pI1;
double* pD1 = new double(3.14);
delete pD1;
//自定义类型
int* pArr1 = new int[10];
delete[] pArr1;
int* pArr2 = new int[10] { 1, 2, 3 };
delete[] pArr2;
//类
A* pA1 = new A(20);
delete pA1;
A* pA2 = (A*)malloc(sizeof(A)); //为了方便,此处不再进行检查
free(pA2)
cout << "test" << endl;
return 0;
}
运行上面的程序,注意到 new 和 delete 在为类对象开辟内存空间时,会调用构造函数和析构函数,而malloc和free不会。这一点是 new/delete 和 malloc/free 之间最大的区别。
new和delete的实现原理
operator new和operator delete
在 C 语言中,若一个函数处理失败,往往是通过返回值进行反馈的,例如malloc 开辟空间失败会返回 NULL;而在 C++ 等面向对象的语言中,任务处理失败一般不用返回值,而是抛出异常。
C++ 是在C语言的基础上发展而来的,对于开辟和释放内存空间的工作,C++完全可以使用malloc和free完成,但是为了使malloc和free的使用更符合C++的特性和需求,需要对二者进行封装,operator new
和 operator delete
就此而生。
operator new 和 operator delete 是系统提供的全局函数,operator new 通过 malloc 进行空间申请,如果申请成功则直接返回,如果最终无法申请成功,则抛出异常;operator delete 最终通过free释放空间。二者的参考源码如下:
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
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;
}
其中 _free_dbg 的定义为:
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
在V.S.2022编译器下,operator new 和 operator delete 的源码如下:
_CRT_SECURITYCRITICAL_ATTRIBUTE
void* __CRTDECL operator new(size_t const size)
{
for (;;)
{
if (void* const block = malloc(size)) {
return block;
}
if (_callnewh(size) == 0)
{
if (size == SIZE_MAX) {
__scrt_throw_std_bad_array_new_length();
}
else {
__scrt_throw_std_bad_alloc();
}
}
// The new handler was successful; try to allocate again...
}
}
_CRT_SECURITYCRITICAL_ATTRIBUTE
void __CRTDECL operator delete(void* const block) noexcept
{
#ifdef _DEBUG
_free_dbg(block, _UNKNOWN_BLOCK);
#else
free(block);
#endif
}
new和delete
通过上述分析,并观察程序运行的汇编代码,此时已经可以窥探 new 和 delete 的原理:new 和 delete 是用户进行动态内存开辟和释放的操作符,new通过调用 operator new 函数进行空间申请,申请成功后,调用类对象的构造函数进行对象初始化(若是对象),申请失败则抛出异常;delete 首先调用类对象的析构函数,释放对象所开辟的内存空间,然后通过 operator delete 进行对象本身占用的空间的释放。
new/delete[] 和 new[]/delete不配对使用的问题
在实际管理资源的过程中,正确配对和使用new/new[]和delete/delete[]是一个最基本的要求,讨论如题的问题,目的是讨论new/delete关键字针对自定义类型时的底层行为。
对于内置类型,可以认为new与new[]、delete与delete[]的行为是一样的,在开辟内存空间时,new/new[]用malloc()开辟一块内存空间,并返回这块内存空间的起始地址;delete/delete[]调用free(),free()拿到内存空间的起始地址后,底层会自动判断这块内存的范围,并进行整块释放。
对于自定义类型,new[]/delete[]的行为有所不同。new[]调用malloc()时,实际多开辟了一个指针大小的空间,而new返回的是跳过这个指针大小的起始空间的地址。与new[]相配,delete[]在释放内存空间时,会在接收到的地址基础上,向前走一个指针空间大小的距离,并以这个地址调用free()进行整块空间的释放。同时,由于delete[]要首先调用析构函数,具体在这块空间中调用析构函数的次数,是由多余的一个指针大小空间中存储的数据决定了,即,new[]调用malloc()开辟的多余的空间记录了这块空间中自定义对象的个数。
使用new[]开辟,而使用delete释放自定义类型的空间时,delete不会向前对地址进行修正,而是以接收到的地址直接调用free(),此时会造成内存局部释放问题,使程序崩溃。同理,使用new开辟,而使用delete[]释放自定义类型的空间时,delete[]会错误地进行向前修正,释放一块未申请的内存空间,导致程序崩溃。
struct MyStruct
{
MyStruct()
{
cout << "MyStruct()" << endl;
}
~MyStruct()
{
cout << "~MyStruct()" << endl;
}
int _i;
char _c;
};
void test()
{
MyStruct* pa = new MyStruct[10];
//32位机器下,查看多余空间中的值
cout << *((int*)pa - 1) << endl;
delete[] pa;
}
需要注意的是,上述new[]/delete[]对内存空间的多余开辟和指针修正是针对自定义类型的,对于内置类型没有所述差异。
placement-new
定位new 即 placement-new,是 new 操作符的一种用法,用来在一块已申请空间的基础上,调用类对象的构造函数初始化一个或多个对象。
class A
{
private:
int _i;
public:
A(int i = 10)
:_i(i)
{
cout << "A(int i)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A* pA = (A*)operator new(sizeof(A) * 10); //申请空间
new(pA)A; //显式调用构造函数初始化一个对象
new(pA + 1)A[2]{ A(1), A(2) }; //显式调用构造函数初始化多个对象
pA->~A(); //显式析构
(pA + 1)->~A();
(pA + 2)->~A();
operator delete(pA); //释放空间
return 0;
}
需要注意的是,使用 placement-new 初始化出一个对象后,编译器不自动调用析构函数,用户必须自行显式调用。
placement-new在池化技术中应用广泛。在 STL 的空间配置器中,为了避免频繁直接向堆区申请内存,提高效率,往往使用placement-new从内存池中构造对象。
template <class T1, class T2>
inline void _construct(T1* p, const T2& value) {
new(p) T1(value); // placement new. invoke ctor of T1
}
malloc/free和new/delete
malloc/free 和 new/delete 都是在堆上申请一块内存空间,并且都需要用户手动释放。不同在于下面几点:
- malloc/free是函数,new/delete是操作符;
- malloc的返回类型是
void*
,使用时要进行类型转换,而 new 不需要; - 申请自定义类型空间时,malloc/free只会申请和释放空间,而new会在申请内存空间后调用构造函数完成初始化,delete会先调用析构函数清理对象资源,最后完成空间释放。
- malloc申请空间失败时,返回的是 NULL,需要进行指针检查,而new不需要,但是new的使用需要捕获异常;
- malloc/free使用时不支持初始化,new/delete支持初始化;
- malloc申请空间时需要明确要申请空间的大小,而new不需要,因为new后面跟的是类型;
其中前 4 点可以大致认为是malloc/free和new/delete之间特性和原理方面的区别,而最后 2 点是使用方面的区别。
内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
长期运行的程序,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
内存泄漏是个复杂的问题,这里只做简单介绍,后续会对内存泄漏的避免和检测方法做一详细总结。