C++内存管理Part1: new/delete及其重载
C++内存管理Part1: new/delete及其重载
内存分配的每一层面
这门课不会探讨到O.S. API
C++可以分配内存的工具
这是一些内存的基础工具
首先是前两个比较基础的:
1 |
|
然后是::operator new()
,这里提示下::
表示全局函数
1 |
|
看起来和malloc/free
的使用方法很相似,事实上也如此,源码里他也是调用了malloc/free
最后是C++标准库提供的allocator
:
在不同编译器下使用方法略有不同,下面三个不同的编译器下的使用方法如下。
然而一般人是不会利用allocator来拿内存的,因为可以发现你分配后还需要记住当时分配了多大的内存,这是反直觉的。其实准确来说这是给stl用的,stl的容器封装后我们就可以忽略这些细节了。
G2.9的时候还叫alloc,但实际在新版本中alloc
已经没了, 换成了和其他两者一样的名字叫做allocator,用法也和另外两者一样了,如下,现在的GNU其实有多种分配器,s。
基本构件:new/delete expression
new除了上一节的使用方法,new可以搭配构造函数使用:
1 |
|
面向对象那份笔记里有提到,new其实是分三步走的。我们这里讨论的更细致一些:
- 首先是第1步中的operator new,这里没有加上全局的
::
(当全局变量在局部函数中与其中某个变量重名,那么就可以用::
来区分)。这里不加上::
的意义是可以重载这个operator new
,按照你重载的方法来申请内存。 - 第3步的调用方法实际上在许多编译器都是不允许的,在VC6.0中这种古老的编译器是可以的,如果你真的想直接在已有的内存上来调用构造函数,更正确的做法应该是
placement new
。当然这里都是编译器在做的事情,我们无需考虑。
其中的operator new
函数在VC中如下:
- 本质就是malloc申请内存,当申请不到内存,会调用
_callnewh()
这个方法,这个方法是留给我们自己来写的,它用来当malloc申请内存却内存不够时调用,因此这个函数主要是释放一些我们认为可以释放的内存。当释放成功我们的malloc就可以申请到内存从而跳出这个while循环。 - 我们发现这里还有一个第二参数,其实就是保证不抛出异常,这其实现在可以用
noexcept
来代替了。
同理delete本质会转换为:先析构再调用operator delete
所以到这里我们再来看一眼这个图,图左整理了new的调用关系:
这里有一个容易混淆的概念:
我们称new叫做new operator,还有一个operator new,new operator其实是调用了operator new,这两者未免太容易混淆,其实new operator就是一个表达式,而operator new是一个函数/操作符,因此我们有时候也会把new operator叫做new expression。
delete同理。
ctor和dtor是不可以直接调用的,如下图,但可以通过placement new来调用,后面我们提到placement new再细说。
array new/array delete
1 |
|
我们知道如果用了array new,那么也要使用对应的array delete。但往往我们会使用了array new,却忘记使用array delete,这会造成没有对每个对象都执行析构函数dtor。然而这种行为并不总是有影响的:
- 对于没有指针成员的class,依靠cookie依然可以正确的回收掉所有内存(cookie中存储了class的大小个数等信息, 是new内存时的附带产物)。
- 对于指针成员的class,比如string。析构函数中往往写了delete 指针的逻辑,所以此时如果你忘记了使用array delete会导致指针成员指向的内存没有通过析构函数释放掉这篇内存,然而指针成员本身会被通过cookie释放掉占用的内存,因此最后指针指向的内存就泄露了。
所以永远记得array new对应着array delete可以保证你的程序永远不会出现上述所讨论的复杂情况。
placement new
可以翻译为定点的new,即我已经有了内存,给出这个内存的指针来在这篇内存调用构造函数。如下图,我们可以发现:placement new的用法new (tmp++) A(i)
, 即在指针处直接调用构造函数。
placement new
前面我们提到过placement new可以不分配内存,直接在某一内存处处调用构造函数。
他也是一种new expression所以也是经过new的三部的,只不过第一步申请内存时利用的operator new是void* operator new(size_t,void* loc) {return loc;}
这个函数。这个函数并没有申请内存,而是直接返回了内存的地址。其他和new expression并无区别。因此我们可以认为placement new实际上是对operator new的重载。
operator new的重载(1)
我们在前面提到了 ::operator new
代表全局operator new,但如果你想自己改造一下,比如每次new一个元素底层malloc都要产生一个cookie,在某些情况下这可能不是很好的一个选择,因此我们选择可以走自己的operator new的方案,你只需要在class的作用于下重载下operator new即可,如下图的Foo类下我们重载了这个类,那么就不会走全局的那条路,而是走我们自己重载的operator new这条路,因为对于同名函数总是先调用类里的,其次才是全局的:
ok,那么我们知道了new expression的3部流程(开内存,强转,调用构造函数),我们可不可以不用new,直接malloc模仿呢?当然可以毕竟new底层就是malloc,不过这只是做个验证而已,实际中当然用new会方便许多了,况且这个模仿也并不是完美的,毕竟你还用了placement new,然而你不用这个又没法像编译器一样直接调用构造函数:
同理我们可以提前看一下容器中的allocator分配内存的途径:
其实本质和new是没什么去别的,只不过allocator自己封装了一套allocate() + construct()
代替了operator new + placement new
,当然他的最后底层调用的东西肯定是一样的。
为什么要在重载这一节介绍这个呢?因为allocator其实实现了自己的一套内存管理,就像new expression一样。
有几种可以重载的各种new呢?
首先是全局的::operator new
,重载这个会影响很多,虽然可以,但我们很少重载全局的operator new。
我们更多的是在某个类里重载operator new,就像这一节一开始提到的那样,在类里写一个operator new就可以在这个类operator new的时候走自己的一套而不走全局的::operator new
, 下面是一个例子,下面有几点要注意:
- 重载的operator delete有个第二参数,其实可有可无。
- 我们知道加上这两个重载的operator new/delete后,这两个函数应该在没有函数对象也可以被调用到,因此我们需要她是静态函数的。但是为什么下面没用static修饰呢?可以认为这是cpp很体贴帮助你添加上了,因为这个static修饰是必须有的,cpp帮你做了处理。
当然全局的array new/delete和类里的array new/delete和上面也同理,加个[]
即可。
例子:
下图1,2,3,4对应着看,虚函数仅仅只是为了改变class大小而已。经过验证确实如我们上面所讲的内容一致。
调用全局的new/delete:会在下一步自动调用到全局的::operator new/::operator delete
operator new的重载(2)
我们在placement new中提到了,placement new也是一种new expression所以也是经过new的三部的,只不过第一步申请内存时利用的operator new是void* operator new(size_t,void* loc) {return loc;}
这个函数。这个函数并没有申请内存,而是直接返回了内存的地址。其他和new expression并无区别。因此我们可以认为placement new实际上是对operator new的重载。
new expression可以有多个形式,比如placement new就是一种,还有自带new class()这种,我们可以写出一种像placement new一样新的new expression吗?当然可以,就像placement new一样通过重载operator new即可。cpp里对这个概念没有那么准确,这种new我们其实也可以称之为placement new,也就是说这种带括号的我们都可以称为placement new, 当然此时的placement不能再翻译成定点了,翻译成放置也许比较好,因为括号里可以放不同的参数类型。
比如我们希望如下可以成立:
1 |
|
那么下面第四个版本可以实现。
- 首先要注意第一参数必须是
size_t
,不然就会报错,如(5) - (1)中是一般的operator new
- (2)中标准库提供的一种是placement new,不申请内存,直接返回内存的指针。
- (3)(4)都是全新的placement new,自己实现的。
operator new一般来说应该有着对应的operator delete,但是对于operator delete来说:
你自己写的非默认参数的operator delete版本 永远不会被 delete 直接调用的, 他的出现仅仅是为了处理异常: 即当一个new expression出现错误后,比如operator new分配空间后,调用构造函数出错,此时 要处理异常回收内存才会调用到你写的对应的operator delete版本。如果正常永远是调用的是基本的delete类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30#include <stdexcept>
#include <iostream>
struct X
{
X() { throw std::runtime_error(""); }
// custom placement new
static void* operator new(std::size_t sz, bool b)
{
std::cout << "custom placement new called, b = " << b << '\n';
return ::operator new(sz);
}
// custom placement delete
static void operator delete(void* ptr, bool b)
{
std::cout << "custom placement delete called, b = " << b << '\n';
::operator delete(ptr);
}
};
int main()
{
try
{
X* p1 = new (true) X;//custom placement new called, b = 1
}
catch (const std::exception&) {}//custom placement delete called, b = 1
}
不写operator delete也是被允许的,前提是你的
ctor
和operator new
都是noexcept
的。那么是可以不写对应的operator delete的。无论是new还是delete,当你重载了以后,默认给出的版本就失效了,需要你自己写。当然你也可以用
::new/::delete
调用全局的默认版本。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class A
{
public:
void* operator new(size_t size, char) { return malloc(size);}
void operator delete(void* pdead, size_t size, char c) { cout<<"whysb1"<<endl;free(pdead);}
void operator delete(void* pdead, size_t size) { cout<<"whysb2"<<endl;free(pdead);}
A() { }
};
int main() {
A* a = new('c') A();//OK, 调用了void* operator new(size_t size, char)
A* a = new A();//error: no matching function for call to 'A::operator new(sizetype)'
A* a = ::new A();// ok,调用全局的默认版本
//delete永远是调用默认参数版本的operator delete(即operator delete(void* pdead, size_t size))的,是
//不会调用到重载的operator delete版本的。
delete a;//输出whysb2
return 0;
}
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!