Search

10.1 — Introduction to dynamic memory allocation

In Chapter 6, we talked about the storage duration of a variable, which governs when the variable is created and destroyed.

Global and static variables have static duration, which means they are created when the program starts, and destroyed when it ends.

Function parameters and local variables have automatic duration, which means they are created at the point of definition and destroyed at the end of the block they are defined in.

Objects with static duration and automatic duration have two things in common:

  • The size of the object must be known at compile time.
  • Memory allocation and deallocation happens automatically (when the object is instantiated / destroyed).

Most of the time, this is just fine. However, you will come across situations where one or both of these constraints cause problems. For example, we may be creating a game, with a number of monsters that changes over time as some monsters die and new ones are spawned. Or if we’re creating a text editor that can read in a text file from disk, we won’t know in advance how many characters the text file contains.

If we have to declare the size of everything at compile time, the best we can do is try to guess the maximum size of variables we’ll need and hope that’s enough:

This is a poor solution for at least four reasons:

First, it leads to wasted memory if the variables aren’t actually used. For example, if we allocate 40 monsters for every game, but on average there are only 12 monsters, we’re using over three times more memory than we really need. Or if the text file only contains 100 chars, we have 29,900 unused characters.

Second, how do we tell which bits of memory are actually used? For strings, it’s easy: a string that starts with a \0 is clearly not being used. But what about monster[24]? Does it exist right now or is it just a placeholder for a monster to be created in the future? That necessitates having some way to tell active from inactive items, which adds complexity and can use up additional memory.

Third, most normal variables (including fixed arrays) are allocated in a portion of memory called the stack. The amount of stack memory for a program is generally quite small -- Visual Studio defaults the stack size to 1MB. If you exceed this number, stack overflow will result, and the operating system will probably close down the program.

On Visual Studio, you can see this happen when running this program:

Being limited to just 1MB of memory would be problematic for many programs, especially those that deal with graphics.

Fourth, and most importantly, it can lead to artificial limitations and/or array overflows. What happens when we’ve set our text editor to hold 30,000 chars but the user tries to read in a file that contains 100,000 chars? If we handle the case properly, we’ll likely error out, and maybe load in the first 30,000 characters only. If we don’t handle the case properly, we’ll try to load 100,000 chars into a 30,000 char array, which will overflow the array and overwrite/corrupt the next 70,000 characters of data in sequential memory.

Fortunately, we can work around these problems by using dynamic memory allocation.

Dynamic memory allocation

Dynamic memory allocation refers to the process of requesting memory from the operating system while the program is running.

Your computer has memory that is available for applications to use. When you run an application, your operating system loads the application into some of that memory. This memory used by your application is divided into different areas (called segments), each of which serves a different purpose. One segment contains your compiled code. Another segment (called the stack) is used for normal operations (keeping track of which functions were called, creating and destroying global and local variables, etc…). However, most of the memory available on a machine just sits there, waiting to be handed out to programs that request it.

When you dynamically allocate memory, you’re asking the operating system to reserve some of that memory for your program’s use. This memory does not come from the program’s limited stack memory -- instead, it is allocated from a much larger pool of memory managed by the operating system called the heap. On modern machines, the heap can be gigabytes in size. If the operating system can fulfill this request, it will return the address of that memory to your application. From that point forward, your application can use this memory as it wishes.

Unlike static and automatic memory allocation, which happen automatically, dynamic memory allocation only happens when the programmer requests it.

Objects allocated dynamically have at least four key advantages:

  1. We can dynamically allocate objects only when they are needed, so we don’t pay the memory cost for objects we don’t need.
  2. The amount of memory allocated dynamically does not need to be known at compile time. In our text editor example, we could wait until the user tries to load a file, determine the file’s size, allocate exactly enough memory to hold the contents of the file, and then read the contents of the file into that memory. This results in no wasted memory!
  3. Objects allocated dynamically have dynamic duration, meaning the object has a lifetime that is determined by the programmer. This gives the programmer lots of flexibility (but also comes with some complexities).
  4. Dynamically allocated memory comes from the heap (which is large), not the stack (which is small), which gives our programs access to a lot more memory.

If you’ve been wondering how std::vector can change sizes to hold content of differing lengths, this is how -- under the hood, std::vector uses dynamic memory allocation to request memory for the vector’s internal array dynamically. If the vector runs out of free elements, it allocates a larger array, copies the elements to the new array, and discards the old array.

