Team LiB
Previous Section Next Section

18.1. Optimizing Memory Allocation

18.1. 优化内存分配

Memory allocation in C++ is a typed operation: new (Section 5.11, p. 174) allocates memory for a specified type and constructs an object of that type in the newly allocated memory. A new expression automatically runs the appropriate constructor to initialize each dynamically allocated object of class type.

C++ 的内存分配是一种类型化操作:new第 5.11 节)为特定类型分配内存,并在新分配的内存中构造该类型的一个对象。new 表达式自动运行合适的构造函数来初始化每个动态分配的类类型对象。

The fact that new allocates memory on a per-object basis can impose unacceptable run-time overhead on some classes. Such classes might need to make user-level allocation of objects of the class type faster. A common strategy such classes use is to preallocate memory to be used when new objects are created, constructing each new object in preallocated memory as needed.

new 基于每个对象分配内存的事实可能会对某些类强加不可接受的运行时开销,这样的类可能需要使用用户级的类类型对象分配能够更快一些。这样的类使用的通用策略是,预先分配用于创建新对象的内存,需要时在预先分配的内存中构造每个新对象。

Other classes want to minimize allocations needed for their own data members. For example, the library vector class preallocates (Section 9.4, p. 330) extra memory to hold additional elements if and when they are added. New elements are added in this reserved capacity. Preallocating elements allows vector to efficiently add elements while keeping the elements in contiguous memory.

另外一些类希望按最小尺寸为自己的数据成员分配需要的内存。例如,标准库中的 vector 类预先分配(第 9.4 节)额外内存以保存加入的附加元素,将新元素加入到这个保留容量中。将元素保持在连续内存中的时候,预先分配的元素使 vector 能够高效地加入元素。

In each of these casespreallocating memory to hold user-level objects or to hold the internal data for a classthere is the need to decouple memory allocation from object construction. The obvious reason to decouple allocation and construction is that constructing objects in preallocated memory is wasteful. Objects may be created that are never used. Those objects that are used must be reassigned new values when the preallocated object is actually used. More subtly, some classes could not use preallocated memory if it had to be constructed. As an example, consider vector, which uses a preallocation strategy. If objects in preallocated memory had to be constructed, then it would not be possible to have vectors of types that do not have a default constructorthere would be no way for vector to know how to construct these objects.

在每种情况下(预先分配内存以保存用户级对象或者保存类的内部数据)都需要将内存分配与对象构造分离开。将内存分配与对象构造分离开的明显的理由是,在预先分配的内存中构造对象很浪费,可能会创建从不使用的对象。当实际使用预先分配的对象的时候,被使用的对象必须重新赋以新值。更微妙的是,如果预先分配的内存必须被构造,某些类就不能使用它。例如,考虑 vector,它使用了预先分配策略。如果必须构造预先分配的内存中的对象,就不能有基类型为没有默认构造函数的 vector——vector 没有办法知道怎样构造这些对象。

The techniques presented in this section are not guaranteed to make all programs faster. Even when they do improve performance, they may carry other costs such as space usage or difficulty of debugging. It is always best to defer optimization until the program is known to work and when run-time measurements indicate that improving memory allocation will solve known performance problems.

本节提出的技术不保证使所有程序更快。即使它们确实能改善性能,也可能带来其他开销,如空间的使用或调试困难。最好将优化推迟到已知程序能够工作,并且运行时测试指出改进内存分配将解决已知的性能问题的时候。

18.1.1. Memory Allocation in C++

18.1.1. C++ 中的内存分配

In C++ memory allocation and object construction are closely intertwined, as are object destruction and memory deallocation. When we use a new expression, memory is allocated, and an object is constructed in that memory. When we use a delete expression, a destructor is called to destroy the object and the memory used by the object is returned to the system.

C++ 中,内存分配和对象构造紧密纠缠,就像对象和内存回收一样。使用 new 表达式的时候,分配内存,并在该内存中构造一个对象:使用 delete 表达式的时候,调用析构函数撤销对象,并将对象所用内存返还给系统。

When we take over memory allocation, we must deal with both these tasks. When we allocate raw memory, we must construct object(s) in that memory. Before freeing that memory, we must ensure that the objects are properly destroyed.

接管内存分配时,必须处理这两个任务。分配原始内存时,必须在该内存中构造对象;在释放该内存之前,必须保证适当地撤销这些对象。

Assigning to an object in unconstructed memory rather than initializing it is undefined. For many classes, doing so causes a crash at run time. Assignment involves obliterating the existing object. If there is no existing object, then the actions in the assignment operator can have disastrous effects.

对未构造的内存中的对象进行赋值而不是初始化,其行为是未定义的。对许多类而言,这样做引起运行时崩溃。赋值涉及删除现存对象,如果没有现存对象,赋值操作符中的动作就会有灾难性效果。

C++ provides two ways to allocate and free unconstructed, raw memory:

C++ 提供下面两种方法分配和释放未构造的原始内存。

  1. The allocator class, which provides type-aware memory allocation. This class supports an abstract interface to allocating memory and subsequently using that memory to hold objects.

    allocator,它提供可感知类型的内存分配。这个类支持一个抽象接口,以分配内存并随后使用该内存保存对象。

  2. The library operator new and operator delete functions, which allocate and free raw, untyped memory of a requested size.

    标准库中的 operator newoperator delete,它们分配和释放需要大小的原始的、未类型化的内存。

C++ also provides various ways to construct and destroy objects in raw memory:

C++ 还提供不同的方法在原始内存中构造和撤销对象。

  1. The allocator class defines members named construct and destroy, which operate as their names suggest. The construct member initializes objects in unconstructed memory; the destroy member runs the appropriate destructor on objects.

    allocator 类定义了名为 constructdestroy 的成员,其操作正如它们的名字所指出的那样:construct 成员在未构造内存中初始化对象,destroy 成员在对象上运行适当的析构函数。

  2. The placement new expression takes a pointer to unconstructed memory and initializes an object or an array in that space.

    定位 new 表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或一个数组。

  3. We can directly call an object's destructor to destroy the object. Running the destructor does not free the memory in which the object resides.

    可以直接调用对象的析构函数来撤销对象。运行析构函数并不释放对象所在的内存。

  4. The algorithms uninitialized_fill and uninitialized_copy execute like the fill and copy algorithms except that they construct objects in their destination rather than assigning to them.

    算法 uninitialized_filluninitialized_copyfillcopy 算法一样执行,除了它们的目的地构造对象而不是给对象赋值之外。

Modern C++ programs ordinarily ought to use the allocator class to allocate memory. It is safer and more flexible. However, when constructing objects, the placement new expression is more flexible than the allocator::construct member. There are some cases where placement new must be used.

现代 C++ 程序一般应该使用 allocator 类来分配内存,它更安全更灵活。但是,在构造对象的时候,用 new 表达式比 allocator::construct 成员更灵活。有几种情况下必须使用 new

18.1.2. The allocator Class

18.1.2. allocator

The allocator class is a template that provides typed memory allocation and object construction and destruction. Table 18.1 on the following page outlines the operations that allocator supports.

allocator 类是一个模板,它提供类型化的内存分配以及对象构造与撤销。表 18.1 概述了 allocator 类支持的操作。

