Lesson #8

Dynamic Memory Using new and delete

Overview

In this lesson I will cover the following topics

  1. What is dynamic memory.

  2. How to allocate and free dynamic memory.

  3. How dynamic objects are initialized and cleaned up.

Body

What is dynamic memory?

So far I have talked about the basics of classes: public and private sections, methods, constructors and destructors, friends, and operator overloading. I have talked about how you can create your own types using classes and arrange things to make those types easy to use. The C++ standard library provides you with a rich collection of classes that do a variety of useful things. I will introduce a few of the most important such classes in some upcoming lessons. However, to fully appreciate what the library classes are doing for you, you need to understand how C++ classes can be used to manage the resources a program needs. In this lesson, I will talk about one of the most important resources of all: memory. In the next lesson, I will generalize the topic.

In my C course, all the objects you created had either static duration (meaning they lasted as long as the program) or automatic duration (meaning they lasted as long as the block in which they were declared. Here is a quick example to review

#include <iostream>

int global;
  // This is a static duration object. It exists as long as the program runs.

int main( )
{
    int local;
      // This is an automatic duration object. It exists only while
      // the block in which it is declared is executing.

    // etc...
}

In C++, these objects might be of class type and thus have constructors and destructors. Local automatic duration objects are constructed where they are declared, each time the block in which they are located is entered. Global static duration objects are constructed only once: just as the program starts executing and before main begins. Local automatic duration objects are destroyed when the block in which they are located ends. Global static duration objects are destroyed when main returns and the program ends. All of this is consistent with the usual rules for initialization of objects that are used with C.

The problem with allocating objects the way we have been doing it is that you have to know ahead of time just how many objects you need. Suppose, for example, that you wanted to read a file a line at a time. You could first allocate an array of characters to hold that line and then use one of the overloaded get functions to fill it.

#include <iostream>

int main( )
{
    char line_buffer[256 + 1];

    while( std::cin.get( line_buffer, 256 + 1 ) ) {
        // Process the text in line_buffer.
    }

    return 0;
}

The problem with this is that you have to guess ahead of time how large to make the array. What if you wanted to handle really huge lines? Should you make the array really huge? But in that case, you end up wasting a lot of memory since most lines are not that huge. Furthermore, no matter how huge you make the array, there will always be some line somewhere that is larger.

int main( )
{
    char line_buffer[1000000];
      // Waste of memory most of the time. Yet there still might be a line
      // longer than 1 million characters! What then?

    // etc...
}

What you want in a case like this is a way to create objects while the program is running without knowing ahead of time how many you will need. This is called dynamic memory allocation. You allocate the memory you need dynamically—while the program executes—and not statically when you compile it. Using dynamic memory allocation, your program can expand as it runs to use all available memory if necessary without having absurd memory requirements when it doesn't need them. All serious programs use this technique. You need to know about it.

How does it look in C++?

If you want to create an object dynamically, you first need to declare something that will hold that object's address. You need to declare a pointer. Then you need to use the special C++ operator new to create the object. The operator new will locate memory for the object, construct the object by calling the appropriate constructor if one exists, and then return the address of the new object to you. Here is how it would look with just integers.

int main( )
{
    int *p;  // Used to hold the address of the dynamic object.

    p = new int;
      // Create a new integer some place and put its address into p. In
      // this example, the integer will have an indeterminate initial value.

    *p = 10;
      // Put a 10 into the dynamically created integer.

    // etc...

    delete p;
      // Remove the dynamically created integer and return the memory it
      // was occupying to the system for later reuse.
}

There are several points to notice about this.

  1. Dynamically allocated objects do not have names. However, you need to create a named pointer to hold the address of such an object.

  2. To access the object, you have to go through the pointer. You'll need to use the indirection operator (the asterisk) to do this. If the object is a structure or class object, you'll probably want to use the arrow operator (->) a lot.

  3. You must not forget to delete the object when you no longer need it. Dynamic objects are not automatically removed when the block in which they are created exits. If you forget to delete an object but lose the pointer that was holding the object's address, you'll never be able to access the object again, yet it will still exist. A program that does this is said to leak memory. It is a common, and often serious, problem.

  4. If the object you create dynamically has a constructor, the compiler will ensure that it is called before you get the pointer back from operator new. If the object you create dynamically has a destructor, the compiler will call it when you delete the object. Thus, dynamically created objects will get initialized and cleaned up properly.

It is very important to remember that in C++ you must explicitly delete dynamic objects that you no longer need. Other languages have garbage collection and arrange to automatically reclaim objects that can no longer be used. C++ is not like that. Some feel that this is a major disadvantage of C++.

In practice, objects that need to use dynamic memory internally normally allocate it in their constructors and release it in their destructors. Since the compiler calls destructors automatically, it is normally easy to ensure that all dynamically allocated objects get released properly. You will see some important examples of this later.

Dynamically allocated arrays.

It might seem pointless to dynamically allocate an object when you still need a named pointer around to hold its address. In practice, this isn't much of an issue. Often you will want to dynamically allocate an array, the size of which is calculated just before the allocation. Here is a silly example:

#include <iostream>

int main( )
{
    char *buffer;
    int   size;

    std::cout << "How large a buffer should I make? ";
    std::cin  >> size;

    buffer = new char[size];
      // Allocate an array of size "size". How large this is depends on user input.

    // Now use the array buffer[0] to buffer[size - 1].

    delete [] buffer;
    return 0;
}

This program has no idea how much memory it will need. It asks the user when it runs. It then uses operator new to allocate an array of size characters. Depending on what the user says, that might be 100 or that might be 1 million. (Probably the program should check to verify that size is not negative). After allocating the array, the program then uses it. When it is finished with the array, the program deletes it.

Notice that deleting an array looks a little different from deleting a single object. You must include (empty) square brackets in the delete statement as I show above. If you don't do this, you might only end up releasing the first object in the array. When the compiler sees you deleting a pointer to character, it figures that you probably only want to delete a single character! Notice, however, that you don't have to specify the size when you delete the array.

When you allocate an array of objects with constructors, the compiler arranges to call the default (no parameter) constructor for each object in the array before new gives you the final pointer. Similarly, when you delete an array of objects with destructors, the compiler arranges to call the destructor on each object before actually returning the memory to the system.

A practical example.

Suppose you wanted to read a file a line at a time. Probably you want to handle very long lines should they occur but don't want to statically specify a large buffer size. How can you do this? I have written a function (in the attached file get_long_line.cpp that reads the standard input a line at a time and returns a pointer to an array of characters containing each line. It is intended to be used like this:

int main( )
{
    char *line;

    while( line = vtsu::get_long_line( ) ) {
        // Process the null terminated string pointed at by Line.
        delete [] line;
    }
    return 0;
}

This example is quite instructive. Please study it over carefully.

The function get_long_line (which is in namespace vtsu) reads the standard input device one character at a time. As it goes, it dynamically allocates an array to hold what it has read. If the array fills up and there is still more input, it allocates a new array, copies the contents of the old array into the new one, and deletes the old one. In this way, it can keep reading input without ever running out of room to put it.

When get_long_line finally sees a '\n' character on the input (or comes to the end of the input file), it puts a null character at the end of the array and returns a pointer to the array it has allocated. If it doesn't read anything because it encounters the end of the file right away, it will return a NULL pointer.

The main program above calls get_long_line in a loop. If it returns the NULL pointer, the loop condition will be false and the program ends. Otherwise, for each line returned, the program does some processing on that line and then deletes the line. The delete is very important. The get_long_line function can't delete the line it is returning (of course!) so it becomes up to the caller to do it. If the main function forgot to delete the line, the program would leak memory.

Unlike my earlier example, this program can process lines of arbitrary length. If the user chooses to enter a line with 10 million characters in it, the program will process it fine. This is an extremely important ability.

Now let's look at how get_long_line works in more detail. I refer you to the attached file, get_long_line.cpp. I will copy highlights from that file into this lesson to make it easier for me to talk about them here. However, you should probably at least review the entire file as well.

The get_long_line function looks fairly complicated. In some ways it is. It starts out by allocating an array of 16 characters.

// Create an initial buffer.
buffer          = new char[16];
buffer_size     = 16;
character_count = 0;

By allocating more than I actually need (at first), I reduce the number of times I have to do a dynamic allocation. Dynamically allocating memory is prone to being slow. It would be undesirable to do it for each and every character input. As a result, I have to distinguish between the number of "interesting" characters in the array and the size of the array. The code above allocates an array of 16 characters, sets buffer_size to 16 and character_count to zero. As the function runs, it will update these values accordingly.

The function then goes into a loop where it reads the standard input device. Most of the time, there will be free space in the buffer, and the function can just install the next character directly

buffer[character_count++] = ch;

This puts ch into the current buffer position, and then afterward (because of the POST-increment) it advances the character count. Occasionally the buffer will be full when a new character is input. In that case, the function allocates a new one.

buffer_size *= 2;
new_buffer = new char[buffer_size];
std::memcpy( new_buffer, buffer, character_count );
delete [] buffer;
buffer = new_buffer;

// Don't forget to put the character into it too!
buffer[character_count++] = ch;

Notice that the new buffer is twice the size of the existing one. The function uses memcpy from the C standard library to copy the characters that have already been input into the new buffer. Then it deletes the original buffer and makes the new buffer the current buffer. The line buffer = new_buffer is just copying an address from one pointer object to another. The actual buffer is not copied or moved here.

Finally, at the end of the function (after the loop has terminated) the buffer is resized to the precise size necessary and its address is returned. It is left up to the caller to delete the final buffer. In the special case where there was no input, the function does take responsibility for deleting the (empty) buffer before returning the NULL pointer. This is necessary because the caller won't be able to delete the buffer in this case. If get_long_line didn't delete the buffer here, it would leak a little memory every time it came to the end of the file. Can you see that finding all the possible memory leaks in a program can be difficult? It's no wonder that programmers often miss a few.

Doubling the buffer size at each reallocation is much better than just expanding its size by a constant amount. At first, the buffer is 16 characters long. When it fills, it will be expanded to 32 characters. After another 16 characters are added to fill it again, it will be expanded to 64 characters. The theory is that if the line is long enough to fill 1 million characters (for example), it's probably long enough to fill another million. Expanding the buffer by a small fixed size each time, such as 16 bytes, would be pretty silly if the buffer was already 1 million characters long. Keep in mind that each reallocation also involves copying the buffer. That can take quite a bit of time if the buffer is very long. You want to arrange things so that copying is done less and less often the longer the buffer gets. That is exactly how get_long_line works.

So why isn't get_long_line in the standard?

You might think that the C++ standard library should have a function like get_long_line in it. After all, the ability to get arbitrarily long lines of text is useful and widely needed. Surely it is something that should be standardized.

In fact, the C++ standard does even better. The standard library contains a string class that can hold strings of arbitrary length. All the memory allocation necessary to make that work is done inside the class's methods. The users of std::string don't have to worry about it. In C, you had to use arrays of characters to hold strings. That is very primitive. In C++ you can create your own fully dynamic string type. The standard has such a type already. I will talk about the standard string type in more detail in Lesson #10. Once you have learned about it, you won't want to go back to C-style arrays of characters... and you shouldn't!

Running out of memory.

At this point, you might be wondering what happens if there isn't enough memory to satisfy a call to operator new. What if you try to allocate 1 billion bytes of memory by doing something like

char *p = new char[1000000000];

Since many systems support virtual memory, a technology that allows the operating system to use disk space to "simulate" memory, it's often harder to force a program to run out of memory than it appears. Even if you ask for more memory than the system physically has, your request might succeed! But the fact remains that no system can provide an infinite amount of memory. No matter how much (virtual) memory your system has, it might not be able to honor your requests.

In C++ errors are handled using a language feature called exceptions. I do not want to talk about exceptions much right now, but I will say that operator new throws an exception if it can't allocate the memory you want. If you don't do anything to deal with that, your program will end abruptly at that point. That might sound harsh, but it is often the best you can do. When you run out of memory, it is usually difficult to do anything meaningful afterward.

However, you can at least (try to) print out an error message. You can do this by catching and handling the out of memory exception:

#include <iostream>
#include <new>      // Where std::bad_alloc is declared.

int main( )
{
    try {
        // This is where you put your program. You can call functions here
        // just like always. If an exception is thrown in a function, it
        // will end up here anyway (unless it is handled before it gets
        // here).
    }
    catch( std::bad_alloc ) {
        std::cerr << "Out of memory!\n";
    }
    return 0;
}

I will have much more to say about exception handling in later lessons.

Summary

  1. Dynamic memory is memory that is obtained by the program while it is running. Using dynamic memory, a program can adjust its memory consumption on demand without knowing ahead of time how much memory it will require. This is a very important ability. Most programs make extensive use of dynamic memory.

  2. In C++ you allocate an object dynamically using new. The result of new is the address of the new object. For example:

    std::string *p = new std::string;
    

    If a constructor has been defined for the object, it will be executed before new returns. Thus, the dynamically allocated object will be properly initialized.

    Dynamic objects are released using delete. For example:

    delete p;
    

    This causes the destructor of the object to execute if one has been defined.

    Arrays are allocated and freed with a slightly different syntax:

    std::string *p = new std::string[100];
    delete [] p;
    
  3. It is very important to remember to eventually free all memory that you allocate. If you fail to do this, your program is said to leak memory.

© Copyright 2023 by Peter Chapin.
Last Revised: August 4, 2023