C++内存分配方法new与placement new使用方法详解

 
Category: C_C++

写在前面

总结一下C++内存分配中的new/delete方法, 以及一个很有意思的工具: placement new.

参考:

  1. cppprimer5ed, pp409, pp726(19.1).
  2. 侯捷C++ video

new的基本使用

编译器角度

在使用new分配内存的时候, 例如下面这样:

string *sp = new string("abc"); // 分配并初始化一个string对象
string *sa = new string[10];    // 分配10个默认初始化的string对象

上面的new内存分配, 本质上进行了三个步骤:

  1. new表达式调用一个名为operator new(或operator new[])的标准库函数, 这个函数分配了一块足够大的/原始的/未命名的内存空间, 以便存储特定类型的对象(或对象构成的数组);
  2. 编译器运行相应的构造函数以构造这些对象, 传入初始值;
  3. 对象被分配空间, 并且构造完成, 返回指向该对象的指针.

对于delete, 同理:(两个步骤)

delete sp;   // 销毁*sp, 释放sp所指向的内存空间
delete[] sa; // 销毁数组中的元素, 然后释放对应的内存空间

步骤:

  1. sp所指向的对象或者arr所指的数组中的元素执行相应的析构函数;
  2. 编译器调用operator delete(或operator delete[])释放内存.

operator new/operator delete调用规则

当重载了全局的operator new和operator delete之后, 内存分配就不是系统默认的了, 所以这两个函数一定要保证正确.

分配内存/析构内存时, 编译器首先在被分配内存类(及其基类)的作用域中查找, 是否有定义operator new和operator delete成员函数. 若未找到, 则在全局作用域内查找, 最后, 会调用标准库定义的版本.

其中, 若在定义了/重载了operator new和operator delete之后, 还想使用全局的operator new和operator delete, 那么应该使用作用域运算符, 即:

::new
::delete

标准库定义的版本

书上写错了, operator delete返回值类型应该是void而不是void*.

下面的版本可能会抛出std::bad_alloc异常:

void *operator new(size_t);   // 分配一个对象
void *operator new[](size_t); // 分配一个数组
void operator delete(void*) noexcept;   // 释放一个对象
void operator delete[](void*) noexcept; // 释放一个数组

下面的版本承诺不会抛出异常:

#include <new> // struct nothrow_t, nothrow
void *operator new(size_t, nothrow_t&) noexcept;    // 分配一个对象
void *operator new[](size_t, nothrow_t&) noexcept;  // 分配一个数组
void operator delete(void*, nothrow_t&) noexcept;   // 释放一个对象
void operator delete[](void*, nothrow_t&) noexcept; // 释放一个数组

注意:

  1. 重载上述运算符函数时, 必须使用noexcept异常说明符指定其不抛出异常.

  2. 应用程序可以自定义上述函数中任意一个, 前提是自定义的版本必须位于全局作用域或者类作用域中.

  3. 将上述运算符函数定义成类的成员函数时, 其默认就是隐式静态的(implicit, static), 这是因为operator new用在对象构造之前, 而operator delete用在对象销毁之后, 所以这两个成员必须静态, 并且不能操纵类的任何数据成员.

  4. 在类中定义时, 无需显式声明static.(声明了也不会报错)

  5. $\bigstar$对operator newoperator new[]来说, 其返回值类型必须是void*, 第一个形参必须size_t且该形参不能含有默认实参.

  6. 当编译器调用operator new时, 把存储指定类型对象所需的字节数传给size_t形参; 当调用operator new[]时, 传入函数的是存储数组中所有元素所需的空间.

  7. 如果要自定义operator new, 可以为其提供额外形参, 这就用到了后面会提到的placement new, 将实参传给新增的形参.

  8. 下面这个全局函数不能被重载: (在类中作为成员函数可以被重载)
    void *operator new(size_t, void*);
    

    这个函数只能供标准库使用.

    下面是一个测试:

    // error: redefinition of 'void* operator new(size_t, void*)'
    void* operator new(size_t size, void* start) {
        cout << "operator new(size_t size, void* start), size=" << size
             << ", start=" << start << endl;
        return start;
    }
    

