乍一看,在C++中动态分配内存很简单:new是分配,delete是释放,就这么简单。然而,这篇文章讲得要复杂一点,并且要考虑到自定义层次。这也许对简单的程序并不重要,但对你在代码中控制内存却是十分必要的,是否能写一个自定义的分配器,某种高级内存管理表或一个特定的垃圾回收机制。
这篇文章并不是一个综合的手册,而是一个C++中各种内存分配方法的概述。它面向已经很熟悉C++语言的读者。
原生operator new
我们先从原生operator new开始。考虑如下代码,它用来分配5个int型的空间并返回指向他们的指针[1]:
int* v = static_cast<int*>(::operator new(5 * sizeof(*v)));
当像如上的调用,operator new扮演原生的内存分配角色,类似malloc。上面等价于:
int* v = static_cast<int*>(malloc(5 * sizeof(*v)));
释放用operator new分配的内存用operator delete:
::operator delete(v);
你愿意永远用原生new和delete函数吗?是,只在极少数不用,我在下面的文章中会论证的。为什么用它们而不用原来的可信的malloc和free呢?一个很充分的原因就是你想保持代码在C++领域的完整性。混合使用new和free(或malloc和delete)是很不可取的(big NO NO)。用new和delete的另一个原因是你可以重载(overload)或重写(override)这些函数,只要你需要。下面是个例子:
void operator delete(void* ptr) throw()
{
cerr << "deallocating at " << ptr << endl;
free(ptr);
}
</div>
通常,注意到new被用来给内置类型,不包含用户自定义new函数的类的对象,和任意类型的数组分配空间,使用的都是全局的运算符new。当new被用来为已经被重定义new的类实例化时,用的就是那个类的new函数。
下面来看下带new函数的类。
特定类的operator new
大家有时很好奇"operator new"和"new operator"的区别。前者可以是一个重载的operator new,全局的或者特定类或者原生的operator new。后者是你经常用来分配内存的C++内置的new operator,就像:
Car* mycar = new Car;
C++支持操作符重载,并且我们可以重载的其中一个就是new。
下面是个例子:
void operator delete(void* p)
{
cerr << "deleten";
::operator delete(p);
}
private:
int m_data;
};
class Derived : public Base
{
private:
int m_derived_data;
vector<int> z, y, x, w;
};
int main()
{
Base* b = new Base;
delete b;
Derived* d = new Derived;
delete d;
return 0;
}
</div>
打印结果:
new 4 bytes
delete
new 56 bytes
delete
在基类被重载的operator new和operator delete也同样被子类继承。如你所见,operator new得到了两个类的正确大小。注意实际分配内存时使用了::operator new,这是前面所描述过的原生new。在调用前面的两个冒号很关键,是为了避免进行无限递归(没有它函数将一直调用自己下去)。
为什么你要为一个类重载operator new?这里有许多理由。
性能:默认的内存分配算符被设计成通用的。有时你想分配给一个非常特殊的对象,通过自定义分配方式可以明显地提高内存管理。许多书和文章都讨论了这种情况。尤其是"Modern C++ Design"的第4章展示了一个为较小的对象的非常好的设计并实现了自定义的分配算符。
调试 & 统计:完全掌握内存的分配和释放为调试提供了很好的灵活性,统计信息和性能分析。你可将你的分配算符插入进专门用来探测缓冲区溢出的守卫,通过分配算符和释放算符(deallocations)的比较来检测内存泄漏,为统计和性能分析积累各种指标,等等。
个性化:对于非标准的内存分配方式。一个很好的例子是内存池或arenas,它们都使得内存管理变得更简单。另一个例子是某个对象的完善的垃圾回收系统,可以通过为一个类或整个层面写你自己的operators new和delete。
研究在C++中new运算符是很有帮助的。分配是分两步进行:
1. 首先,用全局operator new指导系统请求原生内存。
2. 一旦请求内存被分配,一个新的对象就在其中开始构造。
The C++ FAQ给出一个很好的例子,我很愿意在这里这出来:
当你写下这段代码:
Foo* p = new Foo();
编译器会生成类似这种功能的代码:
// don't catch exceptions thrown by the allocator itself
//不用捕捉分配器自己抛出的异常
void* raw = operator new(sizeof(Foo));
// catch any exceptions thrown by the ctor
//捕捉ctor抛出的任何异常
try {
p = new(raw) Foo(); // call the ctor with raw as this 像这样用raw调用ctor分配内存
}
catch (...) {
// oops, ctor threw an exception 啊哦,ctor抛出了异常
operator delete(raw);
throw; // rethrow the ctor's exception 重新抛出ctor的异常
}
</div>
其中在try中很有趣的一段语法被称为"placement new",我们马上就会讨论到。为了使讨论完整,我们来看下用delete来释放一个对象时一个相似的情况,它也是分两步进行:
1.首先,将要被删除对象的析构函数被调用。
2.然后,被对象占用的内存通过全局operator delete函数返还给系统。
所以:
delete p;
等价于[2]:
if (p != NULL) {
p->~Foo();
operator delete(p);
}
这时正适合我重复这篇文章第一段提到的,如果一个类有它自己的operator new或 operator delete,这些函数将被调用,而不是调用全局的函数来分配和收回内存。
Placement new
现在,回来我们上面看到样例代码中的"placement new"问题。它恰好真的能用在C++代码中的语法。首先,我想简单地解释它如何工作。然后,我们将看到它在什么时候有用。
直接调用 placement new会跳过对象分配的第一步。也就是说我们不会向操作系统请求内存。而是告诉它有一块内存用来构造对象[3]。下面的代码表明了这点:
&nb