Table 18.1. Standard allocator Class and Customized Algorithms
表 18.1. 标准 allocator 类与定制算法

allocator<T> a;

Defines an allocator object named a that can allocate memory or construct objects of type T.

定义名为 aallocator 对象,可以分配内存或构造 T 类型的对象

a.allocate(n)

Allocates raw, unconstructed memory to hold n objects of type T.

分配原始的未构造内存以保存 T 类型的 n 个对象

a.deallocate(p, n)

Deallocates memory that held n objects of type T starting at address contained in the T* pointer named p. It is the user's responsibility to run destroy on any objects that were constructed in this memory before calling deallocate.

释放内存,在名为 pT* 指针中包含的地址处保存 T 类型的 n 个对象。运行调用 deallocate 之前在该内存中构造的任意对象的 destroy 是用户的责任

a.construct(p, t)

Constructs a new element in the memory pointed to by the T* pointer p. The copy constructor of type T is run to initialize the object from t.

T* 指针 p 所指内存中构造一个新元素。运行 T 类型的复制构造函数用 t 初始化该对象

a.destroy(p)

Runs the destructor on the object pointed to by the T* pointer p.

运行 T* 指针 p 所指对象的析构函数

uninitialized_copy(b, e, b2)

 

Copies elements from the input range denoted by iterators b and e into unconstructed, raw memory beginning at iterator b2. The function constructs elements in the destination, rather than assigning them. The destination denoted by b2 is assumed large enough to hold a copy of the elements in the input range.

从迭代器 be 指出的输入范围将元素复制到从迭代器 b2 开始的未构造的原始内存中。该函数在目的地构造元素,而不是给它们赋值。假定由 b2 指出的目的地足以保存输入范围中元素的副本

uninitialized_fill(b, e, t)

 

Initializes objects in the range denoted by iterators b and e as a copy of t. The range is assumed to be unconstructed, raw memory. The objects are constructed using the copy constructor.

将由迭代器 be 指出的范围中的对象初始化为 t 的副本。假定该范围是未构造的原始内存。使用复制构造函数构造对象

uninitialized_fill_n(b, e, t, n)

 

Initializes at most an integral number n objects in the range denoted by iterators b and e as a copy of t. The range is assumed to be at least n elements in size. The objects are constructed using the copy constructor.

将由迭代器 be 指出的范围中至多 n 个对象初始化为 t 的副本。假定范围至少为 n 个元素大小。使用复制构造函数构造对象


The allocator class separates allocation and object construction. When an allocator object allocates memory, it allocates space that is appropriately sized and aligned to hold objects of the given type. However, the memory it allocates is unconstructed. Users of allocator must separately construct and destroy objects placed in the memory it allocates.

allocator 类将内存分配和对象构造分开。当 allocator 对象分配内存的时候,它分配适当大小并排列成保存给定类型对象的空间。但是,它分配的内存是未构造的,allocator 的用户必须分别 constructdestroy 放置在该内存中的对象。

Using allocator to Manage Class Member Data
使用 allocator 管理类成员数据

To understand how we might use a preallocation strategy and the allocator class to manage the internal data needs of a class, let's think a bit more about how memory allocation in the vector class might work.

为了理解可以怎样使用预分配策略以及 allocator 类来管理类的内部数据需要,让我们再想想 vector 类中的内存分配会怎样工作。

Recall that the vector class stores its elements in contiguous storage. To obtain acceptable performance, vector preallocates more elements than are needed (Section 9.4, p. 330). Each vector member that adds elements to the container checks whether there is space available for another element. If so, then the member initializes an object in the next available spot in preallocated memory. If there isn't a free element, then the vector is reallocated: The vector obtains new space, copies the existing elements into that space, adds the new element, and frees the old space.

回忆一下,vector 类将元素保存在连续的存储中。为了获得可接受的性能,vector 预先分配比所需元素更多的元素(第 9.4 节)。每个将元素加到容器中的 vector 成员检查是否有可用空间以容纳另一元素。如果有,该成员在预分配内存中下一可用位置初始化一个对象;如果没有自由元素,就重新分配 vectorvector 获取新的空间,将现在元素复制到空间,增加新元素,并释放旧空间。

The storage that vector uses starts out as unconstructed memory; it does not yet hold any objects. When the elements are copied to or added in this preallocated space, they must be constructed using the construct member of allocator.

vector 所用存储开始是未构造内存,它还没有保存任何对象。将元素复制或增加到这个预分配空间的时候,必须使用 allocator 类的 construct 成员构造元素。

To illustrate these concepts we'll implement a small portion of vector. We'll name our class Vector to distinguish it from the standard vector class:

为了说明这些概念,我们将实现 vector 的一小部分。将我们的类命名为 Vector,以区别于标准类 vector

     // 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
         // ...
     };

Each Vector<T> type defines a static data member of type allocator<T> to allocate and construct the elements in Vectors of the given type. Each Vector object keeps its elements in a built-in array of the indicated type and maintains three pointers into that array:

每个 Vector<T> 类型定义一个 allocator<T> 类型的 static 数据成员,以便在给定类型的 Vector 中分配和构造元素。每个 Vector 对象在指定类型的内置数组中保存其元素,并维持该数组的下列三个指针:

  • elements, which points to the first element in the array

    elements,指向数组的第一个元素。

  • first_free, which points just after the last actual element

    first_free,指向最后一个实际元素之后的那个元素。

  • end, which points just after the end of the array itself

    end,指向数组本身之后的那个元素。

Figure 18.1 illustrates the meaning of these pointers.

图 18.1 说明了这些指针的含义。

Figure 18.1. Vector Memory Allocation Strategy
图 18.1. Vector 内存分配策略


We can use these pointers to determine the size and capacity of the Vector:

可以使用这些指针来确定 Vector 的大小和容量:

  • The size of a Vector (the number of elements actually in use) is equal to first_free elements.

    Vectorsize(实际使用的元素的数目)等于 first_free-elements

  • The capacity of a Vector (the total number of elements that could be defined before the Vector has to be reallocated) is equal to end elements.

    Vectorcapacity(在必须重新分配 Vector 之前,可以定义的元素的总数)等于end-elements

  • The free capacity (the number of elements that can be added before a reallocation is necessary) is end first_free.

    自由空间(在需要重新分配之前,可以增加的元素的数目)是 end-first_free

Using construct
使用 construct

The push_back member uses these pointers to add a new element to the end of the Vector:

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;
     }

The push_back function starts by determining whether there is space available. If not, it calls reallocate. That member allocates new space and copies the existing elements. It resets the pointers to point to the newly allocated space.

push_back 函数首先确定是否有可用空间,如果没有,就调用 reallocate 函数,reallocate 分配新空间并复制现存元素,将指针重置为指向新分配的空间。

Once push_back knows that there is room for the new element, it asks the allocator object to construct a new last element. The construct function uses the copy constructor for type T to copy the value t into the element denoted by first_free. It then increments first_free to indicate that one more element is in use.

一旦 push_back 函数知道还有空间容纳新元素,它就请求 allocator 对象构造一个新的最后元素。construct 函数使用类型 T 的复制构造函数将 t 值复制到由 first_free 指出的元素,然后,将 first_free 加 1 以指出又有一个元素在用。

Reallocating and Copying Elements
重新分配元素与复制元素

The reallocate function has the most work to do:

