C++内存管理Part1: new/delete及其重载

C++内存管理Part1: new/delete及其重载

内存分配的每一层面

这门课不会探讨到O.S. API

image-20220906172739707

C++可以分配内存的工具

这是一些内存的基础工具

image-20220906172946109

首先是前两个比较基础的:

1
2
3
4
5
6
7
//CRT,C Runtime Library中的malloc/free
void* p1 = malloc(512);
free(p1);

//new
complex<int>* p2 = new complex<int>;
delete p2;

然后是::operator new(),这里提示下::表示全局函数

1
2
void* p3 = ::operator new(512);
::operator delete(p3);

看起来和malloc/free的使用方法很相似,事实上也如此,源码里他也是调用了malloc/free

最后是C++标准库提供的allocator

在不同编译器下使用方法略有不同,下面三个不同的编译器下的使用方法如下。

image-20220906174728663

然而一般人是不会利用allocator来拿内存的,因为可以发现你分配后还需要记住当时分配了多大的内存,这是反直觉的。其实准确来说这是给stl用的,stl的容器封装后我们就可以忽略这些细节了。

G2.9的时候还叫alloc,但实际在新版本中alloc已经没了, 换成了和其他两者一样的名字叫做allocator,用法也和另外两者一样了,如下,现在的GNU其实有多种分配器,s。

image-20220906175442703

基本构件:new/delete expression

new除了上一节的使用方法,new可以搭配构造函数使用:

1
Complex* pc = new Complex(1,2);

面向对象那份笔记里有提到,new其实是分三步走的。我们这里讨论的更细致一些:

  • 首先是第1步中的operator new,这里没有加上全局的::(当全局变量在局部函数中与其中某个变量重名,那么就可以用::来区分)。这里不加上::的意义是可以重载这个operator new,按照你重载的方法来申请内存。
  • 第3步的调用方法实际上在许多编译器都是不允许的,在VC6.0中这种古老的编译器是可以的,如果你真的想直接在已有的内存上来调用构造函数,更正确的做法应该是placement new。当然这里都是编译器在做的事情,我们无需考虑。

image-20220906192659184

其中的operator new函数在VC中如下:

  • 本质就是malloc申请内存,当申请不到内存,会调用_callnewh()这个方法,这个方法是留给我们自己来写的,它用来当malloc申请内存却内存不够时调用,因此这个函数主要是释放一些我们认为可以释放的内存。当释放成功我们的malloc就可以申请到内存从而跳出这个while循环。
  • 我们发现这里还有一个第二参数,其实就是保证不抛出异常,这其实现在可以用noexcept来代替了。

image-20220906204334985

同理delete本质会转换为:先析构再调用operator delete

image-20220906205327563

所以到这里我们再来看一眼这个图,图左整理了new的调用关系:

image-20220906205549692

这里有一个容易混淆的概念:

我们称new叫做new operator,还有一个operator new,new operator其实是调用了operator new,这两者未免太容易混淆,其实new operator就是一个表达式,而operator new是一个函数/操作符,因此我们有时候也会把new operator叫做new expression。

image-20220906205720597

delete同理。

ctor和dtor是不可以直接调用的,如下图,但可以通过placement new来调用,后面我们提到placement new再细说。

image-20220906220344006

array new/array delete

1
2
Complex* pca = new Complex[3];
delete[] pca;

我们知道如果用了array new,那么也要使用对应的array delete。但往往我们会使用了array new,却忘记使用array delete,这会造成没有对每个对象都执行析构函数dtor。然而这种行为并不总是有影响的:

  • 对于没有指针成员的class,依靠cookie依然可以正确的回收掉所有内存(cookie中存储了class的大小个数等信息, 是new内存时的附带产物)。
  • 对于指针成员的class,比如string。析构函数中往往写了delete 指针的逻辑,所以此时如果你忘记了使用array delete会导致指针成员指向的内存没有通过析构函数释放掉这篇内存,然而指针成员本身会被通过cookie释放掉占用的内存,因此最后指针指向的内存就泄露了。

