目录
3.4. operator new与operator delete函数
2.4.1. operator new与operator delete函数(重点)
2.4.2. 重载operator new和 operator delete
1. C/C++内存分布
C/C++ 的进程地址空间如下:
接下来有一个问题:
int global_val = 1;
static int static_global_val = 1;
void Test()
{
static int static_val = 1;
int local_val = 1;
int num[10] = { 1, 2, 3, 4 };
char char1[] = "abcd";
const char* p_char2 = "abcd";
int* ptr = (int*)malloc(sizeof(int)* 4);
free(ptr);
}
global_val在哪里?____ static_global_val在哪里?____static_val在哪里?____ local_val在哪里?____num 在哪里?____char1在哪里?____ *char1在哪里?___p_char2在哪里?____ *p_char2在哪里?____ptr在哪里?____ *ptr在哪里?____
要解决这个问题,可以用画图来解决;
根据图我们可以很轻松的解决上面的问题:
global_val在哪里? 数据段 static_global_val在哪里? 数据段static_val在哪里? 数据段 local_val在哪里? 栈num 在哪里? 栈char1在哪里? 栈 *char1在哪里? 栈p_char2在哪里? 栈 *p_char2在哪里? 代码段ptr在哪里? 栈 *ptr在哪里? 堆
2. C语言中动态内存管理方式
malloc/calloc/reallloc/free;
3. C++中动态内存管理
在C++里面,C语言的内存管理方式可以继续使用,但有些地方不方便,因此C++提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理;
首先我们要明确一点,new 和 delete 是操作符,不是函数;
3.1. new和delete操作内置类型
new 和 malloc对于内置类型没有本质上区别,都是在堆空间上申请一段资源,带有一些细小区别:
- new 是操作符,malloc 是函数,也就决定了它们的用法稍有差异;
- new 失败会抛异常 (try catch),malloc 失败会返回NULL。
// 申请一个int大小的空间
int *p1_c = (int*)malloc(sizeof(int));
int *p1_cc = new int;
// 申请三个int大小的连续空间
int *p2_c = (int*)malloc(sizeof(int)* 3);
int *p2_cc = new int[3];
//这是申请10个连续int类型的空间, 不初始化(针对内置类型)
int *p1_cc = new int[10];
//这是申请一个int类型的空间,并初始化为10
int *p2_cc = new int(10);
// C++11 支持在{}里面进行初始化, C++98 不支持
int *p_cc = new int[10]{10,int()};
使用new时,释放空间一定要匹配,new 就对应 delete,new[] 对应 delete[];
int *p1_c = (int*)malloc(sizeof(int));
int *p1_cc = new int;
int *p2_c = (int*)malloc(sizeof(int)* 3);
int *p2_cc = new int[3];
free(p1_c);
free(p2_c);
// 使用new时 注意一定要匹配
delete p1_cc; //new 对应 delete
delete[] p2_cc; //new[] 对应 delete[]
3.2. new和delete操作自定义类型
其实对于内置类型来说,malloc和new没有什么太大的区别,但是对于自定义类型来说,就有差别了;
#include <iostream>
namespace Xq
{
class A
{
public:
A()
{
std::cout << " A() " << std::endl;
}
A(const A& copy)
{
std::cout << " A(const A& copy) " << std::endl;
}
~A()
{
std::cout << " ~A() " << std::endl;
}
};
}
int main()
{
// C
std::cout << "=====================================, C: " << std::endl;
Xq::A* p1 = (Xq::A*)malloc(sizeof(Xq::A));
if (nullptr == p1)
{
perror("malloc failed");
return 1;
}
std::cout << "====================================, C++: " << std::endl;
// C++
Xq::A* p2 = new Xq::A;
free(p1);
delete p2;
return 0;
}
结果如下:
通过上面的现象,我们发现一个问题,当我们申请了两个对象,但是只调用了一次构造和一次析构,究竟是 malloc 函数调用了默认构造,还是 new 操作符调用了默认构造呢?
答案显而易见,是 new 操作符调用了默认构造。
对于自定义类型来说, new操作符 会先在堆上申请空间,然后调用类的默认构造函数初始化对象。
同时,我们发现,delete 操作符是先调用析构函数清理对象的资源,然后在释放空间;
如果这个类没有默认构造并且没有主动初始化,会发生什么呢?
#include <iostream>
namespace Xq
{
class A
{
public:
A(int a) { std::cout << " A() " << std::endl; }
};
}
int main()
{
Xq::A* p = new Xq::A;
delete p;
return 0;
}
如果没有默认构造且没有主动初始化,则会编译报错,如下:
如果此时这个类没有默认构造,可以进行显式初始化:
int main()
{
Xq::A* p = new Xq::A(3);
delete p;
return 0;
}
结论:new和delete操作符就是为了自定义类型而准备,不仅会在堆申请和释放空间,还会调用默认构造和析构函数进行初始化对象资源和清理对象资源;
注意:new 和 delete 以及 new[] 和 delete[] 尽量要匹配使用,如果不匹配使用可能带来一些问题。
3.3. malloc和new失败的处理方式不同
malloc 函数申请资源失败,现象如下:
new 操作符申请资源失败,现象如下:
new如果失败了,不会执行后续代码,直接跳到 catch 的处理块中,因此 new 不需要检测返回值;面向对象一般失败都会抛异常,不会以返回值的形式判断失败;
3.4. operator new与operator delete函数
2.4.1. operator new与operator delete函数(重点)
new 和 delete 操作符是用户进行动态资源申请和释放的操作符,operator new 和operator delete 是系统提供的全局函数 (这里不是运算符重载),new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。
new 内置类型:
int main()
{
try
{
int* p = new int(10);
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
转到反汇编,可以看到, new 内置类型,会call operator new 全局函数来进行申请空间,如下:
new 自定义类型:
int main()
{
try
{
Xq::A* aa = new Xq::A(10);
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
转到反汇编,可以看到, new 自定义类型,会先 call operator new 全局函数来进行申请空间,在 call 这个类的构造函数,如下:
我们发现,new 一个自定义类型,会先去call operator new,如果失败则抛异常,如果成功会再去call 这个类合适的构造函数;
可以看到 new 开空间实际上是 operator new 帮助 new 开空间;
那么operator new是什么?
operator new 是一个全局函数,并非对 new 的运算符重载,operator new的底层实现:
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 new实际上是通过malloc来申请空间,当malloc成功就直接返回,申请空间间失败,会抛异常;封装malloc,符合C++new的失败机制,即失败抛异常;
我们可以自己单独使用 operator new 这个全局函数申请空间,如下:
int main()
{
try
{
Xq::A* aa = (Xq::A*)operator new(sizeof(Xq::A));
new(aa)Xq::A(10); // 定位new, 显式调用构造函数
operator delete(aa);
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
事实上,operator new 和 malloc 几乎是一样的,它们的差别主要在于,失败的判断机制不一样了,operator new 失败会抛异常,malloc 失败会返回 NULL,并且,这两者对于自定义类型来说,只会申请空间,而不会调用类的构造函数,最后,operator new 申请的空间通过operator delete 释放,malloc 申请的空间用 free 释放。
那么 operator delete 又是什么呢?
事实上,operator delete 也是一个全局函数,它也是对 free 的封装,operator delete 的实现如下:
// 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 和 operator delete) 的实现我们知道:
- operator new 实际上是通过 malloc 这个全局函数来申请空间的,如果 operator new 中的 malloc 申请失败,则会进行抛异常;
- operator delete 事实上也是通过封装 free 来释放空间的。
2.4.2. 重载operator new和 operator delete
重载一个类专属的 operator new,这里既不是函数重载(因为跟全局的那个不在一个作用域里面),也不是运算符重载,而是属于 operator new 的一种特性;
测试如下:
struct ListNode
{
ListNode(int val)
:_val(val)
, _next(nullptr)
{}
void* operator new(size_t n)
{
std::cout << "operator new -> STL内存池allocator申请" << std::endl;
void* obj = alloc.allocate(1);
return obj;
}
void operator delete(void *ptr)
{
std::cout << "operator delete -> STL内存池allocator申请" << std::endl;
alloc.deallocate((ListNode*)ptr, 1);
}
~ListNode() {}
int _val;
ListNode* _next;
static std::allocator<ListNode> alloc; //STL里面的内存池
};
std::allocator<ListNode> ListNode::alloc;
int main()
{
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
delete node1;
delete node2;
delete node3;
return 0;
}
现象如下:
你可以发现,此时 new 操作符和 delete 操作符调用的 operator new 和 operator delete 竟然是这个类中实现的 operator new 和 operator delete,我们也可以通过汇编查看一下:
new 操作符调用的 operator new 如下:
我们发现,此时 new 操作符调用的 operator new 是这个类中自己实现的operator new,且是先调用这个类的 operator new ,再去调用这个类的构造函数,即先申请空间,在初始化对象的资源。
delete 操作符调用的 operator delete 如下:
我们发现,此时 delete 操作符调用的 operator delete 是这个类中自己实现的operator delete,且是先调用这个类的析构函数 ,再去调用这个类的 operator delete,即先清理对象的资源,在释放空间。
总结:
- 如果某一个自定义类型在类中显式定义了 operator new 函数,那么在申请这个自定义对象空间时,编译器会调用这个类显式定义的 operator new, 而不会去调用库中的全局 operator new 函数;
- 同理,如果某一个自定义类型在类中显式定义了 operator delete 函数,那么在释放这个自定义对象空间时,编译器会调用这个类显式定义的 operator delete, 而不会去调用库中的全局 operator delete 函数;
- 因此,对于自定义类型来说,无论是 new 还是 delete,如果这个类显式实现了 operator new 和 operator delete ,那么在调用 new 和 delete 时,编译器会优先使用这个类自己实现的 operator new 和 operator delete, 上面这种规则,可以理解为就近原则。
5. new 和 delete 的实现原理
5.1. 对于内置类型而言
如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本类似,不同点在于:
- malloc 即可以申请一个对象的空间,也可以申请多个对象的连续空间;
- free 即可以释放一个对象的空间,也可以释放多个对象的连续空间;
- new 申请的是一个对象的空间, delete 释放的也是一个对象的空间;
- new [] 申请的是多个对象的连续空间, delete [] 释放的也是多个对象的连续空间;
- new 失败会抛异常, malloc 失败会返回 NULL。
5.2. 对于自定义类型而言
5.2.1. new的原理
- 调用 operator new 函数申请空间,如果这个自定义类型显式定义了 operator new,则会调用这个类自己实现的 operator new;
- 在申请的空间上调用类的构造函数,完成对象的资源初始化工作;
5.2.2. delete的原理
- 在指定的空间上先执行类的析构函数,完成对象的资源的清理工作;
- 调用 operator delete 函数释放对象的空间,至于调用哪个 operator delete,依据就近原则。
5.2.3. new T[N]的原理
- 调用 operator new[] 函数,在 operator new[] 函数中实际调用 operator new 函数完成N个对象空间的申请;
- 在申请的空间上执行N次构造函数。
5.2.4. delete []的原理
- 在指定的空间上先执行N次析构函数(符合LIFO),完成N个对象中资源的清理;
- 调用 operator delete[] 释放空间,实际在 operator delete[] 中调用N次 operator delete 来释放空间。
6. 定位new表达式(placement-new)
定位new表达式是在已申请的内存空间中显式的调用类的构造函数初始化一个对象。
而一个类实例化的对象要调用构造函数会有三个场景:
- 第一个场景,定义对象后由编译器自动调用;
- 第二个创建,new,因为 new = 调用 operator new 申请空间,然后调用类的构造函数初始化对象的资源;
- 第三个场景,定位new,显式调用类的构造函数。
6.1. 定位new的使用格式
使用定位 new 的方式,如下:
- new(place_address)type;
- new(place_address)type(initializer-list);
- place_address 是一个指针,initializer-list 是类型的初始化列表。
示例如下:
namespace Xq
{
class A
{
public:
A(int a = 0) :_a(a) { std::cout << " A(int a = 0) " << std::endl; }
private:
int _a;
};
}
int main()
{
// 开辟空间
Xq::A* p = (Xq::A*)malloc(sizeof(Xq::A*));
//定位new的两种方式,显式调用构造函数
//a.前提需要默认构造
new(p)Xq::A; // new(place_address)type
//b.显式初始化
//new(p)Xq::A(1); //new (place_address) type(initializer-list)
return 0;
}
定位new的应用场景:定位new表达式在实际中一般是配合内存池使用。
有些场景我们需要提高效率会在内存池开空间,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调用构造函数进行初始化对象资源。
7. 常见问题
malloc/free和new/delete的区别,从语法、用法、底层实现的角度分析对比:
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
- malloc和free是函数,new和delete是操作符;
- malloc申请的空间不会初始化,new可以初始化 (本质上是通过构造函数进行初始化的);
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可;
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型;
- malloc申请空间失败时,返回的是NULL,需要通过malloc的返回值检测,new 失败了会抛异常,需要在有相应的catch处理异常;
- 申请自定义类型对象时,malloc/free只会开辟和释放空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象资源的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理;
- 对于自定义类型来说,new = operator new + constructor,delete = destructor + operator delete,而 operator new 其实是对 malloc 的封装,operator delete 是对 free 的封装 (在这里说的是全局的operator new 和 operator delete),只不过operator new 失败了是抛异常(try - catch),而malloc失败了是返回NULL。