C++ memory allocation
While we are aiming to focus on concepts, there are some underlying language aspects that impact on how we explore memory management. In our case, we need to have a little explore of some mechanics of C++ in terms of how it manages memory, as this will impact on our code here.
C malloc, realloc, and free
C is very low level, which is great as we get to play around with all the mechanics that underlie other languages and tools we encounter. This is what we have looked at so far.
Working with these tools will be fine, as long as we stick to C. If we want to start using C++ features, like string
for example, then we need to think about what C++ is doing on top of these basics.
C++ new, new[], delete, and delete[]
C++ provides a number of operators for performing memory management - with the main categories being new
to allocate memory and delete
to free allocated memory. You use the new
operator with a type, and it allocates space on the heap returning you a pointer to that (much like malloc
). Where you indicate an array, the new operator will allocate space for the indicated number of elements. Similarly, you can use delete
to free the memory allocated to a pointer or delete[]
to free an array. These are shown below, with an integer pointer and array pointer.
int main(){ // Use new to allocate space on the heap for an integer int* intPtr = new int; delete intPtr;
// Use new [] to allocate space for an array of 10 integers int* arrayPtr = new int[10]; delete[] arrayPtr;}
Notice there is no equivalent of realloc
. So you can allocate and free, but not change any memory allocation.
Constructors and destructors
Behind the scenes, C++ adds additional features to its memory allocation functions for structs (and classes) to help developers initialise values when they are allocated, and clean up when they are freed. These are called constructors and destructors. Inside the C++ new
operator, C++ will allocate space for the data and then call the constructor to initialise this space. Similarly, the delete
operator will first call the destructor and then free the memory allocation.
If we want to play around with memory allocations, then malloc
, realloc
, and free
are the tools we want to use, but we need to be aware of these extra steps that C++ is performing if we mix in the use of string
.
Mixing malloc and new
Internally, malloc
and new
both allocate blocks of memory. The main difference is the missing call to the constructor when we are working with C++ structs (and classes).
The good news is that C++ provides the capability to call new
on a previous memory allocation using placement new. Using this we can play around with memory using malloc
, and realloc
but still ensure that the constructors are called.
The following code illustrates how we can achieve this.
#include <cstdlib>#include <string>
using std::string;
/*** A test struct that contains a string... we need to* make sure that it's constructor is called.*/struct my_test_struct{ string name;};
void print_test_struct_name(const my_test_struct& value){ printf("Name: %s\n", value.name.c_str());}
int main(){ // The standard C++ way to construct an object on the heap my_test_struct *p3 = new my_test_struct; p3->name = "P3 Name!";
// Mixing new and malloc // - we first use malloc to get memory my_test_struct *p4 = (my_test_struct *)malloc(sizeof(my_test_struct)); // - then call the placement new on this to run the default constructor new(p4) my_test_struct(); p4->name = "P4 Name!";
// Use our procedure to print these print_test_struct_name(p3); print_test_struct_name(p4);
// Code to free and delete needs to be added here return 0;}
Mixing free and delete
Freeing the memory allocation has similar issues when we mix C++ with the standard C allocation functions. In C++, the delete
operator will first call the destructor and then free the memory allocation. When using the standard C functions, you must first add a call to the destructor and then use free
to clear the memory allocation.
We can complete the above example by adding in the code to free the memory allocations.
#include <cstdlib>#include <string>
using std::string;
/*** A test struct that contains a string... we need to* make sure that it's constructor is called.*/struct my_test_struct{ string name;};
void print_test_struct_name(const my_test_struct& value){ printf("Name: %s\n", value.name.c_str());}
int main(){ // The standard C++ way to construct an object on the heap my_test_struct *p3 = new my_test_struct; p3->name = "P3 Name!";
// Mixing new and malloc // - we first use malloc to get memory my_test_struct *p4 = (my_test_struct *)malloc(sizeof(my_test_struct)); // - then call the placement new on this to run the default constructor new(p4) my_test_struct(); p4->name = "P4 Name!";
// Use our procedure to print these print_test_struct_name(p3); print_test_struct_name(p4);
// Free our memory allocations using the standard C++ approach delete p3;
// Mix destructor and free // - first we manually call the destructor. It is named "~ struct name" // so it is ~my_test_struct. We call this using: p4->~my_test_struct(); // - then we call free to release the allocated memory free(p4);
return 0;}
Doing this with arrays
When you use this to work with arrays, you need to make sure to call the constructor and destructor for each element of the array - when allocating or freeing space. The following code demonstrates this with the test struct we have been working with.
#include <cstdlib>#include <string>
using std::string;using std::to_string;
struct my_test_struct{ string name;};
void print_test_struct_name(const my_test_struct& value){ printf("Name: %s\n", value.name.c_str());}
int main(){ // Mixing new and malloc for an array of my_test_struct // Create an array of 10 elements - first use malloc to get space my_test_struct *my_array = (my_test_struct *)malloc(sizeof(my_test_struct) * 10); // For each of the new elements... for (int i = 0; i < 10; i++) { // Call constructor to initialise each of the 10 elements new(&my_array[i]) my_test_struct(); }
// Add an element to the array - reallocate to get more space my_test_struct *tmp_ptr = (my_test_struct *)realloc(my_array, sizeof(my_test_struct) * 11); if ( tmp_ptr != nullptr ) { my_array = tmp_ptr; // Call constructor on the new element new(&my_array[10]) my_test_struct(); }
for(int i = 0; i < 11; i++) { my_array[i].name = "Test name " + to_string(i); print_test_struct_name(my_array[i]); }
// Remove an element - first call the destructor my_array[10].~my_test_struct(); // Reallocate to release memory allocation for last element tmp_ptr = (my_test_struct *)realloc(my_array, sizeof(my_test_struct) * 10); if ( tmp_ptr != nullptr ) { my_array = tmp_ptr; }
// Destroy the array - loop to call destructor on each element for (int i = 0; i < 10; i++) { my_array[i].~my_test_struct(); } // Free the array free(my_array);}
Doing this with generics
This also works with generics, with C++ ensuring that there are appropriate destructors and constructors for all primitive types. The following code shows an example of this where we use a generic function to allocate space on the heap, and a generic procedure to free that allocation. Notice that we can use this to allocate space for the struct, but also allocate space for an integer.
#include <cstdlib>#include <string>
using std::string;
/*** A test struct that contains a string... we need to* make sure that it's constructor is called.*/struct my_test_struct{ string name;};
template <typename T>T* make_on_heap(T init){ // Allocate memory T *result = (T *)malloc(sizeof(T)); // Call constructor new(result) T();
// Copy in data *result = init;
// Return pointer return result;}
template <typename T>void clear_from_heap(T *ptr){ // Call destructor ptr->~T(); // Free memory allocation free(ptr);}
int main(){ my_test_struct *data = make_on_heap<my_test_struct>((my_test_struct){ "Hello"}); int *p = make_on_heap<int>(5);
printf("p -> %d\n", *p); printf("data->name = %s\n", data->name.c_str());
clear_from_heap(p); clear_from_heap(data);
return 0;}