In addition to dynamically allocating single values, we can also dynamically allocate arrays of variables. Unlike a fixed array, where the array size must be fixed at compile time, dynamically allocating an array allows us to choose an array length at runtime.
To allocate an array dynamically, we use the array form of std::make_unique
or std::make_shared
. To keep things clear, we’ll be using std::make_unique
only. std::make_shared
works exactly the same.
Warning
std::make_shared
only started supporting arrays in C++20. Before C++20, some compilers falsely allow std::make_shared
to be used with arrays, but the allocated memory will not be freed correctly. Only use std::make_shared
with arrays after making sure you are using C++20 or later.
See the “Extending std::make_shared() to support arrays” row on the compiler support section of cppreference to check if your compiler can handle arrays in std::make_shared
.
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 |
#include <iostream> #include <cstddef> // std::size_t #include <memory> int main() { std::cout << "Enter a positive integer: "; std::size_t length{}; std::cin >> length; auto array{ std::make_unique<int[]>(length) }; // Use the array form of make_unique. Note that length does not need to be constant! // The type of array is std::unique_ptr<int[]> std::cout << "I just allocated an array of integers of length " << length << '\n'; array[0] = 5; // set element at index 0 to value 5 // We cannot use a range-based for-loop. Dynamically allocated arrays don't know their length. for (std::size_t i{ 0 }; i < length; ++i) { std::cout << array[i] << ' '; } std::cout << '\n'; return 0; } |
Output
5 0 0 0 0
Because we are allocating an array, C++ knows that it should use the array version of std::make_unique
instead of the scalar version. In the array version of std::make_unique
, the array cannot be initialized. We have to tell std::make_unique
the length of the array and all elements get value-initialized (Initialized to a safe default value, in this case 0).
The length of dynamically allocated arrays has to be a type that’s convertible to std::size_t
. We could use int
, but that would cause a compiler warning when the compiler is configured with a high warning level. We have the choice between using std::size_t
as the type of length
, or declaring length
as an int
and then casting it when we create the array like so:
1 2 3 |
int length{}; std::cin >> length; auto array{ std::make_unique<int[]>(static_cast<std::size_t>(length)) }; |
To avoid the lengthy cast, we’re using std::size_t
directly.
Note that because this memory is allocated from a different place than the memory used for fixed arrays, the size of the array can be quite large. You can run the program above and allocate an array of length 1,000,000 (or probably even 100,000,000) without issue. Try it! Because of this, programs that need to allocate a lot of memory in C++ typically do so dynamically.
Dynamically deleting arrays
std::unique_ptr
takes care of freeing the memory for us when we use std::make_unique
to allocate an array, just like it freed the memory for us when we allocated individual variables.
One often asked question about dynamically allocated arrays is, “How does std::unique_ptr know how much memory to delete? The length of the array is unknown!” The answer is that dynamically allocated arrays keep track of how much memory was allocated, so that the proper amount of memory can be deleted without specifying the length. Unfortunately, this size/length isn’t accessible to the programmer. If we want to know the length of an array, we have to keep track of it ourselves.
Dynamic arrays are almost identical to fixed arrays
In lesson 6.8 -- Pointers and arrays, you learned that a fixed array holds the memory address of the first array element. You also learned that a fixed array can decay into a pointer that points to the first element of the array. In this decayed form, the length of the fixed array is not available (and therefore neither is the size of the array via sizeof()), but otherwise there is little difference.
A dynamic array starts its life as a pointer that points to the first element of the array. Consequently, it has the same limitations in that it doesn’t know its length or size. A dynamic array functions identically to a decayed fixed array.
Initializing dynamically allocated arrays
If you want to initialize a dynamically allocated array to 0, the syntax is quite simple:
1 |
auto array{ std::make_unique<int[]>(length) }; |
All of the elements in array
are 0. If you want to initialize the elements to specific values, as was possible with fixed-length arrays like so
1 |
int array[3]{ 1, 9, 0 }; |
you’re out of luck. std::make_unique
can’t perform any initialization of arrays apart from value-initialization to a default value. std::make_shared
can initialize all elements to a specific value, but not selectively. It either value-initializes all elements, or initializes all elements to the same value. To fill a unique array with values, the values have to be assigned to the array later. This isn’t too big of an issue. After all, if we knew what to initialize the array with, we’d know how long the array is, so we wouldn’t need to dynamically allocate the array in the first place.
Resizing arrays
Dynamically allocating an array allows you to set the array length at the time of allocation. However, C++ does not provide a built-in way to resize an array that has already been allocated. It is possible to work around this limitation by dynamically allocating a new array, copying the elements over, and deleting the old array. However, this is error prone, especially when the element type is a class (which have special rules governing how they are created).
Consequently, we recommend avoiding doing this yourself and using a standard container instead. To get a better understanding of standard containers, we’ll implement class similar to std::vector
in the next lessons.
Conclusion
Dynamic memory allocation can be used to create arrays at run-time, but working with dynamically-allocated arrays can be tricky. Fortunately, most of the containers that you need in day-to-day programming are available in the standard library and dynamically-allocated arrays can be avoided.
Quiz time
Question #1
Write a program that:
* Asks the user how many names they wish to enter.
* Dynamically allocates a std::string
array.
* Asks the user to enter each name.
* Calls std::sort
to sort the names (See 6.4 -- Sorting an array using selection sort and 6.8a -- Pointer arithmetic and array indexing)
* Prints the sorted list of names.
std::string
supports comparing strings via the comparison operators < and >. You don’t need to implement string comparison by hand.
Your output should match this:
How many names would you like to enter? 5 Enter name #1: Jason Enter name #2: Mark Enter name #3: Alex Enter name #4: Chris Enter name #5: John Here is your sorted list: Name #1: Alex Name #2: Chris Name #3: Jason Name #4: John Name #5: Mark
A reminder
You can use std::getline
to read in names that contain spaces.
A reminder
When passing a std::unique_ptr
to a function, use get()
to get a regular pointer to the data. To the function it doesn’t matter if the array is dynamically allocated or not, it only needs a pointer. The pointer returned by get()
can also be used to perform pointer arithmetic. This cannot be done on std::unique_ptr
directly.
Question #2
Update the code from the previous quiz to use std::vector
instead of a dynamically allocated array. Your updated code should not use any pointers.
Leave a Reply