reallocate 函数所做的工作最多:

     template <class T> void Vector<T>::reallocate()
     {
         // compute size of current array and allocate space for twice as many elements
         std::ptrdiff_t size = first_free - elements;
         std::ptrdiff_t newcapacity = 2 * max(size, 1);
         // allocate space to hold newcapacity number of elements of type T
         T* newelements = alloc.allocate(newcapacity);

         // construct copies of the existing elements in the new space
         uninitialized_copy(elements, first_free, newelements);
         // destroy the old elements in reverse order
         for (T *p = first_free; p != elements; /* empty */ )
            alloc.destroy(--p);

         // deallocate cannot be called on a 0 pointer
         if (elements)
             // return the memory that held the elements
             alloc.deallocate(elements, end - elements);
         // make our data structure point to the new elements
         elements = newelements;
         first_free = elements + size;
         end = elements + newcapacity;
     }

We use a simple but surprisingly effective strategy of allocating twice as much memory each time we reallocate. The function starts by calculating the current number of elements in use, doubling that number, and asking the allocator object to obtain the desired amount of space. If the Vector is empty, we allocate two elements.

我们使用一个简单但效果惊人的策略:每次重新分配时分配两倍内存。函数首先计算当前在用的元素数目,将该数目翻倍,并请求 allocator 对象来获得所需数量的空间。如果 Vector 为空,就分配两个元素。

If Vector holds ints, the call to allocate allocates space for newcapacity number of ints. If it holds strings, then it allocates that space for the given number of strings.

如果 Vector 保存 int 值,allocate 函数调用为 newcapacity 数目的 int 值分配空间;如果 Vector 保存 string 对象,它就为给定数目的 string 对象分配空间。

The call to uninitialized_copy uses a specialized version of the standard copy algorithm. This version expects its destination to be raw, unconstructed memory. Rather than assigning elements from the input range to the destination, it copy-constructs each element in the destination. The copy constructor for T is used to copy each element from the input range to the destination.

uninitialized_copy 调用使用标准 copy 算法的特殊版本。这个版本希望目的地是原始的未构造内存,它在目的地复制构造每个元素,而不是将输入范围的元素赋值给目的地,使用 T 的复制构造函数从输入范围将每个元素复制到目的地。

The for loop calls the allocator member destroy on each object in the old array. It destroys the elements in reverse order, starting with the last element in the array and finishing with the first. The destroy function runs the destructor for type T to free any resources used by the old elements.

for 循环对旧数组中每个对象调用 allocatordestroy 成员它按逆序撤销元素,从数组中最后一个元素开始,以第一个元素结束。destroy 函数运行 T 类型的析构函数来释放旧元素所用的任何资源。

Once the elements have been copied and destroyed, we free the space used by the original array. We must check that elements actually pointed to an array before calling deallocate.

一旦复制和撤销了元素,就释放原来数组所用的空间。在调用 deallocate 之前,必须检查 elements 是否实际指向一个数组。

deallocate expects a pointer that points to space that was allocated by allocate. It is not legal to pass deallocate a zero pointer.

deallocate 期待指向由 allocate 分配的空间的指针,传给 deallocate 一个零指针是不合法的。

Finally, we have to reset the pointers to address the newly allocated and initialized array. The first_free and end pointers are set to denote one past the last constructed element and one past the end of the allocated space, respectively.

最后,必须重置指针以指向新分配并初始化的数组。将 first_freeend 指针分别置为指向最后构造的元素之后的单元以及所分配空间末尾的下一单元。

Exercises Section 18.1.2

Exercise 18.1:

Implement your own version of the Vector class including versions of the vector members reserve (Section 9.4, p. 330), resize (Section 9.3.5, p. 323), and the const and nonconst subscript operators (Section 14.5, p. 522).

实现自己的 Vector 类的版本,包括 vector 成员 reserve第 9.4 节)、resize第 9.3.5 节)以及 const 和非 const 下标操作符(第 14.5 节)。

Exercise 18.2:

Define a typedef that uses the corresponding pointer type as the iterator for your Vector.

定义一个类型别名,使用对应指针类型作为 Vectoriterator

Exercise 18.3:

To test your Vector class, reimplement earlier programs you wrote using vector to use Vector instead.

为了测试你的 Vector 类,重新实现前面用 vector 编写的程序,用 Vector 代替 vector


18.1.3. operator new and operator delete Functions

18.1.3. operator new 函数和 operator delete 函数

The previous subsection used the vector class to show how we could use the allocator class to manage a pool of memory for a class' internal data storage. In the next three subsections we'll look at how we might implement the same strategy using the more primitive library facilities.

前几节使用 vector 类说明了怎样使用 allocator 类来管理用于类的内部数据存储的内存池,下面三节将介绍怎样用更基本的标准库机制实现相同的策略。

First, we need to understand a bit more about how new and delete expressions work. When we use a new expression

首先,需要对 newdelete 表达式怎样工作有更多的理解。当使用 new 表达式

     // new expression
     string * sp = new string("initialized");

three steps actually take place. First, the expression calls a library function named operator new to allocate raw, untyped memory large enough to hold an object of the specified type. Next, a constructor for the type is run to construct the object from the specified initializers. Finally, a pointer to the newly allocated and constructed object is returned.

的时候,实际上发生三个步骤。首先,该表达式调用名为 operator new 的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象;接下来,运行该类型的一个构造函数,用指定初始化式构造对象;最后,返回指向新分配并构造的对象的指针。

When we use a delete expression to delete a dynamically allocated object:

当使用 delete 表达式

     delete sp;

two steps happen. First, the appropriate destructor is run on the object to which sp points. Then, the memory used by the object is freed by calling a library function named operator delete.

删除动态分配对象的时候,发生两个步骤。首先,对 sp 指向的对象运行适当的析构函数;然后,通过调用名为 operator delete 的标准库函数释放该对象所用内存。

Terminology: new Expression versus operator new Function

术语对比:new 表达式与 operator new 函数

The library functions operator new and operator delete are misleadingly named. Unlike other operator functions, such as operator=, these functions do not overload the new or delete expressions. In fact, we cannot redefine the behavior of the new and delete expressions.

标准库函数 operator newoperator delete 的命名容易让人误解。与其他 operator 函数(如 operator=)不同,这些函数没有重载 newdelete 表达式,实际上,我们不能重定义 newdelete 表达式的行为。

A new expression executes by calling an operator new function to obtain memory and then constructs an object in that memory. A delete expression executes by destroying an object and then calls an operator delete function to free the memory used by the object.

通过调用 operator new 函数执行 new 表达式获得内存,并接着在该内存中构造一个对象,通过撤销一个对象执行 delete 表达式,并接着调用 operator delete 函数,以释放该对象使用的内存。

Because the new (or delete) expressions and the underlying library functions have the same name, it is easy to confuse the two.

因为 new(或 delete)表达式与标准库函数同名,所以二者容易混淆。

The operator new and operator delete Interface
operator newoperator delete 接口

There are two overloaded versions of operator new and operator delete functions. Each version supports the related new and delete expression:

operator newoperator 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

Using the Allocation Operator Functions
使用分配操作符函数

