前言
在C语言中,动态管理内存的方式是使用各种内存管理函数,如:malloc/calloc/realloc/free
等,这篇文章我们了解C++中如何动态管理内存。
1. C/C++内存分布
首先认识一下C/C++中内存是如何分布的:
栈:又叫堆栈,存放非静态局部变量、函数参数、返回值等等。栈向下增长。
内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可使用系统接口创建共享内存,做进程间通信。
堆:用于程序运行时的动态内存分配。堆向上增长。
数据段:又叫静态区,存储全局变量和静态数据。
代码段:又叫常量区,存储可执行代码、只读常量。
接下来通过一段代码来进一步加深对这些分区的认识:
int globalVar = 1; // 全局变量
static int staticGlobalVar = 1; // 静态变量(全局)
void Test()
{
static int staticVar = 1; // 静态变量(局部)
int localvar = 1; // 局部变量
int num[10] = { 1 }; // 局部变量
char ch1[] = "abcd"; // 局部变量 - "abcd"是只读常量,程序创建一个临时变量,将"abcd"赋值给临时变量,然后临时变量赋值给ch1后销毁。
const char* pCh2 = "abcd"; // 局部变量 - "abcd"是只读常量,pCh2存的是这个只读常量的地址,但是它是指针,它本身是个局部变量。
int* ptr1 = (int*)malloc(sizeof(int) * 4); // 局部变量 - malloc创建的空间是动态开辟的内存,存在堆中
int* ptr2 = (int*)calloc(4, sizeof(int)); // 局部变量 - calloc创建的空间是动态开辟的内存,存在堆中
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // 局部变量 - realloc创建的空间是动态开辟的内存,存在堆中
free(ptr1); // 释放的是堆中的动态空间
free(ptr3); // 释放的是堆中的动态空间
}
在图中展示如下:
注意:
ch1 // 栈区 *ch1 // 栈区 "abcd" // 代码段(常量区) pCh2 // 栈区 *pCh2 // 代码段(常量区) ptr1 // 栈区 *ptr1 // 堆区 sizeof(num1) = 40 sizeof(ch1) = 5 strlen(ch1) = 4 sizeof(pCh2) = 4/8 strlen(pCh2) = 4 sizeof(ptr1) = 4/8
2. C语言中动态内存管理方式
主要是四个函数:malloc、calloc、realloc、free。前面的篇章中已经介绍过,这里就不多赘述。
3. C++中动态内存管理方式
C语言的那一套在C++中还是可以使用的,但是在某些场景下使用起来就较为麻烦,因此C++提出了自己的内存管理方式。
这涉及到两个操作符:new
和delete
。
3.1 new/delete 操作内置类型
// c
int* p1 = (int)malloc(sizeof(int));
int* p2 = (int)malloc(sizeof(int) * 4);
free(p1);
free(p2);
// c++
int* p3 = new int;
int* p4 = new int[4];
int* p5 = new int(10); // 一个整型初始化
int* p6 = new int[10]{1,2,3}; // 数组初始化
delete p3;
delete[] p4;
delete p5;
delete[] p6;
对于内置类型而言,new/delete和malloc/free的区别是:用法不同;new/delete多了个初始化的功能。
3.2 new/delete操作自定义类型
-
对于自定义类型而言,new/delete和malloc/free的最大区别是:new/delete除了开辟空间外,还会调用构造函数和析构函数。
class ListNode { public: ListNode(int data = 0) : _data(data) , _next(nullptr) { cout << "ListNode(int data = 0)" << endl; } ~ListNode() { _data = 0; _next = nullptr; cout << "~ListNode()" << endl; } private: int _data; ListNode* _next; }; int main() { ListNode* n1 = new ListNode[3]{1,2,3}; // new自动调用构造函数 delete[] n1; // delete自动调用析构函数 return 0; }
-
new的时候,貌似只能传一个参数进去初始化,实则不然,请看下面的这种方法:
class A { public: // 构造 A(int a, int b) :_a(a) , _b(b) { cout << "A(int a, int b)" << endl; } // 拷贝构造 A(const A& aa) : _a(aa._a) , _b(aa._b) { cout << "A(const A& aa)" << endl; } // 析构 ~A() { cout << "~A()" << endl; } private: int _a; int _b; }; int main() { A* p6 = new A[3]{A(1,1),A(2,2),A(3,3)}; // 如果没有默认构造,那么new几个,就要传几个参数进去 delete[] p6; return 0; } // 结果显示:编译器只调用了构造函数,并没有调用拷贝构造, // 原本应该是先构造{}内的三个匿名对象,然后分别拷贝构造给new的三个对象, // 过程应该是3个构造+3个拷贝构造+3个析构 // 结果时3个构造+3个析构 // 这是编译器的优化 // 另外:对于对象内有多个需要初始化的变量,可以用这种方式:A* p6 = new A[3]{A(1,1),A(2,2),A(3,3)};
-
注意:尽管C/C++可以混合使用,但是malloc与free、new与delete一定要配对,否则可能出现问题!
如果随意匹配,在不同的编译器下的结果可能是不同的,随意配对的后果是未知的,因此我们一定要配对使用,以防出现未知错误!
4. operator new与 operator delete函数
4.1 用法
这两个是库里的全局函数。
用法和malloc和free是一样的,但是我们一般不会去直接用它们。
class A
{
private:
int _a;
};
int main()
{
A* p1 = (A*)operator new(sizeof(A));
A* p2 = (A*)malloc(sizeof(A));
operator delete(p1);
free(p2);
}
4.2 new/delete、oprerator new/operator delete、malloc/free的关系
对于C语言而言,当某个操作执行失败了,习惯返回一个数值,-1、0等等;
但是在面向对象语言中,某个操作执行失败,习惯抛出异常。
// 获取异常
try
{
...
}
catch (const exception& e)
{
cout << e.what() << endl;
}
我们看看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);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
不难看出,operator new其实是对malloc函数的封装,那么这三者的关系就很明确了:
stack类,其构造函数也会开空间。也就是存在三段空间:
Stack* p1 = new Stack; // p1在栈中的局部变量
// 因为用了new,调用operator new,会在堆中开辟空间,将对象放入堆中。
// 然后调用构造函数,Stack的构造函数里又要开辟一个数组的空间,在堆中。
5. new和delete的实现原理
-
对于内置类型:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:
new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。 -
对于自定义类型:
- new
- 调用operator new函数开辟空间
- 在开辟的空间上执行构造函数
- delete
- 在开辟的空间上执行析构函数
- 调用operator delete函数释放空间
- new T[N]
- 调用operator new[]函数,在该函数中调用operator new函数,完成N个对象的空间的申请
- 在申请的空间上执行N次构造函数
- delete[]
- 在申请的空间上执行N次析构函数
- 调用operator delete[]函数,在该函数中调用operator delete函数,释放N个对象的空间
6. 定位new表达式
6.1 概念
定位new表达式是在已分配的原始内存空间中调用构造函数来初始化一个对象。(相当于自定义类型malloc开辟了空间,但是没有调用构造函数,就可以使用定位new)
6.2 使用格式
new(指针)类型
或者 new(指针)类型(初始化列表)
class A
{
public:
A(int a = 1)
: _a(a)
{ }
~A()
{}
private:
int _a;
};
int main()
{
A* p1 = (A*)malloc(sizeof(A)); // 分配内存
new(p1)A(2); // 显示调用构造
p1->~A(); // 显示调用析构
free(p1); // 释放空间
return 0;