所以永远记得array new对应着array delete可以保证你的程序永远不会出现上述所讨论的复杂情况。

image-20220906230507396

placement new可以翻译为定点的new,即我已经有了内存,给出这个内存的指针来在这篇内存调用构造函数。如下图,我们可以发现:placement new的用法new (tmp++) A(i), 即在指针处直接调用构造函数。

image-20220906233251293

placement new

前面我们提到过placement new可以不分配内存,直接在某一内存处处调用构造函数。

他也是一种new expression所以也是经过new的三部的,只不过第一步申请内存时利用的operator new是void* operator new(size_t,void* loc) {return loc;}这个函数。这个函数并没有申请内存,而是直接返回了内存的地址。其他和new expression并无区别。因此我们可以认为placement new实际上是对operator new的重载。

image-20220906235254602

operator new的重载(1)

我们在前面提到了 ::operator new代表全局operator new,但如果你想自己改造一下,比如每次new一个元素底层malloc都要产生一个cookie,在某些情况下这可能不是很好的一个选择,因此我们选择可以走自己的operator new的方案,你只需要在class的作用于下重载下operator new即可,如下图的Foo类下我们重载了这个类,那么就不会走全局的那条路,而是走我们自己重载的operator new这条路,因为对于同名函数总是先调用类里的,其次才是全局的:

image-20220907000726513

ok,那么我们知道了new expression的3部流程(开内存,强转,调用构造函数),我们可不可以不用new,直接malloc模仿呢?当然可以毕竟new底层就是malloc,不过这只是做个验证而已,实际中当然用new会方便许多了,况且这个模仿也并不是完美的,毕竟你还用了placement new,然而你不用这个又没法像编译器一样直接调用构造函数:

image-20220907001428577

同理我们可以提前看一下容器中的allocator分配内存的途径:

image-20220907002308274

其实本质和new是没什么去别的,只不过allocator自己封装了一套allocate() + construct()代替了operator new + placement new,当然他的最后底层调用的东西肯定是一样的。

image-20220907002543646

为什么要在重载这一节介绍这个呢?因为allocator其实实现了自己的一套内存管理,就像new expression一样。

有几种可以重载的各种new呢?

首先是全局的::operator new,重载这个会影响很多,虽然可以,但我们很少重载全局的operator new。

image-20220907003741283

我们更多的是在某个类里重载operator new,就像这一节一开始提到的那样,在类里写一个operator new就可以在这个类operator new的时候走自己的一套而不走全局的::operator new, 下面是一个例子,下面有几点要注意:

  • 重载的operator delete有个第二参数,其实可有可无。
  • 我们知道加上这两个重载的operator new/delete后,这两个函数应该在没有函数对象也可以被调用到,因此我们需要她是静态函数的。但是为什么下面没用static修饰呢?可以认为这是cpp很体贴帮助你添加上了,因为这个static修饰是必须有的,cpp帮你做了处理。

image-20220907004557421

当然全局的array new/delete和类里的array new/delete和上面也同理,加个[]即可。

例子:

image-20220910140836353

下图1,2,3,4对应着看,虚函数仅仅只是为了改变class大小而已。经过验证确实如我们上面所讲的内容一致。

image-20220910140914771

调用全局的new/delete:会在下一步自动调用到全局的::operator new/::operator delete

image-20220910141133249

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的重载。

image-20220906235254602

new expression可以有多个形式,比如placement new就是一种,还有自带new class()这种,我们可以写出一种像placement new一样新的new expression吗?当然可以,就像placement new一样通过重载operator new即可。cpp里对这个概念没有那么准确,这种new我们其实也可以称之为placement new,也就是说这种带括号的我们都可以称为placement new, 当然此时的placement不能再翻译成定点了,翻译成放置也许比较好,因为括号里可以放不同的参数类型。

比如我们希望如下可以成立:

1
Foo* pf = new(300,'c') Foo;

那么下面第四个版本可以实现。

image-20220910144057367

  • 首先要注意第一参数必须是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也是被允许的,前提是你的 ctoroperator 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
    23
    class 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;
    }

image-20220910173909879


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!