重载operator new/operator delete

#include <iostream>
using namespace std;

class Foo {
public:
    int _id;
    long _data;
    string _str;

public:
    Foo() : _id(0) {
        cout << "default ctor.this=" << this << " id=" << _id << endl;
    }
    Foo(int i) : _id(i) {
        cout << "ctor.this=" << this << " id=" << _id << endl;
    }

    // virtual or not
    // ~Foo() { cout << "dtor.this=" << this << " id=" << _id << endl; }
    virtual ~Foo() { cout << "dtor.this=" << this << " id=" << _id << endl; }

    static void* operator new(size_t size);
    static void operator delete(void* pdead, size_t size);
    static void* operator new[](size_t size);
    static void operator delete[](void* pdead, size_t size);
};

void* Foo::operator new(size_t size) {
    Foo* p = (Foo*)malloc(size);
    cout << "Foo::new, size=" << size << endl;
    return p;
}

void Foo::operator delete(void* pdead, size_t size) {
    cout << "Foo::delete" << endl;
    free(pdead);
}

void* Foo::operator new[](size_t size) {
    Foo* p = (Foo*)malloc(size);
    cout << "Foo::new[], size=" << size << endl;
    return p;
}

void Foo::operator delete[](void* pdead, size_t size) {
    cout << "Foo::delete[]" << endl;
    free(pdead);
}

接下来是测试函数:

  1. 调用全局的new/delete:
    void t1() {
        Foo* pf = new Foo;
        delete pf;
        /*
        Foo::new, size=48
        default ctor.this=0x600003388270 id=0
        dtor.this=0x600003388270 id=0
        Foo::delete, size=48
        */
         
        Foo* pf1 = ::new Foo;
        ::delete pf1;
        // if not overload new and delete: call system new and delete
        /*
        default ctor.this=0x600003388270 id=0
        dtor.this=0x600003388270 id=0
        */
    }
    
  2. 调用自定义的new/delete:
    void t2() { // 验证内存大小
        cout << "sizeof(int)=" << sizeof(int) << endl;
        cout << "sizeof(long)=" << sizeof(long) << endl;
        cout << "sizeof(string)=" << sizeof(string) << endl;
        cout << "sizeof(Foo)=" << sizeof(Foo) << endl;
        /*clang++
        sizeof(int)=4
        sizeof(long)=8
        sizeof(string)=24
        sizeof(Foo)=40
        */
        /*g++
        sizeof(int)=4
        sizeof(long)=8
        sizeof(string)=32
        sizeof(Foo)=48
        */
    }
    // 然后是内存分配测试:
    void t3() {
        cout << "sizeof(Foo)=" << sizeof(Foo) << endl; // g++: 48
        Foo* p = new Foo(7);
        cout << "sizeof(new Foo)=" << sizeof(*p) << endl;
        delete p;
         
        Foo* pArr = new Foo[5];
        cout << "sizeof(new Foo[5])=" << sizeof(*pArr) << endl;
        delete[] pArr;
        /*
        sizeof(Foo)=48
        Foo::new, size=48
        ctor.this=0x600000f18000 id=7
        sizeof(new Foo)=48
        dtor.this=0x600000f18000 id=7
        Foo::delete
        Foo::new[], size=248 
        default ctor.this=0x600003d1c008 id=0
        default ctor.this=0x600003d1c038 id=0
        default ctor.this=0x600003d1c068 id=0
        default ctor.this=0x600003d1c098 id=0
        default ctor.this=0x600003d1c0c8 id=0
        sizeof(new Foo[5])=48
        dtor.this=0x600003d1c0c8 id=0
        dtor.this=0x600003d1c098 id=0
        dtor.this=0x600003d1c068 id=0
        dtor.this=0x600003d1c038 id=0
        dtor.this=0x600003d1c008 id=0
        Foo::delete[]
        */
    }
    

    这里要注意, 为什么对象数组的大小不是48*5=240, 反而还多了一个8呢? 248=48*5+8

    因为8就是size_t也就是unsigned long在64位机器下的大小, 保存了对象数组长度.

    下面是分别在64位机器和32位(使用g++ -m32选项, 仅支持x86_64)机器下测试的情况:

      // 64bit:
      cout << sizeof(size_t) << endl;               // 8
      cout << sizeof(long) << endl;                 // 8
      cout << sizeof(unsigned long) << endl;        // 8
      cout << sizeof(unsigned) << endl;             // 4
      cout << typeid(unsigned).name() << endl;      // j
      cout << typeid(unsigned int).name() << endl;  // j
      cout << typeid(unsigned long).name() << endl; // m
      cout << typeid(size_t).name() << endl;        // m
      // 32bit:
      4
      4
      4
      4
      j
      j
      m
      j
    
  3. 类中含有虚析构函数的情况:
    // 析构函数为:
    virtual ~Foo() {cout << "dtor.this=" << this << " id=" << _id << endl;}
    

    此时t3()函数执行情况如下:

        sizeof(Foo)=56
        Foo::new, size=56 // has a vptr: 48+8
        ctor.this=0x6000039701c0 id=7
        sizeof(new Foo)=56
        dtor.this=0x6000039701c0 id=7
        Foo::delete
        Foo::new[], size=288 // 288=56*5+8
        default ctor.this=0x139e04588 id=0
        default ctor.this=0x139e045c0 id=0
        default ctor.this=0x139e045f8 id=0
        default ctor.this=0x139e04630 id=0
        default ctor.this=0x139e04668 id=0
        sizeof(new Foo[5])=56
        dtor.this=0x139e04668 id=0
        dtor.this=0x139e04630 id=0
        dtor.this=0x139e045f8 id=0
        dtor.this=0x139e045c0 id=0
        dtor.this=0x139e04588 id=0
        Foo::delete[]
    

