前言
C++ 支持面向对象的设计,一个对象可以在栈或堆上创建,在栈上创建的话当离开当前函数栈,对象将被释放;而堆上创建则需要我们手动的销毁。而在堆上创建/销毁的关键字是 new 和 delete ,之前一直以为 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 表达式发生了什么
- 调用
operator new(或operator new[]),为对象存储分配内存; - 编译器运行相应的构造函数;
- 返回一个指向该对象的指针。
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++ 引入了 new 和 delete 来在堆上创建/销毁对象,内存不足时会导致分配失败,此时 new 会默认抛出 std::bad_alloc 异常,但很多情况不能因为分配失败而终止程序,对此处理异常的方式有哪些呢?总结有如下三点:
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异常来执行相应的处理。- 使用
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异常。 - placement new
给int *p1 = new int; // 分配失败时,new 抛出 bad_alloc int* p2 = new (nothrow) int; // 分配失败时,new 返回一个指针place_address传递nothrow,分配失败时,表示new不能抛出异常,且不会分配所需内存,而是返回一个空指针。
参考
- 《CPP Primer 第五版》第十九章
- 《STL 源码剖析》第二章
- 深入C++的new