Unfortunately, dynamically allocated memory also has some disadvantages:

  1. Objects that are dynamically allocated will not automatically deallocated. Instead, the programmer is responsible for deallocating these objects.
  2. Allocating dynamic memory is typically slower than allocating static memory, so you generally won’t want to do a lot of small allocations.
  3. Accessing dynamic memory can be slower than accessing static memory. This typically is not a problem, but is worth knowing in case you need to optimize a particular bit of code for speed.

If a dynamically allocated class object is not properly returned to the operating system, the destructor for that object will not be called for that object, which can result in data loss.

Memory leaks

Dynamically allocated memory stays allocated until it is explicitly returned back to the operating system, or until the program ends (and the operating system cleans it up, assuming your operating system does that). If your program loses track of some dynamically allocated memory before giving it back to the operating system, a memory leak will result. When a memory leak occurs, your program can’t return the dynamically allocated memory back to the operating system, because it no longer knows where it is. The operating system also can’t use this memory, because that memory is considered to be still in use by your program. This memory is typically lost until the program is terminated and the operating system can reclaim it.

If you’ve ever seen a problem slowly use up more and more memory the longer it runs, it probably has a memory leak somewhere and is continually requesting memory from the operating system without properly returning it. Programs with severe memory leak problems can eat all the available memory on the machine, causing the entire machine to run slowly or even crash.

Warning

Dynamically allocated memory needs to be carefully tracked so that it can be properly deallocated. Failure to do so will result in leaked memory. Additionally class objects will not be destructed, which can lead to data loss.

So how does one dynamically allocate memory?

C++ contains two operators that are used for dynamically allocating memory: new and delete. We’ll take a look at these operators in the next lesson, as well as some of the key problems that new and delete have. Much of the rest of the chapter will be devoted to solving these problems.


10.2 -- Dynamic memory allocation with new and delete
Index
7.x -- Chapter 7 summary and quiz

//////////////////////////////////////////////////////////
Future lesson
////////////////////////////////////////////////////////

Dynamically allocating single variables

Luckily, C++ makes this easy for us via smart pointers. There are two types of smart pointers that handle ownership

  • std::unique_ptr: The allocated resource has a unique owner, a variable. When this variables dies, the resource is freed.
  • std::shared_ptr: The allocated resource has multiple owners, also variables. When all of these variables die, the resource is freed.

There is also the possibility of allocating and freeing memory manually without using smart pointers, but doing so places the responsibility of freeing the memory on the programmer. If the programmer forgets to free the memory, the application is likely to consume more and more memory until it terminates itself or gets killed by the operating system because no more memory is available. We’ll take a look at manually allocating memory near the end of the tutorials. For now, and in practice, we’ll use smart pointers.

To dynamically allocate a single variable, we use std::make_unique from the <memory> header:

In the above case, we’re requesting an integer’s worth of memory from the operating system. std::make_unique creates the object in that memory, and then returns a variable of type std::unique_ptr<int> containing the address of the memory that has been allocated.

Most often, we’ll store the return value in our own pointer variable so we can access the allocated memory later.

We can then dereference the std::unique_ptr to access the memory just like a regular pointer:

If it wasn’t before, it should now be clear at least one case in which pointers are useful. Without a pointer to hold the address of the memory that was just allocated, we’d have no way to access the memory that was just allocated for us!

std::unique_ptr

A std::unique_ptr is the unique owner of a resource. This means that the returned std::unique_ptr variable is the only std::unique_ptr that points to this specific resource. No other std::unique_ptr can point to the same resource. The consequence for our code is that std::unique_ptr cannot be copied, because that would mean that the original and the copy both own the same resource.

The copy of pineHeight when creating birchHeight is avoidable by allocating a new double and initializing it with the value of pineHeight

Now pineHeight and birchHeight point to distinct doubles in memory that happen to have the same value, but are otherwise unrelated.

printTreeHeight is a different story. Whenever you see a variable of type std::unique_ptr, it means that this variable is the unique owner of the resource. If a parameter is a std::unique_ptr, the caller has to give up ownership of their resource by moving the pointer. We cover move semantics later.

If we don’t want printTreeHeight to obtain ownership of the resource, we shouldn’t use a smart pointer. Instead, use a regular double or, if the type is not fundamental, a reference.

If nullptr is a valid value, which smart pointers can hold just as well as regular pointers, a regular pointer should be used instead. To get a regular pointer from a smart pointer, the get() function can be used. There are several ways of causing a smart pointer to be a nullptr, which are shown below. If the smart pointer held a resource before being set to a nullptr, the old resource is freed first.