一个小坑

来说一个奇怪的情况:

首先看下面的例子:(算是上面例子的一个简化)

#include <iostream>
using namespace std;

class P {
public:
    int a; // 4bytes
    void* operator new(size_t size) {
        P* p = (P*)malloc(size);
        cout << "P::new, size=" << size << endl;
        return p;
    }
    void* operator new[](size_t size) {
        P* p = (P*)malloc(size);
        cout << "P::new[], size=" << size << endl;
        return p;
    }
    // virtual
    // ~P() {}
};

这里面为了省事我直接把成员函数定义也放在类中了.

然后是测试函数:

void t1() {
    P* p = new P;
    // 4 (无析构函数)
    // 4 (无虚析构, 仅有析构)
    // 16 (虚析构)
    delete p;
    P* p1 = new P[5]();
    // 20 (无析构函数) 因为没设置析构函数,
    // 这时候就不会记录之后delete时候要释放的大小, 所以没有size_t信息,
    // 也就只有4*5=20大小 28 (无虚析构, 仅有析构) 88 (虚析构)
    delete[] p1;
}
int main(int argc, char const* argv[]) {
    t1();
    return 0;
}

思考一下输出是多少(运行环境:64bit机器, g++-12)

如果你的答案是:

P::new, size=4
P::new[], size=20

并且你知道为什么, 那么就要恭喜你, 可以不用往后看了.

想知道为什么注释掉析构函数之后就不会多出来size_t类型的记录数组大小的cookie的话, 就接着往下看吧.

下面我做了一个表格, 就是关于上面这个例子有析构函数, 没有析构函数以及有虚析构函数三种情况得到的内存大小数据.

析构行为 单个对象大小 对象数组大小
无析构函数 4 20=4*5
有非虚析构函数 4 28=4*5+8
有虚析构函数 16=4+8+4 88=16*5+8

并且, clang++ -cc1 -fdump-record-layouts test.cpp查看内存布局发现:

*** Dumping AST Record Layout
         0 | class P
         0 |   (P vtable pointer)
         8 |   int a
           | [sizeof=16, dsize=12, align=8,
           |  nvsize=12, nvalign=8]

发现存在内存对齐, 所以就变成了16而不是12了.

并且, 没有析构函数的时候并不会占用多余的内存来记录数组大小, 这是因为没有析构动作, 那么也不会去记录析构的数组大小, 换句话说, 只有在调用用户的析构函数的时候, 才会申请额外的内存size_t来存储数组大小, 这就是8的由来.

