C++中的new

265 阅读4分钟

前言

C++ 支持面向对象的设计,一个对象可以在栈或堆上创建,在栈上创建的话当离开当前函数栈,对象将被释放;而堆上创建则需要我们手动的销毁。而在堆上创建/销毁的关键字是 newdelete ,之前一直以为 new 的语法只有一个,即:

class A;
auto a = new A();

直到最近看《STL 源码剖析》(P45)一书中发现有个用法是:

template<class T1, class T2>
inline void_construct(T1* p, const T2& value) {
    new(p) T1(value); // placement new, invoke ctor of T1
}

在网上搜索并翻阅《CPP Primer》了解到一些,本文将对 new 进行一下总结。

调用 new 表达式发生了什么

  1. 调用 operator new(或 operator new[]),为对象存储分配内存;
  2. 编译器运行相应的构造函数;
  3. 返回一个指向该对象的指针。

new 的三种含义

new 表达式

就是我们常规使用的 new 表达式,用于在堆上建立对象,执行的三个步骤在上面提到。new 表达式和 delete 表达式的行为都无法由用户定义。

operator new 函数

包含在 new 头文件中,定义如下

void *operator new(size_t);

我们可以在自己定义的类中重载 opearator new 函数,但是第一个形参的类型必须是 size_t 且该形参不能含有默认实参,因为当编译器调用 operator new 函数时,会把存储指定类型对象所需的字节数传给 size_t 形参(调用 opeartor new[] 时,传入函数的是存储数组中所有元素所需的空间),在此基础上可以为其提供额外的形参。不过用到这些自定义函数的 new 表达式必须使用 new 的定位形式(即 placement new)。一般情况下定义具有任何形参的 operator new 是允许的,但是下面这个函数只能供标准库使用,不可以被重载:

void *operator new(size_t, void*);

placement new

当希望把内存分配和初始化操作分离开的话,对于前者我们已经知道可以调用 operator new,对于后者,placement new 形式可以解决这一问题。placement new 的形式如下:

new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }

其中 place_address 必须是一个指针,同时在 initializers 中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象。并且 placement new 允许我们在一个特定的、预先分配的内存地址上构造对象。这句话是什么意思呢?可以看以下用法,是允许的:

#include <iostream>
using namespace std;

class A {
public:
    A() = default;
    ~A() = default;
    void test()
    {
        cout << "test" << endl;
    }
};

int main()
{
    char s[sizeof(A)];
    A* a = (A*)s;
    new (a) A();
    a->test();
    return 0;
}

当然这有点刻意,似乎也没什么实际用途,再来看一下《CPP Primer》(chap 19)提到的一个自定义名为 Token 的类,内部含有 union 成员:

class Token {
public:
    Token();
    Token(const Token& t);
    Token& operator=(const Token&);
    ~Token();
    Token& operator=(const std::string&);
    Token& operator=(char);
    Token& operator=(int);
    Token& operator=(double);
}
private:
    enum {INT, CHAR, DBL, STR} tok;
    union {
        char cval;
        int ival;
        double dval;
        std::string sval;
    }
    void copyUnion(const Token&);

这里看一下 Token& operator=(const std::string&) 的实现:

Token& Token::operator=(const std::string& s)
{
    if (tok == STR)
        sval = s;
    else
        new (&sval) string(s); // 此处使用 placement new
    tok = STR;
    return *this;
}

使用 placement new 时,在原有成员 sval 所在空间处调用构造函数,完成初始化操作。

operator new 的重载

代码如下,在 return ::operator new(size) 之前我们可以自定义一些处理:

#include <iostream>
using namespace std;

class A {
public:
    A() = default;
    ~A() = default;
    void* operator new(size_t size)
    {
        cout << "size is: " << size << endl;
        return ::operator new(size);
    }
    void test()
    {
        cout << "test" << endl;
    }
};

int main()
{
    A* a = new A();
    a->test();
    delete a;
    return 0;
}

new 不成功时怎么办

相比较于 C 语言的内存管理,C++ 引入了 newdelete 来在堆上创建/销毁对象,内存不足时会导致分配失败,此时 new 会默认抛出 std::bad_alloc 异常,但很多情况不能因为分配失败而终止程序,对此处理异常的方式有哪些呢?总结有如下三点:

  1. try catch 异常捕获处理
    #include <iostream>
    #include <new>
    using namespace std;
    
    int main()
    {
        try {
            char *p = new char[8 * 1024 * 1024 * 1024];
            delete p;
        }
        catch (bad_alloc) {
            cout << "alloc failure" << endl;
        }
        return 0;
    }
    
    通过捕获 bad_alloc 异常来执行相应的处理。
  2. 使用 set_new_handler 设置 new 的异常处理函数
    #include <iostream>
    #include <cstdlib>
    #include <new>
    using namespace std;
    
    void mm_handler()
    {
        cout << "alloc failure" << endl;
        exit(0);
    }
    
    int main()
    {
        set_new_handler(mm_handler);
        char *p = new char[8 * 1024 * 1024 * 1024];
        delete p;
        return 0;
    }
    
    set_new_handler(0) 就是不指定任何处理函数,此时会抛 bad_alloc 异常。
  3. placement new
    int *p1 = new int; // 分配失败时,new 抛出 bad_alloc
    int* p2 = new (nothrow) int; // 分配失败时,new 返回一个指针
    
    place_address 传递 nothrow,分配失败时,表示 new 不能抛出异常,且不会分配所需内存,而是返回一个空指针。

参考

  1. 《CPP Primer 第五版》第十九章
  2. 《STL 源码剖析》第二章
  3. 深入C++的new