Although the operator new and operator delete functions are intended to be used by new expressions, they are generally available functions in the library. We can use them to obtain unconstructed memory. They are somewhat analogous to the allocate and deallocate members of the allocator class. For example, instead of using an allocator, we could have used the operator new and operator delete functions in our Vector class. When we allocated new space we wrote

虽然 operator newoperator delete 函数的设计意图是供 new 表达式使用,但它们通常是标准库中的可用函数。可以使用它们获得未构造内存,它们有点类似 allocate 类的 allocatordeallocate 成员。例如,代替使用 allocator 对象,可以在 Vector 类中使用 operator newoperator delete 函数。在分配新空间时我们曾编写

     // allocate space to hold newcapacity number of elements of type T
     T* newelements = alloc.allocate(newcapacity);

which could be rewritten as

这可以重新编写为

     // allocate unconstructed memory to hold newcapacity elements of type T
     T* newelements = static_cast<T*>
                    (operator new[](newcapacity * sizeof(T)));

Similarly, when we deallocated the old space pointed to be the Vector member elements we wrote

类似地,在重新分配由 Vector 成员 elements 指向的旧空间的时候,我们曾经编写

     // return the memory that held the elements
     alloc.deallocate(elements, end - elements);

which could be rewritten as

这可以重新编写为

     // deallocate the memory that they occupied
     operator delete[](elements);

These functions behave similarly to the allocate and deallocate members of the allocator class. However, they differ in one important respect: They operate on void* pointers rather than typed pointers.

这些函数的表现与 allocate 类的 allocatordeallocate 成员类似。但是,它们在一个重要方面有不同:它们在 void* 指针而不是类型化的指针上进行操作。

In general, it is more type-safe to use an allocator rather than using the operator new and operator delete functions directly.

一般而言,使用 allocator 比直接使用 operator newoperator delete 函数更为类型安全。

The allocate member allocates typed memory, so programs that use it can avoid the necessity of calculating the byte-count amount of memory needed. They also can avoid casting (Section 5.12.4, p. 183) the return from operator new. Similarly, deallocate frees memory of a specific type, again avoiding the necessity for converting to void*.

allocate 成员分配类型化的内存,所以使用它的程序可以不必计算以字节为单位的所需内存量,它们也可以避免对 operator new 的返回值进行强制类型转换(第 5.12.4 节)。类似地,deallocate 释放特定类型的内存,也不必转换为 void*

18.1.4. Placement new Expressions

18.1.4. 定位 new 表达式

The library functions operator new and operator delete are lower-level versions of the allocator members allocate and deallocate. Each allocates but does not initialize memory.

标准库函数 operator newoperator deleteallocatorallocatedeallocate 成员的低级版本,它们都分配但不初始化内存。

There are also lower-level alternatives to the allocator members construct and destroy. These members initialize and destroy objects in space allocated by an allocator object.

allocator 的成员 constructdestroy 也有两个低级选择,这些成员在由 allocator 对象分配的空间中初始化和撤销对象。

Analogous to the construct member, there is a third kind of new expression, referred to as placement new. The placement new expression initializes an object in raw memory that was already allocated. It differs from other versions of new in that it does not allocate memory. Instead, it takes a pointer to allocated but unconstructed memory and initializes an object in that memory. In effect, placement new allows us to construct an object at a specific, preallocated memory address.

类似于 construct 成员,有第三种 new 表达式,称为定位 new。定位 new 表达式在已分配的原始内存中初始化一个对象,它与 new 的其他版本的不同之处在于,它不分配内存。相反,它接受指向已分配但未构造内存的指针,并在该内存中初始化一个对象。实际上,定位 new 表达式使我们能够在特定的、预分配的内存地址构造一个对象。

The form of a placement new expression is:

定位 new 表达式的形式是:

     new (place_address) type
     new (place_address) type (initializer-list)

where place_address must be a pointer and the initializer-list provides (a possibly empty) list of initializers to use when constructing the newly allocated object.

其中 place_address 必须是一个指针,而 initializer-list 提供了(可能为空的)初始化列表,以便在构造新分配的对象时使用。

We could use a placement new expression to replace the call to construct in our Vector implementation. The original code

可以使用定位 new 表达式代替 Vector 实现中的 construct 调用。原来的代码

     // construct a copy t in the element to which first_free points
     alloc.construct (first_free, t);

would be replaced by the equivalent placement new expression

可以用等价的定位 new 表达式代替

     // copy t into element addressed by first_free
     new (first_free) T(t);

Placement new expressions are more flexible than the construct member of class allocator. When placement new initializes an object, it can use any constructor, and builds the object directly. The construct function always uses the copy constructor.

定位 new 表达式比 allocator 类的 construct 成员更灵活。定位 new 表达式初始化一个对象的时候,它可以使用任何构造函数,并直接建立对象。construct 函数总是使用复制构造函数。

For example, we could initialize an allocated but unconstructed string from a pair of iterators in either of these two ways:

例如,可以用下面两种方式之一,从一对迭代器初始化一个已分配但未构造的 string 对象:

     allocator<string> alloc;
     string *sp = alloc.allocate(2); // allocate space to hold 2 strings
     // 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

The placement new expression uses the string constructor that takes a pair of iterators to construct the string directly in the space to which sp points. When we call construct, we must first construct the string from the iterators to get a string object to pass to construct. That function then uses the string copy constructor to copy that unnamed, temporary string into the object to which sp points.

定位 new 表达式使用了接受一对迭代器的 string 构造函数,在 sp 指向的空间直接构造 string 对象。当调用 construct 函数的时候,必须首先从迭代器构造一个 string 对象,以获得传递给 constructstring 对象,然后,该函数使用 string 的复制构造函数,将那个未命名的临时 string 对象复制到 sp 指向的对象中。

Often the difference is irrelevant: For valuelike classes, there is no observable difference between constructing the object directly in place and constructing a temporary and copying it. And the performance difference is rarely meaningful. But for some classes, using the copy constructor is either impossible (because the copy constructor is private) or should be avoided. In these cases, use of placement new may be necessary.

通常,这些区别是不相干的:对值型类而言,在适当的位置直接构造对象与构造临时对象并进行复制之间没有可观察到的区别,而且性能差别基本没有意义。但对某些类而言,使用复制构造函数是不可能的(因为复制构造函数是私有的),或者是应该避免的,在这种情况下,也许有必要使用定位 new 表达式。

Exercises Section 18.1.4

Exercise 18.4:

Why do you think construct is limited to using only the copy constructor for the element type?

你认为为什么限制 construct 函数只能使用元素类型的复制构造函数?

Exercise 18.5:

Why can placement new expressions be more flexible?

为什么定位 new 表达式更灵活?

18.1.5. Explicit Destructor Invocation

18.1.5. 显式析构函数的调用

Just as placement new is a lower-level alternative to using the allocate member of the allocator class, we can use an explicit call to a destructor as the lower-level alternative to calling destroy.

正如定位 new 表达式是使用 allocate 类的 construct 成员的低级选择,我们可以使用析构函数的显式调用作为调用 destroy 函数的低级选择。

In the version of Vector that used an allocator, we clean up each element by calling destroy:

在使用 allocator 对象的 Vector 版本中,通过调用 destroy 函数清除每个元素:

     // destroy the old elements in reverse order
     for (T *p = first_free; p != elements; /* empty */ )
         alloc.destroy(--p);

