Chapter 12. Dynamic Memory

Our programs have used only static or stack memory. Static memory is used for local static objects, for class static data members, and for variables defined outside any function. Stack memory is used for non-static objects defined inside functions. Objects allocated in static or stack memory are automatically created and destroyed by the compiler. Stack objects exist only while the block in which they are defined is executing; static objects are allocated before they are used, and they are destroyed when the program ends.

In addition to static or stack memory, every program also has a pool of memory that it can use. This memory is referred to as the free store or heap. Programs use the heap for objects that they dynamically allocate, this is for objects that the program allocates at run time.

12.1 Dynamic Memory and Smart Pointers

In C++, dynamic memory is managed through a pair of operators: new, which allocates, and optionally initialises, an object in dynamic memory and returns a pointer to that object; and delete, which takes a pointer to a dynamic object, destroys that object, and frees the associated memory.

Dynamic memory is problematic because it is surprisingly hard to ensure that we free memory at the right time.

To make using dynamic memory easier, the new library provides two smart pointer types that manage dynamic objects. A smart pointer acts like a regular pointer with the important exception that it automatically deletes the object to which it points.

  • shared_ptr, which allows multiple pointers to refer to the same object.

  • unique_ptr, which "owns" the object to which it points.

  • weak_ptr, which is a weak reference to an object managed by a shared_ptr.

All three are defined in the memory header.

12.1.1 The shared_ptr Class

shared_ptr<string> p1;

A default initialised smart pointer holds a null pointer.

The make_shared function will allocate and initialise an object in dynamic memory and returns a shared_ptr that points to that object.

// shared_ptr that points to an int with value 42
shared_ptr<int> p2 = make_shared<int>(42);
// points to a string with value 9999999999
shared_ptr<string> p3 = make_shared<string>(10, '9');

When we copy or assign a shared_ptr, each shared_ptr keeps track of how many other shared_ptrs point to the same object. We can think of a shared_ptr as if it has an associated counter, usually referred to as a reference count. Whenever we copy a shared_ptr, the count is incremented.

The counter is decremented when we assign a new value to the shared_ptr and when the shared_ptr itself is destroyed. Once a shared_ptr's counter goes to zero, the shared_ptr automatically frees the object that it manages.

Operations Common to shared_ptr and unique_ptr

Description

shared_ptr<T> sp;

Null smart pointer that can point to objects of type T.

unique_ptr<T> up;

p

Use p as a condition; true if p points to an object.

*p

Dereference p to get the object to which p points.

p->mem;

Synonym for (*p).mem.

p.get();

Returns the pointer in p.

swap(p, q);

Swaps the pointers in p and q.

p.swap(q);

It does so through another special member function known as a destructor. Analogous to its constructors, each class has a destructor, the destructor controls what happens when objects of that class type are destroyed.

Operations Specific to shared_ptr

Description

make_shared<T>(args);

Returns a shared_ptr pointing to a dynamically allocated object type T.

shared_ptr<T> p(q);

p is a copy of the shared_ptr q.

p = q;

p and q are shared_ptrs holding pointers that can be converted to one another.

p.unique();

Returns true if p.use_count() is one; false otherwise.

p.use_count();

Returns the number of objects sharing with p.

Programs tend to use dynamic memory for one of three purposes:

  • They don't know how many objects they'll need.

  • They don't know the precise type of the objects they need.

  • They want to share data between several objects.

vector<string> v1; // empty vector
{
    vector<string> v2 = {"a", "an", "the"};
    v1 = v2; // copies the element from v2 into v1
}

The elements allocated by a vector exist only while the vector itself exits. When a vector is destroyed, the elements in the vector are also destroyed.

Unlike the containers, we want Blob objects that are copies of one another to share the same elements. That is, when we copy a Blob, the original and the copy should refer to the same underlying elements.

Blob<string> b1; // empty Blob
{
    Blob<string> b2 = {"a", "an", "the"};
    b1 = b2; // b1 and b2 share the same element.
}

