C++ Dynamic Memory Management

Introduction

C++ dynamic memory management always follows the order of memory allocation, object construction, object destruction and memory deallocation.

In this blog post, I would like to quickly discuss the rationale behind the dynamic memory management.

C++ Dynamic Memory Management

In C++, a common way to create new object on dynamic memory is to use the new and delete keywords. Specifically, new allocates memory and constructs the object on memory, whereas delete destructs the object and releases the memory.

For example, we created a new std::vector<int> object which holds no elements on the dynamic memory.

new.cpp
1
2
3
4
5
6
7
8
9
10
11
#include <cstdlib>
#include <vector>

int main()
{
// Memory allocation followed by object construction.
std::vector<int>* ptr{
new std::vector<int>{}}; // New object allocates no additional memory.
// Object destruction followed by memory deallocation.
delete ptr;
}

It is possible that calling the object constructor will allocate additional memory as well and the additional memory that the object owns can only be released by calling the object destructor.

For example, we created a new std::vector<int> object on the dynamic memory. The new std::vector<int> object will also allocate additional memory on the dynamic memory to store elements.

new.cpp
1
2
3
4
5
6
7
8
9
10
11
#include <cstdlib>
#include <vector>

int main()
{
// Memory allocation followed by object construction.
std::vector<int>* ptr{new std::vector<int>{
1, 2, 3}}; // New object allocates additional memory.
// Object destruction followed by memory deallocation.
delete ptr;
}

In fact, new can be decomposed to aligned_alloc and placement new, whereas delete can be decomposed to calling the destructor explicitly if the object has the destructor (C++ primitive types do not have destructor) and free. Notice that to ensure data alignment, aligned_alloc is used instead of malloc.

new.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdlib>
#include <vector>

int main()
{
// Memory allocation.
void* mem{
aligned_alloc(alignof(std::vector<int>), sizeof(std::vector<int>))};
// Placement of new object to memory.
std::vector<int>* ptr = new (mem)
std::vector<int>{}; // New object allocates no additional memory.
// Object destruction.
// If we know the object owns no additional memory,
// it does not matter whether we call the destructor or not
// as memory deallocation can take care of that.
// However, without knowing if the object owns additional memory,
// without calling the destructor, we cannot guarantee that
// all the memory allocated will be deallocated.
ptr->~vector<int>(); // Explicit destructor call.
// Memory deallocation.
free(ptr);
}

It should be noted that placement new can also place the object on static memory so that free is not needed. But since this blog post is only talking about dynamic memory, the object will only be placed on dynamic memory and free is required.

If the object owns no additional memory, it is safe to deallocate the memory without calling its destructor in this particular application.

new.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdlib>
#include <vector>

int main()
{
// Memory allocation.
void* mem{
aligned_alloc(alignof(std::vector<int>), sizeof(std::vector<int>))};
// Placement of new object to memory.
std::vector<int>* ptr = new (mem)
std::vector<int>{}; // New object allocates no additional memory.
// Object destruction.
// If we know the object owns no additional memory,
// it does not matter whether we call the destructor or not
// as memory deallocation can take care of that.
// However, without knowing if the object owns additional memory,
// without calling the destructor, we cannot guarantee that
// all the memory allocated will be deallocated.
// ptr->~vector<int>(); // Explicit destructor call.
// Memory deallocation.
free(ptr);
}

However, since we cannot guarantee all different objects own no additional memory at any time during runtime, memory deallocation without calling destructor might result in memory leak.

For example, this leads to memory leak because the std::vector<int> object owns additional dynamic memory to store the elements.

new.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <cstdlib>
#include <vector>

int main()
{
// Memory allocation.
void* mem{
aligned_alloc(alignof(std::vector<int>), sizeof(std::vector<int>))};
// Placement of new object to memory.
std::vector<int>* ptr = new (mem)
std::vector<int>{1, 2, 3}; // New object allocates additional memory.
// Object destruction.
// If we know the object owns no additional memory,
// it does not matter whether we call the destructor or not
// as memory deallocation can take care of that.
// However, without knowing if the object owns additional memory,
// without calling the destructor, we cannot guarantee that
// all the memory allocated will be deallocated.
// ptr->~vector<int>(); // Explicit destructor call.
// Memory deallocation.
free(ptr);
}
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
$ g++ new.cpp -o new
$ $ valgrind --leak-check=full ./new
==261759== Memcheck, a memory error detector
==261759== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==261759== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==261759== Command: ./new
==261759==
==261759==
==261759== HEAP SUMMARY:
==261759== in use at exit: 12 bytes in 1 blocks
==261759== total heap usage: 3 allocs, 2 frees, 72,740 bytes allocated
==261759==
==261759== 12 bytes in 1 blocks are definitely lost in loss record 1 of 1
==261759== at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==261759== by 0x109A9D: __gnu_cxx::new_allocator<int>::allocate(unsigned long, void const*) (in /home/leimao/Workspace/deletion/new)
==261759== by 0x109956: std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&, unsigned long) (in /home/leimao/Workspace/deletion/new)
==261759== by 0x1097F5: std::_Vector_base<int, std::allocator<int> >::_M_allocate(unsigned long) (in /home/leimao/Workspace/deletion/new)
==261759== by 0x109624: void std::vector<int, std::allocator<int> >::_M_range_initialize<int const*>(int const*, int const*, std::forward_iterator_tag) (in /home/leimao/Workspace/deletion/new)
==261759== by 0x10944D: std::vector<int, std::allocator<int> >::vector(std::initializer_list<int>, std::allocator<int> const&) (in /home/leimao/Workspace/deletion/new)
==261759== by 0x1092FD: main (in /home/leimao/Workspace/deletion/new)
==261759==
==261759== LEAK SUMMARY:
==261759== definitely lost: 12 bytes in 1 blocks
==261759== indirectly lost: 0 bytes in 0 blocks
==261759== possibly lost: 0 bytes in 0 blocks
==261759== still reachable: 0 bytes in 0 blocks
==261759== suppressed: 0 bytes in 0 blocks
==261759==
==261759== For lists of detected and suppressed errors, rerun with: -s
==261759== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

That’s why before memory deallocation, we will always have to call the destructors of all the objects on the allocated memory.

References

Author

Lei Mao

Posted on

07-02-2022

Updated on

07-03-2022

Licensed under


Comments