For programs that use a placement new expression to construct the object, we call the destructor explicitly:

对于使用定位 new 表达式构造对象的程序,显式调用析构函数:

     for (T *p = first_free; p != elements; /* empty */ )
         p->~T(); // call the destructor

Here we invoke a destructor directly. The arrow operator dereferences the iterator p to obtain the object to which p points. We then call the destructor, which is the name of the type preceded by a tilde (~).

在这里直接调用析构函数。箭头操作符对迭代器 p 解引用以获得 p 所指的对象,然后,调用析构函数,析构函数以类名前加 ~ 来命名。

The effect of calling the destructor explicitly is that the object itself is properly cleaned up. However, the memory in which the object resided is not freed. We can reuse the space if desired.

显式调用析构函数的效果是适当地清除对象本身。但是,并没有释放对象所占的内存,如果需要,可以重用该内存空间。

Calling the operator delete function does not run the destructor; it only frees the indicated memory.

调用 operator delete 函数不会运行析构函数,它只释放指定的内存。



Exercises Section 18.1.5

Exercise 18.6:

Reimplement your Vector class to use operator new, operator delete, placement new, and direct calls to the destructor.

重新实现 Vector 类,使用 operator new、operator delete、定位 new 表达式,并直接调用析构函数。

Exercise 18.7:

Test your new version by running the same programs that you ran against your initial Vector implementation.

运行为原来的 Vector 实现而运行的程序,测试你的新版本。

Exercise 18.8:

Which version do you think is better, and why?

你认为哪个版本更好?为什么?


18.1.6. Class Specific new and delete

18.1.6. 类特定的 newdelete

The previous subsections looked at how a class can take over memory management for its own internal data structure. Another way to optimize memory allocation involves optimizing the behavior of new expressions. As an example, consider the Queue class from Chapter 16. That class doesn't hold its elements directly. Instead, it uses new expressions to allocate objects of type QueueItem.

前几节介绍了类怎样能够接管自己的内部数据结构的内存管理,另一种优化内存分配的方法涉及优化 new 表达式的行为。作为例子,考虑第十六章Queue 类。该类不直接保存它的元素,相反,它使用 new 表达式分配 QueueItem 类型的对象。

It might be possible to improve the performance of Queue by preallocating a block of raw memory to hold QueueItem objects. When a new QueueItem object is created, it could be constructed in this preallocated space. When QueueItem objects are freed, we'd put them back in the block of preallocated objects rather than actually returning memory to the system.

通过预先分配一块原始内存以保存 QueueItem 对象,也许有可能改善 Queue 的性能。创建新 QueueItem 对象的时候,可以在这个预先分配的空间中构造对象。释放 QueueItem 对象的时候,将它们放回预先分配对象的块中,而不是将内存真正返回给系统。

The difference between this problem and our Vector implementation is that in this case, we want to optimize the behavior of new and delete expressions when applied to objects of a particular type. By default, new expressions allocate memory by calling the version of operator new that is defined by the library. A class may manage the memory used for objects of its type by defining its own members named operator new and operator delete.

这个问题与 Vector 的实现之间的区别在于,在这种情况下,我们希望在应用于特定类型的时候优化 newdelete 表达式的行为。默认情况下,new 表达式通过调用由标准库定义的 operator new 版本分配内存。通过定义自己的名为 operator newoperator delete 的成员,类可以管理用于自身类型的内存。

When the compiler sees a new or delete expression for a class type, it looks to see if the class has a member operator new or operator delete. If the class defines (or inherits) its own member new and delete functions, then those functions are used to allocate and free the memory for the object. Otherwise, the standard library versions of these functions are called.

编译器看到类类型的 newdelete 表达式的时候,它查看该类是否有 operator newoperator delete 成员,如果类定义(或继承)了自己的成员 newdelete 函数,则使用那些函数为对象分配和释放内存;否则,调用这些函数的标准库版本。

When we optimize the behavior of new and delete, we need only define new versions of the operator new and operator delete. The new and delete expressions themselves take care of constructing and destroying the objects.

优化 newdelete 的行为的时候,只需要定义 operator newoperator delete 的新版本,newdelete 表达式自己照管对象的构造和撤销。

Member new and delete Functions
成员 newdelete 函数

If a class defines either of these members, it should define both of them.

如果类定义了这两个成员中的一个,它也应该定义另一个。

A class member operator new function must have a return type of void* and take a parameter of type size_t. The function's size_t parameter is initialized by the new expression with the size, in bytes, of the amount of memory to allocate.

类成员 operator new 函数必须具有返回类型 void* 并接受 size_t 类型的形参。由 new 表达式用以字节计算的分配内存量初始化函数的 size_t 形参。

A class member operator delete function must have a void return type. It can be defined to take a single parameter of type void* or to take two parameters, a void* and a size_t. The void* parameter is initialized by the delete expression with the pointer that was deleted. That pointer might be a null pointer. If present, the size_t parameter is initialized automatically by the compiler with the size in bytes of the object addressed by the first parameter.

类成员 operator delete 函数必须具有返回类型 void。它可以定义为接受单个 void* 类型形参,也可以定义为接受两个形参,即 void*size_t 类型。由 delete 表达式用被 delete 的指针初始化 void* 形参,该指针可以是空指针。如果提供了 size_t 形参,就由编译器用第一个形参所指对象的字节大小自动初始化 size_t 形参。

The size_t parameter is unnecessary unless the class is part of an inheritance hierarchy. When we delete a pointer to a type in an inheritance hierarchy, the pointer might point to a base-class object or an object of a derived class. In general, the size of a derived-type object is larger than the size of a base-class object. If the base class has a virtual destructor (Section 15.4.4, p. 587), then the size passed to operator delete will vary depending on the dynamic type of the object to which the deleted pointer points. If the base class does not have a virtual destructor, then, as usual, the behavior of deleting a pointer to a derived object through a base-class pointer is undefined.

除非类是某继承层次的一部分,否则形参 size_t 不是必需的。当 delete 指向继承层次中类型的指针时,指针可以指向基类对象,也可以指向派生类对象。派生类对象的大小一般比基类对象大。如果基类有 virtual 析构函数(第 15.4.4 节),则传给 operator delete 的大小将根据被删除指针所指对象的动态类型而变化;如果基类没有 virtual 析构函数,那么,通过基类指针删除指向派生类对象的指针的行为,跟往常一样是未定义的。

These functions are implicitly static members (Section 12.6.1, p. 469). There is no need to declare them static explicitly, although it is legal to do so. The member new and delete functions must be static because they are used either before the object is constructed (operator new) or after it has been destroyed (operator delete). There are, therefore, no member data for these functions to manipulate. As with any other static member function, new and delete may access only static members of their class directly.

这些函数隐式地为静态函数(第 12.6.1 节),不必显式地将它们声明为 static,虽然这样做是合法的。成员 newdelete 函数必须是静态的,因为它们要么在构造对象之前使用(operator new),要么在撤销对象之后使用(operator delete),因此,这些函数没有成员数据可操纵。像任意其他静态成员函数一样,newdelete 只能直接访问所属类的静态成员。

Array Operator new[] and Operator delete[]
数组操作符 new[] 和操作符 delete[]

We can also define member operator new[] and operator delete[] to manage arrays of the class type. If these operator functions exist, the compiler uses them in place of the global versions.