但是, 类中包含指针对象(例如string)的话就一定会产生size_t大小的一个变量来保存数组长度信息.

[c++ - 成员运算符 new] 的参数“大小”增加,如果类具有析构函数/删除[] - 堆栈溢出 (stackoverflow.com);

placement new(定位 new)

定义

cppprimer的定义是:

通过改变使用new的方式来阻止其抛出异常.

  int *p1 = new int; //分配失败, new throw `std::bad_alloc`
  int *p2 = new (nothorw) int; //分配失败, new返回空指针

这种形式(第二行)的new称为placement new(定位new)表达式, 这种写法允许我们向new传递额外的参数.

侯捷老师的PPT:

placement-new 允许我们将对象建构在allocated memory(已分配好的内存)中, 但是没有placement-delete, 因为并没有额外分配内存空间, 或者可以称呼与placement-new对应的是placement-delete. placement-new:等同于调用构造函数.

基本使用

#include <complex>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <new>
using namespace std;

void t1() {
    char* buf = new char[sizeof(complex<int>) * 3];
    complex<int>* pc = new (buf) complex<int>(1, 2);
    // 这里其实调用了下面的:
    // static void* operator new(size_t size, void* start);
    printf("buf=%p\n", buf);
    printf("pc=%p\n", pc);
    /* buf=0x600001bc1100 */
    /* pc=0x600001bc1100 */

    cout << pc->real() << " " << pc->imag() << endl; // 1 2
    // 标准库提供的placement new()重载
    delete[] pc;
}
int main(int argc, char const* argv[]) {
    t1();
    return 0;
}

直接使用, 原地重新分配, 可以看到地址是相同的.

重载placement new/placement delete

#include <iostream>

using namespace std;
/*placement-new*/
class Bad {}; // 稍后会作为异常抛出
class Foo {
public:
    Foo() { cout << "Foo::Foo()" << endl; }
    Foo(int) {
        cout << "Foo::Foo(int)" << endl;
        throw Bad(); // 这里给出一个异常的例子
    }
    // new
    static void* operator new(size_t size); // 一般的重载
    static void* operator new(size_t size,
                              void* start); // 标准库提供的placement new()重载
    static void* operator new(size_t size, long extra); // 新的placement new()
    static void* operator new(size_t size, long extra,
                              char init); // 另一个新的placement new()
    // static void* operator new(long extra, char init);
    // error, 第一参数必须是size_t类型

    /*可以重载对应版本的placement delete(), 但是不会被调用,
    只有当placement new()调用的ctor抛出异常时候, 才会调用这些重载版本的delete(),
    也只可能被这样调用, 用途是归还未能完全创建成功的object占用的内存memory*/
    static void operator delete(void*, size_t);
    static void operator delete(void*, void*);
    static void operator delete(void*, long);
    static void operator delete(void*, long, char);

private:
    int m_i;
};


void* Foo::operator new(size_t size) {
    cout << "operator new(size_t size), size=" << size << endl;
    return malloc(size);
}

void* Foo::operator new(size_t size, void* start) {
    cout << "operator new(size_t size, void* start), size=" << size
         << ", start=" << start << endl;
    return start;
}

void* Foo::operator new(size_t size, long extra) {
    cout << "operator new(size_t size, long extra), size=" << size
         << ", extra=" << extra << endl;
    return malloc(size + extra);
}

void* Foo::operator new(size_t size, long extra, char init) {
    cout << "operator new(size_t size, long extra, char init), size=" << size
         << ", extra=" << extra << ", init=" << init << endl;
    return malloc(size + extra);
}

// void* Foo::operator new(long extra, char init) {
//     //error: 'operator new' takes type size_t ('unsigned long') as first
//     parameter return malloc(extra);
// }

void Foo::operator delete(void*, size_t) {
    cout << "operator delete(void*, size_t)" << endl;
}

void Foo::operator delete(void*, void*) {
    cout << "operator delete(void*, size_t)" << endl;
}

void Foo::operator delete(void*, long) {
    cout << "operator delete(void*, long)" << endl;
}

void Foo::operator delete(void*, long, char) {
    cout << "operator delete(void*, long, char)" << endl;
}

