Dynamic Memory

In today's lecture, we'll consider the lifetimes we need for different objects in our program, how lifetime corresponds to the way objects are managed in memory, and how we as the programmer can use a new technique - dynamic memory to have more precise control of all this when we need it.

Updated Fall 2024

1: Memory Model and The Heap

When we define a variable in C++, the lifetime of the corresponding object and the memory it uses at runtime are all managed automatically for us. This specifically applies for both global and local variables, although in different ways.

To start, consider the Bird class and code in main() below. Bird defines a custom constructor (Bird()) and destructor (~Bird()) that print out a message when they run. Recall that constructors and destructors are special functions that run at the start/end of an object's lifetime…that means thinking about when these functions run (and print their messages!) is a helpful exercise in thinking precisely about their lifetimes in our code.

Take a moment to mentally trace through the code and predict what you think will be printed. You can use the simulation to check.


In the video below, I'll walk through the example above and introduce dynamic memory, which allows direct control over object lifetimes.


To recap:

  • Global variables correspond to objects with static storage duration - their lifetime that extends throughout the entire program.
  • Local variables correspond to objects with automatic storage duration - their lifetime is limited to the block (i.e. a set of { }) in which they are defined and are automatically cleaned up when they go out of scope.
  • Dynamically allocated objects are stored in a separate section of memory called the heap. The lifetime of such objects is controlled directly by the new and delete expressions in the code we write (and not automatically managed).


2: Using new and delete
2.1 Not Started

The general workflow for using dynamic memory is something like this:

  1. Use the new operator to create a dynamically allocated object on the heap.
  2. Keep track of that object's address using a pointer. The object isn't restricted to any scope and can be used throughout our program.
  3. Once we're finished with the object, give its address (i.e. the pointer we're using to track it) to the delete operator, which destroys the object and frees its memory to be used for something else.

Here's the details:


2.1 Exercise: Dynamic Object Lifetimes

Let's add dynamic memory with new and delete to another example like the warm up exercise from earlier. Here, we're working with a Mole class rather than Bird, since the objects popping in and out of existence in dynamic memory remind me of the old "Whac-a-Mole" arcade game.

For each commented line in the main() function below, write in the blank the number of Mole objects are currently alive in memory (i.e. their constructor has run, but their destructor has not run yet).

class Mole {
public:
  Mole(int id_in)
    : id(id_in) {
    cout << "Mole ctor: " << id << endl;
  }

  ~Mole() {
    cout << "Mole dtor: " << id << endl;
  }

private:
  int id;
};

Mole * func() {
  Mole m(123);
  return new Mole(456);
}
int main() {
  Mole m1(1);
  Mole *mPtr;
  // Line 1 
  mPtr = func();
  // Line 2 
  delete mPtr;
  // Line 3 
  mPtr = new Mole(2);
  func();
  // Line 4 
  delete mPtr;
  // Line 5 
  cout << "all done!" << endl;
}
// Line 6 - after main returns 

Sample solution… Line 1
1 Mole alive. Only m1 exists at this point. Note that mPtr is not an actual Mole, it's just a pointer.

Line 2
2 Moles alive. The func() function created a local Mole (with ID 123), but it is destroyed when the function returns. However, it also allocated a new Mole (with ID 456) on the heap, which persists and is now pointed to by mPtr.

Line 3
1 Mole alive. We've just deleted the Mole with ID 456.

Line 4
3 Moles alive. We've reassigned mPtr to point at a new dynamically allocated Mole (with ID 2). We also called func() again, which has the side effect of creating another 456 Mole on the heap. However, we don't do anything with the returned address from func(), and that is ultimately going to cause problems - there's now no way to ever clean up that Mole, since we didn't store its address anywhere.

Line 5
2 Moles alive. We've just deleted the Mole with ID 2.

