c++ primer笔记24: Optimizing Memory Allocation

1. operator new and operator delete Functions

We have learned how to use the allocate class. Now we are using the more primitive library facilities. 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. When we use a delete expression to delete a dynamically allocated object: 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.

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. 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.

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

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

T* newelements = alloc.allocate(newcapacity);
// which could be rewritten as
T* newelements = static_cast<T*>(operator new[](newcapacity * sizeof(T)));

alloc.deallocate(elements, end - elements);
// which could be rewritten as
operator delete[](elements);

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

2. Placement new Expressions

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. 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. Placement new allows us to construct an object at a specific, preallocated memory address. The form of a placement new expression is:

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

alloc.construct(first_free, t);
// would be replaced by the equivalent placement new expression
// 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.

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.

3. Explicit Destructor Invocation

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

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.

4. Class Specific new and delete

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.

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.

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.

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.

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. 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. 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 delete d. 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.

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, 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.

These functions are implicitly static members. 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.

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.

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.

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.

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.

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

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

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.