首页 > 代码库 > C++内存分配
C++内存分配
new为特定类型分配内存,并在新分配的内存中构造该类型的一个对象。new 表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象。某些情况下,需要将内存分配与对象构造分离开。
使用 new表达式的时候,分配内存,并在该内存中构造一个对象:使用 delete 表达式的时候,调用析构函数撤销对象,并将对象所用内存返还给系统。接管内存分配时,必须处理这两个任务。
C++ 提供下面两种方法分配和释放未构造的原始内存。allocator 类;以及标准库中的 operator new 和 operator delete函数。
C++ 还提供不同的方法在原始内存中构造和撤销对象:
allocator 类定义了名为 construct 和 destroy 的成员,construct 成员在未构造内存中初始化对象,destroy 成员在对象上运行适当的析构函数。
定位 new 表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象所在的内存。
算法 uninitialized_fill 和 uninitialized_copy 像 fill 和 copy算法一样执行,除了它们是在目的内存构造对象,而不是给对象赋值。
现代 C++ 程序一般应该使用 allocator 类来分配内存,它更安全更灵活。但是,在构造对象的时候,用 new 表达式比allocator::construct 成员更灵活。
一:allocator类
allocator 类是一个模板,它提供类型化的内存分配以及对象构造与撤销。
allocator<T> a |
定义名为 a 的 allocator 对象,可以分配内存或构造 T 类型的对象 |
a.allocate(n) |
分配原始的未构造内存以保存 T 类型的 n 个对象 |
a.deallocate(p, n) |
释放内存,在名为 p 的 T* 指针中包含的地址处保存 T 类型的 n 个对象。运行调用 deallocate 之前在该内存中构造的任意对象的 destroy 是用户的责任 |
a.construct(p, t) |
在 T* 指针 p 所指内存中构造一个新元素。运行 T 类型的复制构造函数用 t 初始化该对象 |
a.destroy(p) |
运行 T* 指针 p 所指对象的析构函数 |
uninitialized_copy(b, e, b2) |
从迭代器 b 和 e 指出的输入范围将元素复制到从迭代器b2 开始的未构造的原始内存中。该函数在目的地构造元素,而不是给它们赋值。假定由 b2 指出的目的地足以保存输入范围中元素的副本 |
uninitialized_fill(b, e, t) |
将由迭代器 b 和 e 指出的范围中的对象初始化为 t 的副本。假定该范围是未构造的原始内存。使用复制构造函数构造对象 |
uninitialized_fill_n(b, e, t, n) |
将由迭代器 b 和 e 指出的范围中至多 n 个对象初始化为 t 的副本。假定范围至少为 n 个元素大小。使用复制构造函数构造对象 |
allocator 类将内存分配和对象构造分开。当 allocator 对象分配内存的时候,它分配适当大小并排列成保存给定类型对象的空间。但是,它分配的内存是未构造的。
下面将模拟实现 vector 的一部分,定义名为Vector的类来展示的allocator使用:
// pseudo-implementation of memory allocation strategy for a vector-like class template <class T> class Vector { public: Vector(): elements(0), first_free(0), end(0) { } void push_back(const T&); // ... private: static std::allocator<T> alloc; // object to get raw memory void reallocate(); // get more space and copy existing elements T* elements; // pointer to first element in the array T* first_free; // pointer to first free element in the array T* end; // pointer to one past the end of the array // ... };
每个 Vector<T> 类型定义一个 allocator<T> 类型的 static 数据成员,以便在给定类型的 Vector 中分配和构造元素。每个 Vector 对象在指定类型的内置数组中保存其元素,并维持该数组的下列三个指针:
? elements,指向数组的第一个元素。
? first_free,指向最后一个实际元素之后的那个元素。
? end,指向数组本身之后的那个元素。
下图说明了这些指针的含义
push_back 使用这些指针将新元素加到 Vector 末尾:
template <class T> void Vector<T>::push_back(const T& t) { // are we out of space? if (first_free == end) reallocate(); // gets more space and copies existing elements to it alloc.construct(first_free, t); ++first_free; }
首先确定是否有可用空间,如果没有,就调用 reallocate函数,分配新空间并复制现存元素,将指针重置为指向新分配的空间。
一旦还有空间容纳新元素,它就调用alloc.construct 函数使用类型 T 的复制构造函数将 t 值复制到由 first_free 指出的元素,然后,将 first_free 加 1 以指出又有一个元素在用。
下面是reallocate函数的实现:
template <class T> void Vector<T>::reallocate() { std::ptrdiff_t size = first_free - elements; std::ptrdiff_t newcapacity = 2 * max(size, 1); T* newelements = alloc.allocate(newcapacity); uninitialized_copy(elements, first_free, newelements); // destroy the old elements in reverse order for (T *p = first_free; p != elements; /* empty */ ) alloc.destroy(--p); if (elements) alloc.deallocate(elements, end - elements); elements = newelements; first_free = elements + size; end = elements + newcapacity; }
函数首先计算当前在用的元素数目,将该数目翻倍,并请求 allocator 对象来获得所需数量的空间。如果 Vector 为空,就分配两个元素。
uninitialized_copy 调用使用标准 copy 算法的特殊版本。这个版本希望目的地是原始的未构造内存,它在目的地复制构造每个元素,而不是将输入范围的元素赋值给目的地,使用 T 的复制构造函数从输入范围将每个元素复制到目的地。
for 循环对旧数组中每个对象调用 allocator 的 destroy 成员,它按逆序撤销元素,从数组中最后一个元素开始,以第一个元素结束。destroy 函数运行T 类型的析构函数来释放旧元素所用的任何资源。
一旦复制和撤销了元素,就释放原来数组所用的空间。传给deallocate 一个零指针是不合法的。
最后,重置指针以指向新分配并初始化的数组。将 first_free 和 end指针分别置为指向最后构造的元素之后的单元以及所分配空间末尾的下一单元。
二:operator new 函数和 operator delete 函数
下面将介绍怎样用更基本的标准库机制实现相同的策略
首先,需要对 new 和 delete 表达式怎样工作有更多的理解。当使用 new表达式: string * sp = new string("initialized") 的时候,实际上发生三个步骤。首先,该表达式调用名为 operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。
当使用 delete 表达式 delete sp 删除动态分配对象的时候,发生两个步骤。首先,对 sp 指向的对象运行适当的析构函数;然后,通过调用名为 operator delete 的标准库函数释放该对象所用内存。
注意,new和delete表达式,与operator new和operator delete标准库函数是两回事。
1:operator new和operator delete标准库函数
operator new 和 operator delete 函数有两个重载版本,每个版本支持相关的new 表达式和 delete 表达式:
void *operator new(size_t); // allocate an object
void *operator new[](size_t); // allocate an array
void *operator delete(void*); // free an object
void *operator delete[](void*); // free an array
虽然 operator new 和 operator delete 函数的设计意图是供 new 和delete表达式使用,但它们通常是标准库中的可用函数。可以使用它们获得未构造内存,它们有点类似 allocate 类的 allocator 和 deallocate 成员。例如:
T* newelements = alloc.allocate(newcapacity);
这可以重新编写为:
T* newelements = static_cast<T*> (operator new[](newcapacity * sizeof(T)));
类似地:
alloc.deallocate(elements, end - elements);
可以重新编写为
operator delete[](elements);
这些函数的表现与 allocate 类的 allocator 和 deallocate 成员类似。但是,它们在一个重要方面有不同:它们在 void* 指针而不是类型化的指针上进行操作。allocate 成员分配类型化的内存,所以使用它的程序可以不必以字节为单位计算所需的内存量,也可以避免对 operator new 的返回值进行强制类型转换;类似地,deallocate 释放特定类型的内存,也不必转换为 void*。
一般而言,使用 allocator 比直接使用operator new 和 operator delete 函数更为类型安全。
2:定位new 表达式
标准库函数 operator new 和 operator delete 是 allocator 的allocate 和 deallocate 成员的低级版本,它们都分配但不初始化内存。
allocator 的成员 construct 和 destroy 也有两个低级选择,类似于 construct 成员,有第三种 new 表达式,称为定位 new。定位 new表达式不分配内存,而是在已分配的原始内存中初始化一个对象。定位 new 表达式的形式是:
new (place_address) type new (place_address) type (initializer-list)
其中 place_address 必须是一个指针,而 initializer-list 提供了(可能为空的)初始化列表,以便在构造新分配的对象时使用。
可以使用定位 new 表达式代替 Vector 实现中的 construct 调用。原来的代码:
alloc.construct (first_free, t);
可以用等价的定位 new 表达式代替
new (first_free) T(t);
定位 new 表达式比 allocator 类的 construct 成员更灵活。定位 new 表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。而construct 函数总是使用复制构造函数。
例如,可以用下面两种方式之一,从一对迭代器初始化一个已分配但未构造的 string 对象:
allocator<string> alloc; string *sp = alloc.allocate(2); // two ways to construct a string from a pair of iterators new (sp) string(b, e); // construct directly in place alloc.construct(sp + 1, string(b, e)); // build and copy a temporary
定位 new 表达式使用了接受一对迭代器的 string 构造函数,在 sp 指向的空间直接构造 string 对象。当调用 construct 函数的时候,必须首先从迭代器构造一个 string 对象,以获得传递给 construct 的 string 对象,然后,该函数使用 string 的复制构造函数,将那个未命名的临时 string 对象复制到sp 指向的对象中。
3:显式析构函数的调用
可以使用析构函数的显式调用作为调用 destroy 函数的低级选择。在使用 allocator 对象的 Vector 版本中,通过调用 destroy 函数清除每个元素:
for (T *p = first_free; p != elements; /* empty */ ) alloc.destroy(--p);
对于使用定位 new 表达式构造对象的程序,显式调用析构函数:
for (T *p = first_free; p != elements; /* empty */ ) p->~T(); // call the destructor
显式调用析构函数的效果是适当地清除对象本身。但是,并没有释放对象所占的内存,如果需要,可以重用该内存空间。调用 operator delete 函数不会运行析构函数,它只释放指定的内存。
三:类特定的 new 和 delete
前几节介绍了类怎样能够接管自己的内部数据结构的内存管理,另一种优化内存分配的方法涉及优化 new 表达式的行为。
默认情况下,new 表达式通过调用由标准库定义的 operator new 版本分配内存。通过定义自己的名为 operator new 和 operator delete 的成员,类可以管理用于自身类型的内存。编译器看到类类型的 new 或 delete 表达式的时候,它查看该类是否有operator new 或 operator delete 成员,如果类定义(或继承)了自己的成员new 和 delete 函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本。
类成员 operator new 函数必须具有返回类型 void* 并接受 size_t 类型的形参。由 new 表达式用以字节计算的分配内存量初始化函数的 size_t 形参。
类成员 operator delete 函数必须具有返回类型 void。它可以定义为接受单个 void* 类型形参,也可以定义为接受两个形参,即 void* 和 size_t 类型。由 delete 表达式用被 delete 的指针初始化 void* 形参,该指针可以是空指针。如果提供了 size_t 形参,就由编译器用第一个形参所指对象的字节大小自动初始化 size_t 形参。
除非类是某继承层次的一部分,否则形参 size_t 不是必需的。当 delete指向继承层次中类型的指针时,指针可以指向基类对象,也可以指向派生类对象。派生类对象的大小一般比基类对象大。如果基类有 virtual 析构函数,则传给 operator delete 的大小将根据被删除指针所指对象的动态类型而变化;如果基类没有 virtual 析构函数,那么,通过基类指针删除指向派生类对象的指针的行为,跟往常一样是未定义的。
这些函数隐式地为静态函数,不必显式地将它们声明为static,虽然这样做是合法的。成员 new 和 delete 函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),因此,这些函数没有成员数据可操纵。像任意其他静态成员函数一样,new 和 delete 只能直接访问所属类的静态成员。
也可以定义成员 operator new[] 和 operator delete[] 来管理类类型的数组。如果这些 operator 函数存在,编译器就使用它们代替全局版本。类成员 operator new[] 必须具有返回类型 void*,并且接受的第一个形参类型为 size_t。表示存储特定类型给定数目元素的数组的字节数值自动初始化操作符的 size_t 形参。
成员操作符 operator delete[] 必须具有返回类型 void,并且第一个形参为 void* 类型。用表示数组存储起始位置的值自动初始化操作符的 void* 形参。
类的操作符 delete[] 也可以有两个形参,第二个形参为 size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参。
如果类定义了自己的成员 new 和 delete,类的用户就可以通过使用全局作用域确定操作符,强制 new 或 delete 表达式使用全局的库函数。如果用户编写
Type *p = ::new Type; // uses global operator new ::delete p; // uses global operator delete
那么,即使类定义了自己的类特定的 operator new,也调用全局的 operator new;delete 类似。
如果用 new 表达式调用全局 operator new 函数分配内存,则delete 表达式也应该调用全局 operator delete 函数。
下面是一个定义了成员 new 和 delete的类的例子
class testalloc { public: testalloc(int a = 0): m_int(a),next(NULL){} void * operator new(size_t len); void operator delete(void *ptr); private: int m_int; testalloc *next; static std::allocator<testalloc> alloc; static testalloc *freeptr; static int chunk; static void add_to_freelist(testalloc *ptr); }; int testalloc::chunk = 100; testalloc* testalloc::freeptr = NULL; std::allocator<testalloc> testalloc::alloc; void testalloc::add_to_freelist(testalloc *ptr) { ptr->next = freeptr; freeptr = ptr; } void * testalloc::operator new(size_t len) { testalloc *ptr; if (! freeptr) { std::cout << "alloc new memory" << std::endl; ptr = alloc.allocate(chunk); for (int i = 0; i < chunk; i++) { add_to_freelist(ptr+i); } } ptr = freeptr; freeptr = freeptr->next; return static_cast<void *>(ptr); } void testalloc::operator delete(void *ptr) { if (ptr) add_to_freelist(static_cast<testalloc *>(ptr)); }
C++内存分配