Line 6
1 Mole alive. The local variable m1 went out of scope at the end of main and was destroyed. The 456 Mole on the heap was never freed. :(




3: Memory Leaks
3.1 Not Started

A memory leak occurs if we allocate an object with new but neglect to release it with delete. Why is that problematic? And, how do memory leaks generally show up in code? Let's see…


3.1 Exercise: Memory Leaks

Which of the following programs run out of memory and crash? Assume the program has 8KB of stack space and 4MB of heap space. Assume each int takes up 4 bytes. If the program runs out of memory and crashes, write "crash" in the box. If the program runs successfully, write "ok" in the box. Additionally, justify your answer with a desciption of the memory use of the program.

int main() {
  int *ptr;
  for (int i = 0; i < 1000000000; ++i) {
    ptr = new int(i);
  }
  delete ptr;
}
int main() {
  int x = 10000;
  for (int i = 0; i < 10000; ++i) {
    x = i;
  }
}
int main() {
  int arr[10000];
  for (int i = 0; i < 10000; ++i) {
    arr[i] = i;
  }
}
void helper() {
  int *ptr = new int(10);
  ptr = new int(20);
  delete ptr;
}

int main() {
  for (int i = 0; i < 1000000000; ++i) {
    helper();
  }
}
int main() {
  int *arr = new int[10000];
  for (int i = 0; i < 10000; ++i) {
    arr[i] = i;
  }
  delete[] arr;
}


You're welcome to check your solution with this walkthrough video:




4: Double Frees and Improper Deletes
4.1 Not Started

While we have to make sure we clean up all the memory that we create with new by cleaning it up using delete, we also have to watch out for a few potential errors:

  • Deleting an object twice usually results in a crash.
  • Deleting a non-heap object usually results in a crash.


One minor fact that's not covered in the video - if a null pointer is given to delete, nothing happens (i.e. it doesn't crash or do anything bad!). This is reasonable, if we consider that the behavior of delete could be specified as "destroy the object (if any) that this pointer points to" and that a null pointer indicates that a pointer "isn't pointing at anything right now".

4.1 Exercise: Double Frees and Improper Deletes

Which of the following programs run out of memory and crash? Assume the program has 8KB of stack space and 4MB of heap space. Assume each int takes up 4 bytes. Describe the memory use of the program and any problems in the box provided.

Which of the following programs will likely crash due to one of the two delete errors mentioned above? If the program contains such an error, write "crash" in the box and a description of the problem. If the program runs successfully, write "ok" in the box.

int main() {
  int *ptr1 = new int(1);
  delete ptr1;
  ptr1 = new int(2);
  delete ptr1;
}
int main() {
  int *ptr1 = new int(1);
  ptr1 = new int(2);
  delete ptr1;
  delete ptr1;
}
int main() {
  int x = 0;
  int *ptr1 = &x;
  delete ptr1;
}
int main() {
  int *ptr1 = new int(1);
  delete &ptr1;
}
int main() {
  int *ptr1 = new int(1);
  int *ptr2 = ptr1;
  delete ptr1;
  delete ptr2;
}
int main() {
  int *ptr;
  for (int i = 0; i < 10; ++i) {
    ptr = new int(i);
  }
  for (int i = 0; i < 10; ++i) {
    delete ptr;
  }
}


You're welcome to check your solution with this walkthrough video:




5: Dangling Pointers

We've encountered dangling pointers before, but only in relatively unlikely contexts, like returning the address of a local variable. For example:

int * func() {
  int x = 2;
  return &x; // bad idea!
}

int main() {
  int *ptr = func();
  // ptr ends up pointing at the dead object left after x went out of scope
}

However, dangling pointers naturally arise any time we delete dynamic memory, and we have to be careful not to accidentally use them. This turns out to be somewhat complex.




6: Uses for Dynamic Memory

Finally, let's briefly discuss some of the many uses of dynamic memory.


You've reached the end of this lecture! Your work on any exercises will be saved if you re-open this page in the same web browser.

Participation Credit
Make sure to sign in to the page, complete each of the exercises, and double check the participation indicator at the top left of this page to ensure you've earned credit.