Week 11

Memory Management

Although Java uses the new operator, just like C++, it has no corresponding delete operator. That's because in Java, when you run your program, a set of smaller programs run at the same time, scouring the heap, looking for unused objects, and automatically freeing them. This automatic memory management in Java is called garbage collection. Different types of Java garbage collectors.

In contrast, C++ manual memory management, using raw pointers with new and delete, requires an almost superhuman attention to detail. Mistakes will cause your program to leak memory, or, to corrupt the memory manager itself.

Let's look at the three most common pitfalls that accompany manual memory management: the memory leak, the dangling pointer, and the double delete. Then, you'll meet C++11's new smart pointers, which ameliorate many of the failings of pure manual memory management.

Week 11

Memory Leaks

The delete operator frees the allocated memory on the heap, but it does not physically change the pointer in any way. This leads to three possible errors. Here is one:

bool validDate(int yr, int mo, int da)
{
    Date *pd = new Date(yr, mo, da);
    if (! pd->isValid()) { return false;} 
    cout << "Date " << (*pd) << " OK" << endl;
    delete pd;  // free heap memory
    return true;
}

This function constructs a new Date object on the heap, and then calls the isValid() member function to see if the resulting combination is legal. If it is not valid, the function returns false. If it is valid, then the function prints the Date object, deletes the object on the heap and returns true.

Illustrating a leak.

This code has one new and one matching delete, but it still has a memory leak whenever an invalid data is entered. That's because there are two return statements in the function. If the Date is invalid, the function returns without deleting the data on the heap.

Try running the program oneline.

Week 11

Dangling Pointers

A dangling pointer is a pointer that contains an address, but the address points to data you have already deleted. Here's a function for a rather strange contest:

bool hasWon(int y, int m, int d)
{
    Date *pd = new Date(y, m, d);
    delete pd;  // avoid leaking
    return pd->isValid() && pd->year() % pd->day() == 0;
}

The Date object allocated on the heap is deleted before the function returns. But, after the Date has been deleted, the pointer is used to call three functions. At this point, pd is a dangling pointer, which is a pointer to heap memory that you no longer "own".

The insidious thing is that the function will do exactly what you want, but only under some circumstances. If you run the link, you'll see that since the Date on the heap hasn't changed, when you call the functions they still work correctly. This is similar to checking out of a hotel, but keeping a copy of the key. You may be able to stay another night for free, if the place isn't too busy, and no one catches you, but you might get into trouble.

Of course, the code is still illegal and won't work at all on some platforms. For instance, if you run the same code on Codespaces, you'll see it doesn't produce the right answer. However, it also doesn't provide you with any indication that you've made a mistake.

To help you find your mistakes, you can set the pointer to nullptr every time you delete the pointer. Here's a template function you can use in place of delete that will do that for you.

template <typename T>
void delete_raw(T& p){ 
  delete p; 
  p = nullptr;
}
Week 11

Double Deletes

When you discover a memory leak, your first inclination is to start adding delete statements. That can create more problems. Here is an example:

void checkDate(int yr, int mo, int da)
{
    Date *pd = new Date(yr, mo, da);
    if (! pd->isValid()) delete pd;     // avoid leak
    else cout << (*pd) << " is OK" << endl;
    delete pd;  // OOPS, a double delete
}