下面是测试函数:

// 基本调用方式, 与析构
void t1() {
    Foo* pf = new (300, 'c') Foo;
    delete pf;
    /*
    Foo::Foo()
    operator delete(void*, size_t)
    */
}
// 全部调用方式
void t2() {
    Foo start;
    Foo* p1 = new Foo;
    Foo* p2 = new (&start) Foo;
    Foo* p3 = new (100) Foo;
    Foo* p4 = new (100, 'a') Foo;
    delete p1;
    delete p2;
    delete p3;
    delete p4;
    /*
    Foo::Foo()
    operator new(size_t size), size=4
    Foo::Foo()
    operator new(size_t size, void* start), size=4, start=0x16f35b04c
    Foo::Foo()
    operator new(size_t size, long extra), size=4, extra=100
    Foo::Foo()
    operator new(size_t size, long extra, char init), size=4, extra=100, init=a
    Foo::Foo()
    operator delete(void*, size_t)
    operator delete(void*, size_t)
    operator delete(void*, size_t)
    operator delete(void*, size_t)
    */
}
// 调用重载的operator delete的情况(bad_alloc)
void t3() {
    Foo start;

    Foo* p5 = new (100) Foo(1);
    delete p5;
    /*
    Foo::Foo()
    operator new(size_t size, long extra), size=4, extra=100
    Foo::Foo(int)
    libc++abi: terminating with uncaught exception of type Bad
    [1]    10066 abort
    */
    // Foo* p6 = new(100, 'a') Foo(1);
    // Foo* p7 = new(&start) Foo(1);
    // Foo* p8 = new Foo(1);
}
int main(int argc, char const* argv[]) {
    // t1();
    // t2();
    t3();
    return 0;
}

应用1: 委托构造

在C++11的委托构造函数出现之前, 可以使用placement new原地调用构造函数来完成构造.

#include <iostream>
using namespace std;

class P {
public:
    P() {
        new (this) P(1, 1.2);
        cout << "call P()\n";
    }
    P(int a) {
        new (this) P(a, 1.2);
        cout << "call P(int)\n";
    }
    // 使用委托构造
    //  P() : P(1, 1.2) { cout << "call P()\n"; }
    //  P(int a) : P(a, 1.2) { cout << "call P(int)\n"; }

private:
    // target ctor
    P(int a, double b) : m_a(a), m_b(b) { cout << "call P(int, double)\n"; }

    int m_a;
    double m_b;
};

void t1() {
    // 使用placement new
    P p1;
    // call P(int, double)
    // call P()

    P p2(10);
    // call P(int, double)
    // call P(int)
}

void t2() {
    // C++11: 使用委托构造
    P p1;
    // call P(int, double)
    // call P()

    P p2(10);
    // call P(int, double)
    // call P(int)
}

int main(int argc, char const *argv[]) {
    t1();
    // t2();
    return 0;
}

应用2: 显式调用构造函数

还可以这样在实例化的对象上通过placement new显式调用构造函数: (C++黑科技)

#include <iostream>
#include <string>
using namespace std;

void t1() {
    string* ps = new string;
    cout << "strs: " << *ps << endl;
    // ps->string::string("1");//error,不能直接调用, 但是编译器可以
    // error: 'class std::__cxx11::basic_string<char>' has no member named
    // 'string'
    // ps->string::~string;
}
class A {
public:
    int id;
    A(int i) : id(i) { cout << "ctor. this=" << this << ' ' << id << endl; }
    ~A() { cout << "dtor. this=" << this << endl; }
};

void t2() {
    A* pA = new A(1);
    // vc can build successfully
    // pA->A::A(3); // error: cannot call constructor 'A::A' directly
    delete pA;
    // ctor. this=0x600001d14040 1
    // dtor. this=0x600001d14040
}

void t3() {
    // 采用placement new 调用构造函数
    A* pA = new A(1);
    new (pA) A(2);
    // ctor. this=0x600000578040 1
    // ctor. this=0x600000578040 2
}

int main(int argc, char const* argv[]) {
    // t1();
    t2();
    // t3();
    return 0;
}