12.1.2 Managing Memory Directly

The new returns a pointer to the object it allocates:

int *pi = new int;    // default initialised, *pi is undefined.
int *p1 = new int(); // value initialised to 0

When we provide an initialiser inside parentheses, we can use auto to deduce the type to allocate, we can use auto only with a single initialiser inside parentheses:

auto p1 = new auto(obj);
auto p2 = new auto{a, b, c}; // error: must use parentheses for the initialiser

It is legal to use new to allocate const object, a dynamically allocated const object must be initialised.

const int *pci = new const int(1024);

Once a program has used all of its available memory, new expressions will fail. By default, if new is unable to allocate the requested storage, it throws an exception of type bad_alloc. We can prevent new from throwing an exception by using a different form of new:

int *p1 = new int;
int *p2 = new (nothrow) int;

A delete expression takes a pointer to the object we want to free:

delete p;

Deleting a pointer a memory that was not allocated by new, or deleting the same pointer value more than once, is undefined. The compiler will generate an error for the delete of i because it knows that i is not a pointer.

Functions that return pointers to dynamic memory put a burden on their callers, the caller must remember to delete the memory:

Foo* factory(T arg) {
    return new Foo(arg);
}

void use_factory(T arg) {
    Foo *p = factory(arg);
    // use p but do not delete it
}

After the delete, the pointer becomes what is referred to as a dangling pointer. A dangling pointer is one that refers to memory that once held an object but no longer does so.

12.1.3 Using shared_ptrs with new

We can also initialise a smart pointer from a pointer returned by new:

shared_ptr<int> p2(new int(42));

The smart pointer constructors that take pointers are explicit.

shared_ptr<int> p1 = new int(1024);    // error: must use direct initialisation
shared_ptr<int> p2(new int(1024));

Other Ways to Define and Change shared_ptrs

Description

shared_ptr<T> p(q);

p manages the object to which the built-in pointer q points.

shared_ptr<T> p(u);

p assumes ownership from the unique_ptr u.

shared_ptr<T> p(q, d);

p assumes ownership for the object to which the built-in pointer q points; p will uses the callable object d in place of delete.

shared_ptr<T> p(p2, d);

p is a copy of the shared_ptr p2; p will uses the callable object d in place of delete.

p.reset();

If p is the only shared_ptr pointing at its object, reset frees p's existing object.

p.reset(q);

p.reset(q, d);

The smart pointer types define a function named get that returns a built-in pointer to the object that the smart pointer is managing.

12.1.4 Smart Pointers and Exception

When we use a smart pointer, the smart pointer class ensures that memory is freed when it is no longer needed even if the block is exited prematurely.

When we create a shared_ptr, we can pass an optional argument that points to a deleter function:

shared_ptr<connection> p(&c, end_connection);

12.1.5 unique_ptr

A unique_ptr "owns" the object to which it points. The object to which a unique_ptr points is destroyed when the unique_ptr is destroyed.

Because a unique_ptr owns the object to which it points, unique_ptr does not support ordinary copy or assignment:

unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<strint> p2(p1);    // error: no copy
p3 = p2;    // error: no assign

unique_ptr Operations

Description

unique_ptr<T> u1;

Null unique_ptrs that can point to objects of type T.

unique_ptr<T, D> u2;

unique_ptr<T, D> u(d);

Null unique_ptr that point to objects of type T that uses d, which must be an object type D in place of delete.

u = nullptr;

Deletes the object to which u points.

u.release();

Relinquishes control of the pointer u had held; return the pointer u had held and makes u null.

u.reset();

Deletes the object to which u points.

u.reset(q);

If the built-in pointer q is supplied, make u point to that object.

u.reset(nullptr);

Although we can't copy or assign a unique_ptr, we can transfer ownership from one unique_ptr to another by calling release or reset:

unique_ptr<string> p2(p1.release());
unique_ptr<string> p3(new string("Trex"));
p2.reset(p3.release());
p2.release(); // WRONG: p2 won't free the memory and we've lost the pointer.

