- Learn C++ Test Site - https://test.learncpp.com -

15.1 — Dynamic memory allocation with new and delete

In lesson %Failed lesson reference, id TODO% and lesson %Failed lesson reference, id TODO% we introduced smart pointers and throughout the tutorial we have seen how they can be used to dynamically allocate objects. Smart pointers themselves don’t do much, they are wrappers around a more fundamental part of the language, operator new and delete (and new[] and delete[]).

Using these operators by hand can be dangerous, which is why they are now well hidden behind smart pointers. You’ll only need new and delete if you’re working on old code, or, for whichever reason, don’t have access to smart pointers. new and delete are still used to this day. Even if you’re learning C++ for personal use and have no intentions of working on someone else’s code, it’s still worth knowing about these operators to be able to understand code and some other programming concepts that we’ll show later in this chapter.

Allocating memory using new

Allocating memory with new isn’t all too different from std::make_unique.

Output

3

new allocates an integer, initializes it with the value 3, and returns a pointer to the newly allocated integer. Rather than returning a std::unique_ptr or some other class-type, new returns a regular pointer.

As simple as this program might seem, there’s already a problem. If we had used std::make_unique, everything would be fine, but because we allocated memory manually, we’re responsible for freeing this memory. Although p died at the end of main(), the memory it pointed to still exists. The memory is still owned by our program, but we have no way to access it. This is called a memory leak. Our program is using memory that it can’t access or free, and the operating system can’t give the memory to other programs, because it thinks that we’re still using it. If this happens often enough, not just our program but the entire system will become unstable.

Although memory leaks can result from a pointer going out of scope, there are other ways that memory leaks can occur. For example, a memory leak can occur if a pointer holding the address of the dynamically allocated memory is assigned another value:

This can be fixed by deleting the pointer and freeing the memory before reassigning it.

Freeing memory

This is where delete comes into play. delete is the counterpart to new, it frees the previously allocated memory.

Now our program is correctly cleaning up the resources it used. The memory that was previously occupied by the integer can now be reused for other purposes, for example when we use new the next time. The user of the program won’t notice a difference to the previous code, but the operating system might.

Why memory leaks are bad

Say you want to play basketball at school. You walk up to the basketball stand, net, or whatever container your school uses to store basketballs, take one out, and play with it. The bell rings and it’s time for lunch. The right thing to do is returning your basketball to the basketball stand. If you choose not to return the ball, you can leave it laying on the floor. If enough students don’t care to return their basketball to the basketball stand, the next class has no balls to play with.

In reality, if the coach sees balls on the floor after class has ended, he goes around picking up balls and returning them to the basketball stand. But if he doesn’t, the next class can’t play.

This is what happens to memory as well. If you don’t free the memory you used, another program or your own program will run out of memory. The operating system might clean up the memory after your program exits, but it doesn’t have to. When a program doesn’t get the memory it needs, it will refuse to launch, will be restricted in functionality, be slower, misbehave, or the operating system kills the program.

Even if we use delete, it might get skipped because of other events, for example when an exception is thrown or when a function returns early.

eat() returns early, because meat has too many calories. Because of this, the delete is never reached and memory gets leaked. This will happen if at any point between the call to new and delete an exception occurs or the function uses a return. In this specific case, we could add a delete before the early return. If there are several early returns or multiple dynamic allocations, doing so gets messy. Also, if any of the functions called in between new and delete throws an exception, the delete would get skipped unless we carefully handle the exceptions. If we had used a std::unique_ptr, the memory would be freed when eat() exits, no matter what the reason for the exit is.

Dangling pointers

We’ve already encountered dangling pointers when using smart pointers. The same can happen to pointers that were returned by new, but easier.

This code produces undefined behavior, because after we deleted p, it dangles and we’re not allowed to access the memory pointed to by p anymore. The value of p, ie. the address it points to, hasn’t changed. But after deleting p, we’re no longer allowed to access the memory at that address. It might already be used for something else, we don’t know.

Double deletes

A double delete or double free occurs when the same memory is freed more than once.

The first call to delete frees the memory pointed to by p. As you’ve learned before, after this call p is a dangling pointer. It still points to the same memory address, but that memory is no longer ours. When delete is called a second time, a double delete occurs, invoking undefined behavior.

Even with smart pointers double deletes can occur, but we have to work a little harder to make them happen.

Creating a smart pointer from a regular pointer is valid and can be done in combination with new to automatically free the memory later, but the smart pointer will assume ownership of that memory. There are now 2 owners of the integer, snowman and pumpkin. At the end of main() pumpkin is destroyed first and frees the integer. Then snowman is destroyed and tries to free the integer again, but it’s already gone. A double delete has occurred.

It can be made easier to find pointers that are no longer valid and prevent double deletes by setting the pointer to a nullptr when it’s no longer needed.

The first delete is successful and p is set to a nullptr. When the second delete is encountered, p is a nullptr already. Deleting a nullptr has no effect, so the program behaves correctly. Of course you wouldn’t write 2 deletes right after another in pratice, the second delete would be in a loop or function, which makes it harder to keep track of memory.

The ownership question

When we use smart pointers consistently throughout our code, we can assume that every regular pointer points to memory that is owned by someone (By “someone”, we mean a smart pointer inside function or class, or the pointer points to a variable with non-dynamic duration). At no place do we have to worry about who is responsible for freeing that memory.

This is not true when new and delete are used. Consider the following

If you’re lucky, there’s a documentation for getPlayer() that states whether or not the caller has to free the Player. If there is no documentation or the documentation doesn’t mention it, you’ll have to search for the definition of getPlayer() (Which you might not have if the function comes from a library) or hope that another programmer who uses the same function can tell you. All this costs time and isn’t fun.

If we know that the author of getPlayer() uses smart pointers, we know that getPlayer() owns the Player and getPlayer() or the library it comes from is responsible for freeing it. We know this because if getPlayer() wanted to share or transfer ownership of the Player to the caller, it would have returned a smart pointer.

Allocating arrays using new[]

new and delete work for individual variables. To allocate entire arrays, new[] and delete[] have to be used instead.

Although the [] brackets are appended to the type and not to new, the operator is called new[].

This works just as you’d expect. delete[] tells the CPU that it needs to clean up multiple variables instead of a single variable. One of the most common mistakes that new programmers make when dealing with dynamic memory allocation is to use delete instead of delete[] when deleting a dynamically allocated array.

Warning

Using the non-array version of delete on a pointer that points to array array causes undefined behavior.

Finding memory bugs

As you have hopefully noticed by now, manually allocating memory is dangerous. If you have to use manual allocations, and even if you don’t, we highly recommend using tools to detect misuse of memory. AddressSanitizer [1] is available for the major compilers and detects all of the issues shown in this lesson as well as several others. To enable it in gcc and clang, simply add

-fsanitize=address -fno-omit-frame-pointer

to your compilation options. In Visual Studio, you can enable AddressSanitizer under “Configuration Properties -> C/C++ -> General -> Enable Address Sanitizer”. For details about the Visual Studio setup, see AddressSanitizer (ASan) for Windows with MSVC [2].

Conclusion

Manual dynamic memory allocation is a subject of the past, use smart pointers and standard containers instead. If you do have to use new and delete, be very careful about keeping track of who’s responsible for freeing the memory. Try not to let dynamically allocated memory escape from the function it was allocated in, ie. use delete in the same function that called new.

%Missing lookup for lesson id 4812%