Output

This tree doesn't exist
100

When are dynamically allocated objects useful

All the examples above could be rewritten without any pointers at all, so what is dynamic allocation there for?

A common use case of dynamically allocated memory are objects that are members of a type, eg. of a struct, that cannot be initialized because not all required information is available. Say we’re creating a game about flying airplanes. An airplane may or may not have a pilot, and a pilot cannot exist without an airplane. Making Pilot a member of Airplane is wasteful, because every airplane would require storage for a Pilot. Thus we cannot initialize the Pilot when an Airplane is created (Unless we already have a Pilot). A regular pointer wouldn’t do the trick, because the Pilot has to be created somewhere. If we created the Pilot outside of the Airplane and set the airplane‘s Pilot pointer to this Pilot, the Pilot object could die when the airplane still needs it. By using a smart pointer, the Airplane can own a Pilot if it needs to, and otherwise set the pointer to a nullptr.

Like this, the airplane can exist without a pilot, and if the airplane needs a pilot, one can be created. The creator of the pilot doesn’t have to worry about the pilot dying before the airplane, because the airplane owns the pilot.

Keep in mind that std::unique_ptr cannot be copied. Because Airplane has a std::unique_ptr member, Airplane can also not be copied.

The most important use of dynamically allocated variables is inheritance, which we’ll cover later.

std::shared_ptr

std::unique_ptr is straightforward. The lifetime of the owned resource is handled very similar to objects with automatic storage duration. When the std::unique_ptr dies, the resource dies. But sometimes a resource is owned by more than one owner, for example a game where you play multiple characters but they all share an inventory. It doesn’t make sense for the inventory to exist without a character, but we cannot determine a single character to be the owner of the inventory. If one character object dies, the other character objects still need the inventory. Only when all character objects are dead, the inventory can safely be disposed of.

std::shared_ptr does just that. A resource is shared between one or more std::shared_ptrs and when the last std::shared_ptr dies, the resource is freed. Analogous to std::make_unique, there is std::make_shared to create a shared resource. To add an owner to a resource, one of the existing std::shared_ptrs has to be copied.

We don’t need backpack to outlive the other objects either. We could use reset() or let it go out of scope and the Inventory resource would continue to live as long as some std::shared_ptr points to it.

std::shared_ptr is often perceived to be easier to use than std::unique_ptr when you’re just getting started with smart pointers. However, std::shared_ptr is computationally more expensive than std::unique_ptr and should only be used when no single object can be the owner of a resource.

Dangling pointers

C++ does not make any guarantees about what will happen to the contents of deallocated memory, or to the value of the resource being deleted. In most cases, the memory returned to the operating system will contain the same values it had before it was returned, and the pointer will be left pointing to the now deallocated memory.

A pointer that is pointing to deallocated memory is called a dangling pointer. Dereferencing a dangling pointer will lead to undefined behavior. Consider the following program:

In the above program, the value of 7 that was previously stored in the allocated memory will probably still be there, but it’s possible that the value at that memory address could have changed. It’s also possible the memory could be allocated to another application (or for the operating system’s own usage), and trying to access that memory will cause undefined behavior. Most likely, the program prints garbage or crashes.

Pointers can dangle whenever you have multiple pointers pointing to the same resource. In the above example, the std::unique_ptr and the pointer returned by get() pointed to the same resource. Dangling pointers can also be provoked without a separate function:

Try to avoid having multiple pointers point at the same piece of dynamic memory. If two pointers in the same function point to the same resource, you probably only need one pointer.

Null pointers and dynamic memory allocation

Null pointers (pointers set to nullptr) are particularly useful when dealing with dynamic memory allocation. In the context of dynamic memory allocation, a null pointer basically says “no memory has been allocated to this pointer”. This allows us to do things like conditionally allocate memory:

Conclusion

std::make_unique and std::make_shared allow us to dynamically allocate single variables for our programs.

Dynamically allocated memory has dynamic duration and will stay allocated until it is freed, which happens automatically when smart pointers are used.

Be careful not to dereference dangling or null pointers.

In the next lesson, we’ll take a look at using dynamically allocated arrays.


10.2 -- Dynamic memory allocation with new and delete
Index
7.x -- Chapter 7 summary and quiz

67 comments to 10.1 — Introduction to dynamic memory allocation

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">