Here, the programmer adds a delete for both the true and false branches (unlike the memory leak example, where an invalid Data was never deleted. However, because the programmer forgot to add braces around the two statements in the else part, the final delete is a double delete: deleting an already freed pointer whenever the Date is invalid.

This is also a big no-no. This should always result in a runtime error, because this is almost certain to corrupt the heap. Technically, however, it results in undefined behavior.

Week 11

Smart Pointers

C++ manages memory with two operators: new, which allocates an object in dynamic memory and returns a raw pointer to the object; and delete, which takes a raw pointer to a dynamic object, destroys that object, and frees the associated memory (without changing the pointer at all).

To make this easier (and safer) to use, the new C++ library provides two smart pointer types, defined in the <memory> header, that manage dynamic objects. A smart pointer automatically deletes the object to which it points, when the smart pointer is destroyed.

The two smart pointers are:

  • shared_ptr, which allows multiple pointers to refer to the same object (much like raw pointers do).
  • unique_ptr, which “owns” the object to which it points

Smart pointers do not replace raw pointers, except when dealing with objects on the heap. For stack-based variables, and for by-address functions, you will continue to use raw pointers.

Week 11

Shared Pointers

There are two ways to create shared_ptr objects: with new and using the make_shared() function. Here's an example of each:

shared_ptr<int> shared_1(new int(2));       // using new
auto shared_2 = make_shared<int>(3);        // using make_shared

Click this link to visualize a short program that uses shared pointers. Click the Next> button and watch what the code does. Here's a commentary:

  • Allocate a new int on the heap and assign it to raw. (Line 7)
  • Create a shared_ptr<int>; initialize it by using the new operator. (Line 8)
  • Print out the "use count" for the shared pointer (Line 9)
  • Create a block and then "Copy" a shared_ptr; both shared_1 and copied both "point to" the same int on the heap. (Line 11)
  • Print out the "use count" for shared_1 inside the block (Line 12). Note that there are now 2 pointers sharing the one object on the heap.
  • After the inner block ends, the copied pointer goes out of scope, so when we print the "use count" for shared_1 again, there is now only one pointer pointing to the value on the heap.

The variables shared_1 and copied, both encapsulate a raw pointer which you don't "see" in the Visualizer. In addition, both variables contain additional "plumbing". This includes a reference count which keeps track of which pointers are pointing to this memory. This allows the smart pointer to know that this heap variable is still in use, so it won't automatically be deleted.

When the end of each block is reached, in the Visualizer you'll see the current-line jump back to where the smart pointer was declared, and then, the reference count will be decremented. Once the reference count for each shared pointer reaches 0, the memory is automatically deleted.

The raw pointer is not automatically deleted. It goes out of scope at the end of the block, so that memory is leaked and cannot be reclaimed elsewhere in the program.

Week 11

Unique Pointers

A unique_ptrowns” the object to which it points. Only one unique_ptr at a time can point to a given object. The object to which a unique_ptr points is destroyed when the unique_ptr is destroyed.

Define a unique_ptr by constructing it with an address returned from new, like this:

unique_ptr<int> p(new int45);

The unique_ptr does not support assignment. The following code is illegal:

unique_ptr<Cat> p1(new Cat("Felix"));
unique_ptr<Cat> p2 = p1;      //ERROR. Only one can point to Felix

Transferring Ownership

While you cannot copy unique pointers, you can transfer ownership of a heap object from one unique_ptr to another by calling release() and/or reset():

  • release() returns the "raw" pointer stored inside the unique_ptr and makes that unique_ptr null.
  • reset() takes an optional raw pointer and repositions the unique_ptr to point to the given pointer. If the unique_ptr is not null, then the object to which the unique_ptr had pointed is deleted.
unique_ptr<Cat> p1(new Cat("Felix"));
unique_ptr<Cat> p2(p1.release());     //OK, p2 now owns Felix
p2.reset(new Cat("Kitty"));           //OK. Felix deleted

The pointer returned by release() is often used to initialize another smart pointer; in this way, responsibility for memory is transferred from one smart pointer to another.

Week 11

Smart Pointers & Functions

When you write functions, generally, the parameter types should be raw pointers, not smart pointers. That is because smart pointers only work with memory on the heap, and you generally want to write functions that work with both stack and heap-based variables. For instance, imagine that you have a Cat structure, and you want to write a function that makes a Cat dance. The best way is to write the function like this:

void dance(Cat * p);

Now, if you have a Cat object on the stack, you can call the function like this:

Cat bill; dance(& bill);

To call the function with a smart pointer however, you have to retrieve the raw pointer using the get() function like this:

unique_ptr<Cat> cp(new Cat("Bill"));
dance(cp.get());

Returning Unique Pointers

You can copy or assign a unique_ptr that is about to be destroyed! This means that we can write functions which return unique pointers.

unique_ptr<int> clone(int p)
{
    // Explicitly create a unique_ptr<int> from int
    return unique_ptr<int>(new int(p));
}

Alternatively, you can also return a copy of a local unique_ptr:

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

In both cases, the compiler knows that the object being returned is about to be destroyed. In such cases, the compiler does a special kind of “copy” called a move.

Week 11

Unique Pointers and Containers

Library containers (such as vector) are designed to hold copies of items, using the regular assignment operators. Suppose, for instance, that you are writing a video game, and you have a Sprite class.

vector<unique_ptr<Sprite>> v;
unique_ptr<Sprite> sp(new Sprite);
v.push_back(sp);

If you write this code you will get an altogether baffling error message that appears to say that there are bugs in the standard library.

What the error message means is that you cannot use push_back() on a unique_ptr because there cannot be two copies of the same unique_ptr. Instead, use the standard function move() to explicitly transfer ownership, like this:

unique_ptr<Sprite> sp(new Sprite);
v.push_back(move(sp));
Week 11

Unique Pointers and Dynamic Arrays

One version of unique_ptr can manage arrays allocated by new. To manage a dynamic array, include a pair of empty brackets after the object type:

// up points to the first of ten uninitialized ints
unique_ptr<int[]> up(new int[10]);

The brackets in the type specifier (<int[]>) say that up points not to an int but to an array of int. Because up points to an array, when up destroys the pointer it manages, it will automatically use delete[] rather than delete.

Resizing a Dynamic Array

To resize a dynamic array, you need to:

  1. Allocate a new, larger array on the heap
  2. Copy the elements from the old array to the new
  3. Free the old memory and assign the new memory to the original pointer.

Here's a fragment of code that does this:

int capacity = 5;
int size = 0;
unique_ptr<int > array(new int[capacity]);
while (cin >> n){ 
   if (size == capacity) { // let's resize;
      capacity *= 2;
      unique_ptr<int> temp(new int[capacity]);
      for (int i = 0; i < size; ++i){ 
         temp[i] = array[i];
     } 
      array.reset(temp.release());
  } 
   array[size] = n;
   size++;
}