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:
1 2 |
std::array<Monster, 40> monster; // 40 monsters maximum std::array<char, 30'000> text; // this text file better not have more then 30k characters! |
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:
1 2 3 4 5 6 7 8 |
#include <array> int main() { std::array<int, 1'000'000> arr; // allocate 1 million integers (probably 4MB of memory) return 0; } |
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:
- We can dynamically allocate objects only when they are needed, so we don’t pay the memory cost for objects we don’t need.
- 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!
- 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).
- 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:
- Objects that are dynamically allocated will not automatically deallocated. Instead, the programmer is responsible for deallocating these objects.
- Allocating dynamic memory is typically slower than allocating static memory, so you generally won’t want to do a lot of small allocations.
- 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.
![]() |
![]() |
![]() |
//////////////////////////////////////////////////////////
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:
1 |
std::make_unique<int>(123); // dynamically allocate an integer initialized with the value 123 (and discard the result) |
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.
1 2 3 |
std::unique_ptr<int> ptr{ std::make_unique<int>(123) }; // dynamically allocate an integer and assign the address to ptr so we can access it later // To make this shorter and to avoid the redundant type, auto is commonly used for smart pointers auto ptr2{ std::make_unique<int>(456) }; |
We can then dereference the std::unique_ptr
to access the memory just like a regular pointer:
1 2 |
*ptr = 7; // assign 7 to the dynamically allocated integer std::cout << *ptr << '\n'; // 7 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#include <iostream> #include <memory> enum class Tree { pine, birch, apple, oak }; void printTreeHeight(std::unique_ptr<double> treeHeight) { std::cout << *treeHeight << '\n'; } std::unique_ptr<double> plantTree(Tree type, int age) { auto height{ std::make_unique<double>() }; // height has value 0.0 switch (type) { case Tree::pine: *height = 10.0; break; case Tree::birch: *height = 12.0; break; case Tree::apple: *height = 4.0; break; case Tree::oak: *height = 10.5; break; } *height *= age; return height; // Ok, plantTree gives up ownership of height and transfers it to the caller } int main() { auto pineHeight{ plantTree(Tree::pine, 3) }; auto birchHeight{ pineHeight }; // Error, pineHeight owns the integer printTreeHeight(pineHeight); // Error, this would also cause a copy of pineHeight return 0; } |
The copy of pineHeight
when creating birchHeight
is avoidable by allocating a new double
and initializing it with the value of pineHeight
1 |
auto birchHeight{ std::make_unique<double>(*pineHeight) }; |
Now pineHeight
and birchHeight
point to distinct double
s 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.
1 2 3 4 |
void printtreeheight(double treeHeight) { std::cout << treeHeight << '\n'; } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> #include <memory> void printTreeHeight(const double* treeHeight) { if (treeHeight == nullptr) { std::cout << "This tree doesn't exist\n"; } else { std::cout << *treeHeight << '\n'; } } int main() { std::unique_ptr<double> oakHeight{}; // nullptr oakHeight = nullptr; // nullptr oakHeight.reset(); // nullptr printTreeHeight(oakHeight.get()); // get returns a regular, non-owning, pointer to the resource oakHeight = std::make_unique<double>(100.0); // Create a new and very large oak tree printTreeHeight(oakHeight.get()); return 0; // oakHeight dies here and the memory is freed } |
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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <iostream> #include <memory> struct Pilot { std::string name{}; }; struct Airplane { std::string serialNumber{}; std::unique_ptr<Pilot> pilot{}; }; int main() { Airplane sandringham{ "ecb5e434-e23c-4e45-84c6-b52aca3dda85" }; // sandringham is parked, it doesn't have a pilot. char choice{}; std::cout << "Take off in plane no. " << sandringham.serialNumber << "? [y/n]\n"; std::cin >> choice; if (choice == 'y') { // The plane wants to fly, but it needs a pilot. sandringham.pilot = std::make_unique<Pilot>("Glenn"); std::cout << sandringham.pilot->name << " will be your pilot in " << sandringham.serialNumber << " today\n"; } return 0; // sandringham and the pilot (if any) die here } |
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_ptr
s 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_ptr
s has to be copied.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
#include <iostream> #include <memory> struct Item { std::string name{}; }; struct Inventory { Item items[4]{}; }; struct Character { std::string name{}; // Use a shared pointer to store the inventory std::shared_ptr<Inventory> inventory{}; }; int main() { auto backpack{ std::make_shared<Inventory>() }; // The inventory has 1 owner Character phineas{ "Phineas", backpack }; // The inventory has 2 owners Character ferb{ "Ferb", backpack }; // 3 owners char choice{}; std::cout << "Allow pets? [y/n]"; if (choice == 'y') { Character perry{ "Perry", backpack }; // 4 owners // ... // perry dies here, but the inventory still has 3 owners (backpack, phineas, ferb) } return 0; // ferb dies, inventory has 2 owners (backpack, phineas) // phineas dies, inventory has 1 owner (backpack) // backpack dies, inventory has 0 owners and dies } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <memory> // Returning a regular pointer int* createInteger() { auto i{ std::make_unique<int>(7) }; return i.get(); // i dies here and the memory is freed. The returned pointer points to deallocated memory. } int main() { int* ptr{ createInteger() }; std::cout << *ptr << '\n'; // Dereferencing a dangling pointer causes undefined behavior return 0; } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <memory> int main() { auto i{ std::make_unique<int>() }; // dynamically allocate an integer int* otherPointer{ i.get() }; // otherPointer is now pointed at that same memory location i.reset(); // return the memory to the operating system. i is now a nullptr // however, otherPointer is still pointing at the now deallocated memory! std::cout << i.get() << '\n'; // 0 std::cout << otherPointer << '\n'; // 0x219ce70 return 0; } |
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:
1 2 3 4 5 |
// If ptr isn't already allocated, allocate it if (!ptr) // same as (ptr == nullptr) { ptr = std::make_unique<int>(); } |
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.
![]() |
![]() |
![]() |
To write the code more reliable we need to learn more about the dynamic memory allocation using new and delete operators.