There is one exception to the rule that we cannot copy a unique_ptr: We can copy or assign a unique_ptr that is about to be destroyed. The most common example is when we return a unique_ptr from a function:

unique_ptr<int> clone(int p) {
    return unique_ptr<int>(new int(p));
}

unique_ptr<int> clone(int p) {
    unique_ptr<int> ret(new int(p));
    ...
    return ret;
}

Overriding the deleter in a unique_ptr affects the unique_ptr type as well as how we construct objects of that type. We must supply the deleter type inside the angle brackets along with the type to which the unique_ptr can point.

// p points to an object of type objT
//   and uses an object of type delT to free that objct
//   it will call an  object named fcn of type delT.
unique_ptr<objT, delT> p(new objT, fcn);

12.1.6 weak_ptr

A weak_ptr is a smart pointer that does not control the lifetime of the object to which it points. Instead, a weak_ptr points to an object that is managed by a shared_ptr. Binding a weak_ptr to a shared_ptr does not change the reference count of the shared_ptr.

week_ptrs

Description

weak_ptr<T> w;

Null weak_ptr that can point at objects of type T.

weak_ptr<T> w(sp);

weak_ptr that points to the same object as the shared_ptr sp.

w = p;

p can be a shared_ptr or a weak_ptr.

w.reset();

Makes w null.

w.use_count();

The number of shared_ptrs that share ownership with w.

w.expired();

Returns true if w.use_count() is zero, false otherwise.

w.lock();

If expired is true, return a null shared_ptr; otherwise returns a shared_ptr to the object to which w points.

12.2 Dynamic Arrays

The library includes a template class named allocator that lets us separate allocation from initialisation.

12.2.1 new and Arrays

int *pia = new int[get_size()];

The size inside the brackets must have integral type but need not be a constant.

Under the new standard, we can also provide a braced list of element initialisers:

int *pia3 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

It is legal to dynamically allocate an empty array:

char arr[0];    // error: cannot define a zero-length array.
char *cp = new char[0]; // ok: but cp can't be dereferenced.

To free a dynamic array, we use a special form of delete that includes an empty pair of square brackets:

delete []pa;

The library provides a version of unique_ptr that can manage arrays allocated via new.

unique_ptr<int[]> up(new int[10]);
up.release();

unique_ptrs to Arrays

Description

unique_ptr<T[]> u;

u can point to a dynamically allocated array of the type T.

unique_ptr<T[]> u(p);

u points to the dynamically allocated array to which the built-in point p points.

u[i]

Returns the object at position i in the array that u owns.

12.2.2 The allocator Class

The library allocator class, which is defined in the memory header, lets us separate allocation from construction.

allocator<stirng> alloc;
auto const p = alloc.allocate(n); // allocate n unconstructred strings.

Standard allocator Class and Customised Algorithm

Description

allocator<T> a;

Defines an allocator object named a that can allocate memory for objects of type t.

a.allocate(n);

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

a.deallocate(p, n);

Deallocates memory that held n objects of type T starting at the address in the T* pointer p.

a.construct(p, args);

p must be a pointer to type T that points to raw memory; arg are passed to a constructor for type T, which is used to construct an object in the memory pointed to by p.

a.destroy(p);

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

The memory an allocator allocates is unconstructed.

allocator Algorithms

Description

uninitialised_copy(b, e, b2);

Copies elements from the input range denoted by iterators b and e into unconstructed, raw memory denoted by the iterator b2.

uninitialised_copy_n(b, n, b2);

Copies n elements starting from the one denoted by the iterator b into raw memory starting at b2.

uninitialised_fill(b, e, t);

Constructs objects in the range of raw memory denoted by iterators b and e as a copy of t.

uninitialised_fill_n(b, n, t);

Constructs unsigned number n objects starting at b.

auto p = alloc.allocate(vi.size() * 2);
auto q = uninitialised_copy(vi.begin(), vi.end(), p);
uninitialised_fill_n(q, vi.size(), 42);

Last updated

Was this helpful?