也可以定义成员 operator new[]operator delete[] 来管理类类型的数组。如果这些 operator 函数存在,编译器就使用它们代替全局版本。

A class member operator new[] must have a return type of void* and take a first parameter of type size_t. The operator's size_t parameter is initialized automatically with a value that represents the number of bytes required to store an array of the given number of elements of the specified type.

类成员 operator new[] 必须具有返回类型 void*,并且接受的第一个形参类型为 size_t。用表示存储特定类型给定数目元素的数组的字节数值自动初始化操作符的 size_t 形参。

The member operator delete[] must have a void return type and a first parameter of type void*. The operator's void* parameter is initialized automatically with a value that represents the beginning of the storage in which the array is stored.

成员操作符 operator delete[] 必须具有返回类型 void,并且第一个形参为 void* 类型。用表示数组存储起始位置的值自动初始化操作符的 void* 形参。

The operator delete[] for a class may also have two parameters instead of one, the second parameter being a size_t. If present, the additional parameter is initialized automatically by the compiler with the size in bytes of the storage required to store the array.

类的操作符 delete[] 也可以有两个形参,第二个形参为 size_t。如果提供了附加形参,由编译器用数组所需存储量的字节数自动初始化这个形参。

Overriding Class-Specific Memory Allocation
覆盖类特定的内存分配

A user of a class that defines its own member new and delete can force a new or delete expression to use the global library functions through the use of the global scope resolution operator. If the user writes

如果类定义了自己的成员 newdelete,类的用户就可以通过使用全局作用域确定操作符,强制 newdelete 表达式使用全局的库函数。如果用户编写

     Type *p = ::new Type; // uses global operator new
     ::delete p;           // uses global operator delete

then new invokes the global operator new even if class Type defines its own class-specific operator new; similarly for delete.

那么,即使类定义了自己的类特定的 operator new,也调用全局的 operator newdelete 类似。

If storage was allocated with a new expression invoking the global operator new function, then the delete expression should also invoke the global operator delete function.

如果用 new 表达式调用全局 operator new 函数分配内存,则 delete 表达式也应该调用全局 operator delete 函数。

Exercises Section 18.1.6

Exercise 18.9:

Declare members new and delete for the QueueItem class.

QueueItem 类声明成员 newdelete


18.1.7. A Memory-Allocator Base Class

18.1.7. 一个内存分配器基类

Like the generic handle class (Section 16.5, p. 666) this example represents a fairly sophisticated use of C++. Understanding this example requires (and demonstrates) a good grasp of both inheritance and templates. It may be useful to delay studying this example until you are comfortable with these features.

像泛型句柄类一样(第 16.5 节),这个例子表示 C++ 的相当复杂的使用,理解这个例子需要(并认证)良好掌握地继承和模板,将对这个例子的研究推迟到你熟悉这些特征之后,也许是有用的。

Having seen how to declare class-specific member new and delete, we might next implement those members for QueueItem. Before doing so, we need to decide how we'll improve over the built-in library new and delete functions. One common strategy is to preallocate a block of raw memory to hold unconstructed objects. When new elements are created, they could be constructed in one of these preallocated objects. When elements are freed, we'd put them back in the block of preallocated objects rather than actually returning memory to the system. This kind of strategy is often known as maintaining a freelist. The freelist might be implemented as a linked list of objects that have been allocated but not constructed.

已经看过了怎样声明类特定的成员 newdelete,下面就可以为 QueueItem 类实现这些成员,在这样做之前,需要决定怎样改进内置库的 newdelete 函数。一个通用策略是预先分配一场原始内存来保存未构造的对象,创建新元素的时候,可以在一个预先分配的对象中构造;释放元素的时候,将它们放回预先分配对象的块中,而不是将内存实际返还给系统。这种策略常被称为维持一个自由列表。可以将自由列表实现为已分配但未构造的对象的链表。

Rather than implementing a freelist-based allocation strategy for QueueItem, we'll observe that QueueItem is not unique in wanting to optimize allocation of objects of its type. Many classes might have the same need. Because this behavior might be generally useful, we'll define a new class that we'll name CachedObj to handle the freelist. A class, such as QueueItem, that wants to opimize allocation of objects of its type could use the CachedObj class rather than implementing its own new and delete members directly.

除了为 QueueItem 类实现基于自由列表的分配策略,我们注意到 QueueItem 不是唯一希望优化其对象分配的类,许多类都可能有同一需要。因为这个行为也许通常是有用的,所以我们将定义一个名为 CachedObj 的新类来处理自由列表。像 QueueItem 这样希望优化其对象分配的类可以使用 CachedObj 类,而不用直接实现自己的 newdelete 成员。

The CachedObj class will have a simple interface: Its only job is to allocate and manage a freelist of allocated but unconstructed objects. This class will define a member operator new that will return the next element from the freelist, removing it from the freelist. The operator new will allocate new raw memory whenever the freelist becomes empty. The class will also define operator delete to put an element back on the freelist when an object is destroyed.

CachedObj 类有简单的接口:它的工作只是分配和管理已分配但未构造对象的自由列表。这个类将定义一个成员 operator new,返回自由列表的下一个元素,并将该元素从自由列表中删除。当自由列表为空的时候,operator new 将分配新的原始内存。这个类还定义 operator delete,在撤销对象时将元素放回自由列表。

Classes that wish to use a freelist allocation strategy for their own types will inherit from CachedObj. Through inheritance, these classes can use the CachedObj definition of operator new and operator delete along with the data members needed to represent the freelist. Because the CachedObj class is intended as a base class, we'll give it a public virtual destructor.

希望为自己的类型使用自由列表分配策略的类将继承 CachedObj 类,通过继承,这些类可以使用 CachedObj 类的 operator newoperator delete 定义,以及表示自由列表所需的数据成员。因为打算将 CachedObj 类作为基类,所以将给它一个 public 虚析构函数。

As we'll see, CachedObj may be used only for types that are not involved in an inheritance hierarchy. Unlike the member new and delete operations, CachedObj has no way to allocate different sized objects depending on the actual type of the object: Its freelist holds objects of a single size. Hence, it may be used only for classes, such as QueueItem, that do not serve as base classes.

正如我们将看到的,CachedObj 只能用于不包含在继承层次中类型。与成员 newdelete 操作不同,CachedObj 类没有办法根据对象的实际类型分配不同大小的对象:它的自由列表保存单一大小的对象。因此,它只能用于不作基类使用的类,如 QueueItem 类。

The data members defined by the CachedObj class, and inherited by its derived classes, are:

CachedObj 类定义并被它的派生类继承的数据成员是:

  • A static pointer to the head of the freelist

    指向自由列表表头的 static 指针。

  • A member named next that points from one CachedObj to the next

    名为 next、从一个 CachedObj 对象指向另一个 CachedObj 对象的指针。

The next pointer chains the elements together onto the freelist. Each type that we derive from CachedObj will contain its own type-specific data plus a single pointer inherited from the CachedObj base class. Each object has an extra pointer used by the memory allocator but not by the inherited type itself. When the object is in use, this pointer is meaningless and not used. When the object is available for use and is on the freelist, then the next pointer is used to point to the next available object.

