Overloading Operator new and delete 1
It happens quite too often that a C++ application allocates memory but doesn’t deallocate it. This is the job of the operator to new and delete. Thanks to them both, you can explicitly manage the memory management of an application.
Occasionally, I had to verify that a customer’s application correctly releases its memory. In particular, programs running for an extended period and often allocating and deallocating memory are challenging from the memory management perspective. Of course, the automatic memory release during the program’s shutdown is not an option.
The baseline
As a baseline of my analysis, I use a simple program that often allocates and deallocates memory. In production, the code is bigger. That does not affect my strategy.
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 |
// overloadOperatorNewAndDelete.cpp // #include "myNew.hpp" // #include "myNew2.hpp" // #include "myNew3.hpp" #include <iostream> #include <string> class MyClass{ float* p= new float[100]; }; class MyClass2{ int five= 5; std::string s= "hello"; }; int main(){ int* myInt= new int(1998); double* myDouble= new double(3.14); double* myDoubleArray= new double[2]{1.1,1.2}; MyClass* myClass= new MyClass; MyClass2* myClass2= new MyClass2; delete myDouble; delete [] myDoubleArray; delete myClass; delete myClass2; // getInfo(); } |
The key question is. Is there a corresponding delete for each new call?
Too careless
The question is quite simple to answer by overloading the global operator new and delete. I count for each operator, how often it was called.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Operator new
C++ offers the operator new in four variations.
void* operator new (std::size_t count ); void* operator new[](std::size_t count ); void* operator new (std::size_t count, const std::nothrow_t& tag); void* operator new[](std::size_t count, const std::nothrow_t& tag);
The first two variations will throw a std::bad_alloc exception if they can not provide the memory. The last two variations return a null pointer. It’s quite convenient that is sufficient to overload only version 1 because versions 2 – 4 use the version 1: void* operator new(std::size_t count). This statement also holds for variants 2 and 4, designed for C arrays. You can read the details of the global operator new here: cppreference.com.
The statements also hold for operator delete.
Operator delete
C++ offers six variations for operator delete.
void operator delete (void* ptr); void operator delete[](void* ptr); void operator delete (void* ptr, const std::nothrow_t& tag); void operator delete[](void* ptr, const std::nothrow_t& tag); void operator delete (void* ptr, std::size_t sz); void operator delete[](void* ptr, std::size_t sz);
According to operator new, it is sufficient to overload for operator delete the first variant because the remaining five use void operator delete(void* ptr) as a fallback. Only a word about the two last versions of operator delete. You have in this version the length of the memory block in the variable sz to your disposal. Read the details here at cppreference.com.
This time I use the program overloadOperatorNewAndDelete.cpp the header myNew.hpp (line 3). The same holds for line 34. Here I invoke the function getInfo to get information about my memory management.
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 |
// myNew.hpp #ifndef MY_NEW #define MY_NEW #include <cstdlib> #include <iostream> #include <new> static std::size_t alloc{0}; static std::size_t dealloc{0}; void* operator new(std::size_t sz){ alloc+= 1; return std::malloc(sz); } void operator delete(void* ptr) noexcept{ dealloc+= 1; std::free(ptr); } void getInfo(){ std::cout << std::endl; std::cout << "Number of allocations: " << alloc << std::endl; std::cout << "Number of deallocations: " << dealloc << std::endl; std::cout << std::endl; } #endif // MY_NEW |
I create in the file two static variables alloc and dealloc (lines 10 and 11). They keep track of how often I used the overloaded operator new (line 13) and operator delete (line 18). I delegate in the functions the memory allocation to std::malloc and the memory deallocation to std::free. The function getInfo (lines 23 – 31) provides me with the numbers and displays them.
The question is. Have I cleaned everything clean?
Of course, not. That was the intention of this and the following post. Now I know that I have leaks. Maybe it helps to determine the addresses of the objects which I have forgotten to clean up.
Addresses of the memory leaks.
So, I have to put more cleverness into the header myNew2.hpp.
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 |
// myNew2.hpp #ifndef MY_NEW2 #define MY_NEW2 #include <algorithm> #include <cstdlib> #include <iostream> #include <new> #include <string> #include <array> int const MY_SIZE= 10; std::array<void* ,MY_SIZE> myAlloc{nullptr,}; void* operator new(std::size_t sz){ static int counter{}; void* ptr= std::malloc(sz); myAlloc.at(counter++)= ptr; return ptr; } void operator delete(void* ptr) noexcept{ auto ind= std::distance(myAlloc.begin(),std::find(myAlloc.begin(),myAlloc.end(),ptr)); myAlloc[ind]= nullptr; std::free(ptr); } void getInfo(){ std::cout << std::endl; std::cout << "Not deallocated: " << std::endl; for (auto i: myAlloc){ if (i != nullptr ) std::cout << " " << i << std::endl; } std::cout << std::endl; } #endif // MY_NEW2 |
The key idea is to use the static array myAlloc (line 15) to keep track of the addresses of all std::malloc (line 19) and std::free (line 19) invocations. Of course, I can not use the function operator new container that needs dynamic memory. This container would invoke the operator new. A recursion that would cause my program to crash. Therefore, I use a std::array in line 15 because std::array gets its memory at compile time. Now it can happen that my std::array becomes too small. Therefore, I invoke myAlloc.at(counter++) to check the array boundaries.
Which memory address have I forgotten to release? The output answers.
A simple search for the object having the address is no good idea. Because it is quite probable that a new call of std::malloc reuses an already-used address. That is ok if the objects have been deleted in the meantime.
But why are the addresses part of the solution? I have only to compare the memory address of the created objects with those of the not deleted objects.
Comparison of the memory addresses
In addition to the memory address, I have the reserved memory size at my disposal. I use this information in operator new.
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 |
// myNew3.hpp #ifndef MY_NEW3 #define MY_NEW3 #include <algorithm> #include <cstdlib> #include <iostream> #include <new> #include <string> #include <array> int const MY_SIZE= 10; std::array<void* ,MY_SIZE> myAlloc{nullptr,}; void* operator new(std::size_t sz){ static int counter{}; void* ptr= std::malloc(sz); myAlloc.at(counter++)= ptr; std::cerr << "Addr.: " << ptr << " size: " << sz << std::endl; return ptr; } void operator delete(void* ptr) noexcept{ auto ind= std::distance(myAlloc.begin(),std::find(myAlloc.begin(),myAlloc.end(),ptr)); myAlloc[ind]= nullptr; std::free(ptr); } void getInfo(){ std::cout << std::endl; std::cout << "Not deallocated: " << std::endl; for (auto i: myAlloc){ if (i != nullptr ) std::cout << " " << i << std::endl; } std::cout << std::endl; } #endif // MY_NEW3 |
Now, the allocation and deallocation of the application are more transparent.
A simple comparison shows. I forget to release an object with 4 bytes and an object with 400 bytes. In addition, the allocation sequence in the source code corresponds to the sequence of outputs in the program. Now, it should be quite easy to identify the missing memory releases.
What’s next?
The program is not beautiful in two ways. First, I statically allocate the memory for std::array; second, I want to know which object was not released. In the next post, I will solve both issues.
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!