New and delete are just operators, operators which we can overload
to do something more custom. In the previous lesson, we learned that
these operators have two main jobs. They need to call the
constructor or the destructor, and then they need to allocate or
deallocate memory on the heap.
When we overload these operators,
we can only overload the memory allocation part of the process. Other
functionality that happens in the background like object initialization will
stay unchanged. I will hover over the new operator to check out its prototype
definition. Operators are defined just like functions, but we always need to
remember that unlike functions, they require this operator keyword. The new
operator takes in only one parameter, which defines the required size of the
memory allocation on the heap.
So, when we do something like this, new int, C++ will check out the
data type after the operator and calculate how much memory we need to
allocate to store a value of that size.
In this case, int will probably take up 4 bytes, so the number 4 will
be passed in as a size argument of the new operator.
The data type of this size argument is size_t,
taken from the standard namespace.
To use this data type in our example,
we will need to include the cstddef library.
The letter c in front informs us that this is a C++ wrapper
around the old stddef library used in C language.
This is because the size_t data type is also inherited from C. Size_t
is used to hold the size of objects in your program, and its own size
in memory depends on the implementation.
So basically, whenever you work with object sizes in bytes,
you should use the size_t data type. At the very least,
anyone who reads your code will immediately know your intentions.
We can also see that the return type of this new operator is a void pointer.
This makes sense because new is used to allocate
memory for any existing data type.
Now that we are familiar with the operator's interface, let's
overload it by defining our own implementation.
Since I'm defining this in the global scope,
this overloaded version will be used instead of the default new operator in
any part of the program. I want you to look at this overloaded operator as
nothing more than a piece of a larger puzzle,
a part of a more complicated process.
Whenever we call new, something will first happen in
the background. As I mentioned,
C++ will determine the size we need and pass it down to the operator.
This is where our overloaded implementation comes in. We need to allocate
enough memory to store an object of that size and then return the memory
address from the heap. In most implementations, new operator will actually
call the malloc function we learned about in one of the previous lessons, and
this is exactly what we're going to do.
So let's include the cstdlib library again. We only need
one line of code to make this work.
I am passing the required memory size to the malloc function.
Malloc will then allocate a memory block of that size on the
heap and return a void pointer, which points to the beginning
byte of that allocated space.
We don't need to cast this pointer to any other type because the void
pointer is exactly what we need to return from this operator.
Now, let's see the prototype of the delete operator.
Delete operator will not return anything, which is expected. However,
it will receive two parameters.
The first one is the void pointer, which points to the place
in memory that needs to be deallocated.
The second parameter is the size of that allocated block of memory.
So let's overload this operator, too. To implement this, we only
need the first argument. Since we chose to allocate memory with
malloc, it is only appropriate to deallocate it with its
complimentary free function. As you know,
free only needs a pointer to the place in memory, which needs to be freed.
So, why would we even consider overloading these operators?
It seems like we just used these operators as a wrapper around legacy
allocation functions, and this is true, but we had to start from somewhere.
Now that our overloaded implementation is working,
we can start adding our own custom functionality. If you
remember from the previous lesson,
I mentioned that a tool like Valgrind will intercept memory allocation
functions and use them to track down memory leaks.
This is exactly what we can do now. We can create our own
primitive version of a memory leak finder.
We can start out by printing the amount of allocated
bytes by using the size parameter.
I can do the same thing inside of the delete operator by
printing out the amount of deallocated bytes.
We are just outputting this information to the terminal, but
imagine what you can do in more robust projects.
You could use this information to make analytics and graph out
the progress of allocations through time.
Let's compile this program again. As you can see, four bytes
were allocated to store this integer, and then these same
bytes were deallocated by delete.
Now let's make a simple memory leak finder. To keep things simple,
I will only use one global variable to implement this,
and I will name it allocated_mem.
We didn't talk about global variables since they're not
an important topic in this course.
All you need to know is that global variables are declared
once the program starts, and they are removed only when the
whole program is finished.
They don't belong to any local scope, which means that you can access this
variable at any time from any function in any part of the code.
Global variables are also not stored on stack or the heap.
They have their own special place in memory, but this is all
I'm going to say about this. We will use this variable to
track the amount of allocated bytes.
Whenever the program calls new,
we will add the amount of bytes to this global variable. When
the delete is called, we will do the opposite.
So in theory, if we deallocate all of the memory that was previously allocated,
this global variables should be 0 at the end of the
program. To make sure of this, I will create a condition
at the end of the main function.
If there is no memory leak, this variable will hold 0.
And since 0 in C++ is false, this message will not be shown.
However, if there is a memory leak, we will be notified.
Let's compile this program.
No memory leaks because we did everything correctly.
Now let's pretend that we forgot to deallocate this integer.
This will result in a memory leak.
If I compile the program again,
the message is shown, which means that our simple memory leak
finder is working. This lesson was a simple demonstration of
what you can do by overloading these operators. Here are some
other use cases and ideas.
For example,
some embedded systems will not provide you with a heap memory by
default. Since a lot of standard library containers use dynamic memory
allocation, this restriction can be very limiting.
You could implement your own version of the heap by overloading these
operators. In the previous lesson, we saw how memory allocations on
the heap are scattered all over the place.
We can avoid this type of memory fragmentation by preallocating
larger blocks of memory and then using these blocks to stack more
objects of the same type together,
making our allocations faster and storing these objects next to each other.
You can also have multiple overloads, each of them
with a different number of arguments.
These arguments can modify the allocation behavior.
For example, you may want to pass in the placeholder,
which will initialize the allocated space in memory.
You can also handle exceptions differently, reallocate insufficient memory
or reuse it. In this lesson, we overloaded these operators globally, but you
can also limit your own implementation to a specific class by overloading
them inside of the class definition.
If you are working on projects written in C++,
chances are you will need to make your code faster, more efficient,
and optimized by doing more with less resources.
You can achieve that by adapting the memory allocation strategy to the project
at hand, and this lesson can serve you as a starting point.