next 指针将元素链入自由列表。从 CachedObj 类派生的每个类型都包含自己的类型特定的数据,加上一个从 CachedObj 基类继承的指针。每个对象具有由内存分配器使用但被继承类型自己不用的一个额外指针,对象在使用的时候,该指针无意义且不使用;对象可供使用并在自由列表中的时候,就使用 next 指针来指向下一个可用的对象。

If we used CachedObj to optimize allocation of our Screen class, objects of type Screen (conceptually) would look like the illustration in Figure 18.2.

如果使用 CachedObj 类来优化 Screen 类的分配,Screen 类型的对象(概念上)看起来如图 18.2 所示。

Figure 18.2. Illustration of a CachedObj Derived Class
图 18.2. CachedObj 派生类举例


The CachedObj Class
CachedObj

The only remaining question is what types to use for the pointers in CachedObj. We'd like to use the freelist approach for any type, so the class will be a template. The pointers will point to an object of the template type:

剩下的唯一问题是 CachedObj 类中的指针使用什么类型。我们希望为任意类型使用自由列表方法,所以 CachedObj 类将是一个模板,指针将指向该模板类型的对象:

     /* memory allocation class: Pre-allocates objects and
      * maintains a freelist of objects that are unused
      * When an object is freed, it is put back on the freelist
      * The memory is only returned when the program exits
      */
     template <class T> class CachedObj {
     public:
         void *operator new(std::size_t);
         void operator delete(void *, std::size_t);
         virtual ~CachedObj() { }
     protected:
         T *next;
     private:
         static void add_to_freelist(T*);
         static std::allocator<T> alloc_mem;
         static T *freeStore;
         static const std::size_t chunk;
     };

The class is quite simple. It provides only three public members: operator new, operator delete, and a virtual destructor. The new and delete members take objects off and return objects to the freelist.

这个类相当简单,它只提供三个公用成员:operator new、operator delete 和虚析构函数。newdelete 成员分别从自由列表取走对象和将对象返回到自由列表。

The static members manage the freelist. These members are declared as static because there is only one freelist maintained for all the objects of a given type. The freeStore pointer points to the head of the freelist. The member named chunk specifies the number of objects that will be allocated each time the freelist is empty. Finally, add_to_freelist puts objects on the freelist. This function is used by operator new to put newly allocated objects onto the freelist. It is also used by operator delete to put an object back on the free list when an object is deleted.

static 成员管理自由列表。这些成员声明为 static,是因为只为所有给定类型的对象维持一个自由列表。freeStore 指针指向自由列表的表头。名为 chunk 的成员指定每当自由列表为空时将分配的对象的数目。最后,add_to_freelist 函数将对象放在自由列表,operator new 使用这个函数将新分配的对象放到自由列表,删除对象的时候,operator delete 也使用该函数将对象放回自由列表。

Using CachedObj
使用 CachedObj

The only really tricky part in using CachedObj is understanding the template parameter: When we inherit from CachedObj, the template type we use to instantiate CachedObj will be the derived type itself. We inherit from CachedObj in order to reuse its freelist management. However, CachedObj holds a pointer to the object type it manages. The type of that pointer is pointer to a type derived from CachedObj.

使用 CachedObj 类,真正复杂的部分是理解模板形参:当继承 CachedObj 类的时候,用来实例化 CachedObj 类的模板类型将是派生类型本身。为了重用 CachedObj 类的自由列表管理而继承 CachedObj 类,但是,CachedObj 类保存了指向它管理的对象类型的一个指针,该指针的类型是指向 CachedObj 的派生类型的指针。

For example, to optimize memory management for our Screen class we would declare Screen as

例如,为了优化 Screen 类的内存管理,我们将 Screen 声明为:

     class Screen: public CachedObj<Screen> {
         // interface and implementation members of class Screen are unchanged
     };

This declaration gives Screen a new base class, the instance of CachedObj that is parameterized by type Screen. Each Screen now includes an additional inherited member named next in addition to its other members defined inside the Screen class.

这个声明给了 Screen 类一个新的基类:形参为 Screen 类型的 CachedObj 实例。现在除了 Screen 类内部定义的其他成员之外,每个 Screen 对象还包含附加的名为 next 的继承成员。

Because QueueItem is a template type, deriving it from CachedObj is a bit complicated:

因为 QueueItem 是一个模板类型,从 CachedObj 类派生它有点复杂:

     template <class Type>
     class QueueItem: public CachedObj< QueueItem<Type> > {
          // remainder of class declaration and all member definitions unchanged
     };

This declaration says that QueueItem is a class template that is derived from the instantiation of CachedObj that holds objects of type QueueItem<Type>. For example, if we define a Queue of ints, then the QueueItem<int> class is derived from CachedObj< QueueItem<int> >.

这个声明是说,QueueItem 是从保存 QueueItem<Type> 类型对象的 CachedObj 实例派生而来的类模板。例如,如果定义 int 值的 Queue,就从 CachedObj< QueueItem<int> > 派生 QueueItem<int> 类。

No other changes are needed in our class. QueueItem now has automatic memory allocation that uses a freelist to reduce the number of allocations required when creating new Queue elements.

我们的类不需要其他改变。现在 QueueItem 类具有自动内存分配,这个内存分配使用自由列表减少创建新的 Queue 元素时需要的分配数目。

How Allocation Works
分配怎样工作

Because we derived QueueItem from CachedObj, any allocation using a new expression, such as the call from Queue::push:

因为我们从 CachedObj 类派生 QueueItem 类,任何使用 new 表达式的分配,如 Queue::push: 中的调用:

     // allocate a new QueueItem object
     QueueItem<Type> *pt =
         new QueueItem<Type>(val);

allocates and constructs a new QueueItem. Each new expression:

分配并构造一个新的 QueueItem 对象。每个表达式

  1. Uses the QueueItem<T>::operator new function to allocate an object from the freelist

    使用 QueueItem<T>::operator new 函数从自由列表分配一个对象。

  2. Uses the element type copy constructor for type T to construct an object in that storage

    为类型 T 使用元素类型的复制构造函数,在该内存中构造一个对象。

Similarly, when we delete a QueueItem pointer such as

类似地,当像 delete pt;这样删除一个 QueueItem 指针的时候,运行 QueueItem 析构函数

     delete pt;

the QueueItem destructor is run to clean up the object to which pt points and the class operator delete is called. That operator puts the memory the element used back on the freelist.

指向的对象,并调用该类的 operator delete,将元素所用的内存放回自由列表。

Defining operator new
定义 operator new

The operator new member returns an object from the freelist. If the freelist is empty, new must first allocate a new chunk of memory:

operator new 成员从自由列表返回一个对象,如果自由列表为空,new 必须首先分配 chunk 数目的新内存:

         template <class T>
         void *CachedObj<T>::operator new(size_t sz)
         {
              // new should only be asked to build a T, not an object
              // derived from T; check that right size is requested
              if (sz != sizeof(T))
                  throw std::runtime_error
                   ("CachedObj: wrong size object in operator new");
              if (!freeStore) {
                  // the list is empty: grab a new chunk of memory
                  // allocate allocates chunk number of objects of type T
                  T * array = alloc_mem.allocate(chunk);

                  // now set the next pointers in each object in the allocated memory
                  for (size_t i = 0; i != chunk; ++i)
                        add_to_freelist(&array[i]);
              }
              T *p = freeStore;
              freeStore = freeStore->CachedObj<T>::next;
              return p;   // constructor of T will construct the T part of the object
         }

The function begins by verifying that it is being asked to allocate the right amount of space.

函数首先验证要求它分配正确数量的空间。

This check enforces our design intent that CachedObj should be used only for classes that are not base classes. The fact that CachedObj allocates objects on its freelist that have a fixed size means that it cannot be used to handle memory allocation for classes in an inheritance hierarchy. Classes related by inheritance almost always define objects of different sizes. A single allocator would have to be much more sophisticated than the one we implement here to handle such classes.

这个检查强调了我们的设计意图:CachedObj 类应该只被不是基类的类使用。CachedObj 类在固定大小的自由列表上分配对象,这一事实意味着,继承层次中的类不能使用它来处理内存分配。因继承而相关的类几乎总是定义不同大小的对象,处理这些类的单个分配器,可能必须比这里所实现的这个复杂得多。

The operator new function next checks whether there are any objects on the freelist. If not, it asks the allocator member to allocate chunk new, unconstructed objects. It then iterates through the newly allocated objects, setting the next pointer. After the call to add_to_freelist, each object on the freelist will be unconstructed, except for its next pointer, which will hold the address of the next available object. The freelist looks something like the picture in Figure 18.3.

operator new 函数接着检查自由列表中是否有对象,如果没有,它就请求 allocator 成员分配 chunk 个新的未构造对象,然后,它通过新分配的对象进行迭代,设置 next 指针。调用了 add_to_freelist 函数之后,自由列表上的每个对象除了将保存下一个可用对象的地址 next 指针之外,将是未构造的。自由列表如图 18.3 所示。

Figure 18.3. Illustration CachedObj Freelist
图 18.3. CachedObj 自由列表示例


Having ensured that there are available objects to allocate, operator new returns the address of the first object on the freelist, resetting the freeStore pointer to address the next element on the freelist. The object returned is unconstructed. Because operator new is called from a new expression, the new expression will take care of constructing the object.

在有可用对象可以分配的保证之下,operator new 返回自由列表上第一个对象的地址,将 freeStore 指针重置为指向自由列表上下一个元素。被返回的对象是未构造的。因为从 new 表达式调用 operator new,所以 new 表达式将负责构造对象。

Defining operator delete
定义 operator delete

The member operator delete is responsible only for managing the memory. The object itself was already cleaned up in the destructor, which the delete expression calls before calling operator delete. The operator delete member is trivial:

operator delete 成员只负责管理内存,在析构函数中已经清除了对象本身,delete 表达式在调用 operator delete 之前调用析构函数。operator delete 成员很简单:

     template <class T>
     void CachedObj<T>::operator delete(void *p, size_t)
     {
        if (p != 0)
            // put the "deleted" object back at head of freelist
            add_to_freelist(static_cast<T*>(p));
     }

It calls add_to_freelist to put the deleted object back onto the freelist.

它调用 add_to_freelist 成员将被删除对象放回自由列表。

The interesting part is the cast (Section 5.12.4, p. 183). operator delete is called when a dynamically allocated object of the class type is deleted. The compiler passes the address of that object to operator delete. However, the parameter type for the pointer must be void*. Before calling add_to_freelist, we have to cast the pointer from void* back to its actual type. In this case, that type is pointer to T, which in turn is a pointer to an object of a type derived from CachedObj.

令人感兴趣的部分是强制类型转换(第 5.12.4 节)。在删除动态分配的类类型对象时调用 operator delete,编译器将该对象的地址传给 operator delete。但是,指针的形参类型必须是 void*,在调用 add_to_freelist 之前,必须将指针从 void* 强制转换为它的实际类型,本例中,那个类型是指向 T 的指针,它是 CachedObj 派生类型的对象的指针。

The add_to_freelist Member
add_to_freelist 成员

The job of this member is to set the next pointer and update the freeStore pointer when an object is added to the freelist:

这个成员的任务是设置 next 指针,并且在将对象加到自由列表时更新 freeStore 指针:

     // puts object at head of the freelist
     template <class T>
     void CachedObj<T>::add_to_freelist(T *p)
     {
        p->CachedObj<T>::next = freeStore;
        freeStore = p;
     }

The only tricky part is the use of the next member. Recall that CachedObj is intended to be used as a base class. The objects that are allocated aren't of type CachedObj. Instead, those objects are of a type derived from CachedObj. The type of T, therefore, will be the derived type. The pointer p is a pointer to T, not a pointer to CachedObj. If the derived class has its own member named next, then writing

唯一复杂的部分是 next 成员的使用。回忆一下,我们打算将 CachedObj 作为基类使用,被分配对象不是 CachedObj 类型的,相反,那些对象是 CachedObj 的派生类型的,因此,类型 T 将是派生类型,指针 p 是指向 T 的指针,不是指向 CachedObj 的指针。如果派生类型有自己的名为 next 的成员,则编写

     p->next

would fetch the next member of the derived class! But we want to set the next in the base, CachedObj class.

将获得派生类的成员!但我们希望在基类——CachedObj 类中设置 next

To avoid any possible collision with a member defined in the derived class, we explicitly specify that we are assigning to the base class member next.

为了避免任何与派生类中定义的成员可能的冲突,显式指定我们正在给基类成员 next 赋值。

Defining the Static Data Members
定义静态数据成员

What remains is to define the static data members:

剩下的是定义静态数据成员:

     template <class T> allocator< T > CachedObj< T >::alloc_mem;
     template <class T> T *CachedObj< T >::freeStore = 0;
     template <class T> const size_t CachedObj< T >::chunk = 24;

As usual, with static members of a class template there is a different static member for each type used to instantiate CachedObj. We initialize chunk to an arbitrary valuein this case, 24. We initialize the freeStore pointer to 0, indicating that the freelist starts out empty. There is no initialization required for the alloc_mem member, but we do have to remember to define it.

照常,对于类模板的静态成员,每个类型使用不同的静态成员来实例化 CachedObj 类。将 chunk 初始化为任意值,本例中为 24。将 freeStore 指针初始化为 0,指出自由列表开始时为空。alloc_mem 成员不需要初始化,但必须记得定义它。

Exercises Section 18.1.7

Exercise 18.10:

Explain each of the following initializations. Indicate if any are errors, and if so, why.

解释下面每个初始化,指出是否有错误的,如果有,为什么错。

     class iStack {
     public:
         iStack(int capacity): stack(capacity), top(0) { }
     private:
         int top;
         vector<int> stack;
     };
     (a) iStack *ps = new iStack(20);
     (b) iStack *ps2 = new const iStack(15);
     (c) iStack *ps3 = new iStack[ 100 ];

Exercise 18.11:

Explain what happens in the following new and delete expressions:

解释下面 newdelete 表达式中发生什么。

     struct Exercise {
         Exercise();
         ~Exercise();
     };
     Exercise *pe = new Exercise[20];
     delete[] pe;

Exercise 18.12:

Implement a class-specific memory allocator for Queue or another class of your choice. Measure the change in performace to see how much it helps, if at all.

Queue 类或你选择的其他类实现一个类特定的内存分配器。测量性能的改变,看看到底有多大帮助。

Team LiB
Previous Section Next Section