11. Dynamic Memory Management

So far, every one of this book’s programs has used a fixed amount of memory to accomplish its task. This fixed amount of memory was known and specified (in the form of a variable) when the program was written. These programs could not increase or decrease the amount of memory available for the storage of user data while the program was running. Instead, such changes had to be done in the program’s source code file, and the program had to be recompiled and re-executed.

But the real world is dynamic, and so is the input that has to be processed by a C++ program (for example, users need to be able to submit a varying amount of text in most applications). To handle situations where the amount of data to be stored is not known in advance, you have to use dynamic memory in your C++ programs.

Dynamic memory allows you to create and use data structures that can grow and shrink as needed, limited only by the amount of memory installed in the computer on which they are running. In this chapter you’ll learn how to work with memory in this flexible manner.

Static and Dynamic Memory

Static memory is what you have seen and used so far: variables (including pointer variables), arrays of fixed size, and objects of a given class. You can work with these blocks of memory in your program code using their names as well as their addresses (as you saw in Chapter 6, “Complex Data Types”).

With static memory you define the maximum amount of space required for a variable when you write your program:

int a[1000]; // Fixed at run time.

Whether needed or not, all of that memory will be reserved for that variable, and there is no way to change the amount of static memory while the program runs.

Dynamic memory is different. It comes in blocks without names, just addresses. It is allocated when the program runs, taking chunks from a large pool that the standard C++ library manages for you.

To request some memory from the available pool, use the new statement. It allocates the appropriate amount of memory for the indicated data type. You don’t have to worry about the size of this type: the compiler knows the size very well, and it’ll calculate how many bytes must be allocated. If enough memory is available to satisfy your request, the new statement will return the starting address of the newly allocated block. You usually store this address in a pointer variable for later use (Figure 11.1):

int *i = new int;

Figure 11.1. An unnamed block of memory is allocated to the program while the program is running. The pointer i stores the address of the allocated block.

image

If there is not enough memory available to the program, new will throw a std::bad_alloc exception. This exception will cause the program to end (see Chapter 10, “Error Handling and Debugging,” for more on exceptions).

When you are done with the memory block, you return it to the pool using the delete statement. As an extra precaution, the pointer should be set to NULL after you have freed the associated memory (Figure 11.2):

delete i; // Releases the memory.
i = NULL; // Clears the pointer.

Figure 11.2. Use delete to “reset” the memory back to its preallocation state (although space for the pointer is still reserved).

image

By taking this final step, the program knows that i no longer refers to a block of memory. This step ensures that any subsequent uses of i (unless it is assigned another value beforehand) will fail, rather than create hard-to-debug oddities.

The most crucial rule when it comes to dynamic memory is that every new statement must be balanced by a delete. A missing or double delete call is considered a bug (specifically, a missing delete statement creates a memory leak).

Let’s write our first dynamic memory example using new and delete. The example is short but introduces the basic syntax that will be used again in all subsequent examples, so it is important that you to understand this process clearly.

To request and return dynamic memory

  1. Create a new file or project in your text editor or IDE (Script 11.1).

    // memory.cpp - Script 11.1
    #include <iostream >

  2. Begin the main() function:

    int main() {

  3. Define a pointer to int variable:

    int *i;

    Remember that dynamically created memory blocks are accessed using their address, so a pointer must be used to refer to them.

  4. Allocate a new int and assign its address to i.

    i = new int;

    The address returned by the new statement—which indicates the starting address of the requested block of memory—will be assigned to the integer pointer i.

  5. Print the address.

    std::cout << "Address of allocated memory is " << i << " ";

    To allow you to confirm that the process worked and to view the minimal results, the address stored in the pointer will be printed. The compiler knows that i is a pointer and will therefore print the address in hexadecimal format.

  6. Return the block to the pool using the delete statement.

    delete i;
    i = NULL;

    Script 11.1. The new and delete statements are used to allocate and release dynamic memory.

    image

    image

    The delete statement should be followed by the address of the block to be returned to the pool. After delete, the specific block of memory pointed to by the address may no longer be used by the program. In other words, referring to i may cause problems because it no longer points to a reserved block of memory. For this reason, the second line sets the value of i to NULL, so that any inadvertent references to i after this will quite clearly be wrong.

  7. Complete the main() function:

        std::cout << "Press Enter or Return to continue.";
        std::cin.get();
        return 0;
    }

  8. Save the file as memory.cpp, compile, and debug as necessary.
  9. Run the application (Figure 11.3).

    Figure 11.3. The base address of the memory block returned by the new statement is printed by this program. This simple test confirms that the process worked and demonstrates the proper syntax.

    image

    So far you didn’t actually use the memory, but you’ll be getting to that in the next sections.

image Tips

• Note that the term static memory used in this section has nothing to do with the static C++ keyword. The term static memory as used in this section means that the amount of memory is fixed at compile time (when you build the program) and cannot be changed during run time (while the program is executing).

• The block of memory returned by new may be filled with random garbage. Most of the time, this is not a problem, because you usually write to memory prior to ever reading from it, or because your classes have constructors that initialize everything.

• In Chapter 6, it was demonstrated how the reinterpret_cast operator could be used to display the address as a long number, rather than a hexadecimal one. That syntax is:

reinterpret_cast<unsigned long>(i)

Allocating Objects

Allocating objects works exactly the same way as with primitive types (integers, real numbers, and characters). You can request memory from the pool using new, and you have to free it using delete (this concept was briefly introduced in Chapter 9, “Advanced OOP”).

If the class’s constructor takes parameters, just pass them in parentheses after the class name, exactly as you do when creating static objects:

Company *company = new Company("IBM");

This code allocates an instance of the class Company and passes the string IBM to the constructor. Then it assigns the address of this instance to the pointer variable company.

You can now use this pointer to a Company like any other pointer, and even call the class methods or access the class attributes. The only difference is that you have to use the pointer-member operator when doing so. The syntax is the same as with structs, using ->:

company->printInfo();

This line of code will call the method printInfo() of the object company is pointing to.

Now here is one of the interesting things about using pointers and objects this way: Ordinarily, if you have a pointer to one type (like an int) and you want to use it as a pointer to another type (like a float), you have to type cast it to convert the pointer. But when you create a pointer to a class, you can use that same pointer for new inherited classes, without type casting. For example:

Pet *trixie = new Pet("Trixie");
delete trixie;
trixie = NULL;
trixie = new Dog("Trixie");
delete trixie;
trixie = NULL;

Let’s try an example to make this clear.

To create new objects

  1. Create a new file or project in your text editor or IDE (Script 11.2).

    // company.cpp - Script 11.2
    #include <iostream>
    #include <string>

  2. Declare a class Company that has a name and a method to print out information about itself.

    class Company {
    public:
        Company(std::string name);
        virtual void printInfo();
    protected:
        std::string name;
    };

    Note that the printInfo() method is virtual, so that the compiler is forced to use the run-time type information to decide which method to call. We talked about this in Chapter 9.

  3. Declare a class Publisher that inherits from Company.

    class Publisher : public Company {
    public:
        Publisher(std::string name, int booksPublished);
        virtual void printInfo();
    private:
        int booksPublished;
    };

    A Publisher has an additional attribute that stores the number of books the publisher has released.

    Script 11.2. This program shows how you can assign pointers to objects. Specifically, the pointer can be to either a base class or its inherited classes, without requiring any conversions.

    image

    image

    image

    image

  4. Implement the constructor and printInfo() methods for the class Company.

    Company::Company(std::string name) {
        this->name = name;
    }
    void Company::printInfo() {
        std::cout << "This is a company called '" << name << "'. ";
    }

    This should be nothing new to you by now.

  5. Implement Publisher’s methods.

    Publisher::Publisher(std::string name, int booksPublished) : Company(name) {
        this->booksPublished = booksPublished;
    }
    void Publisher::printInfo() {
        std::cout << "This is a publisher called '" << name << "' that has published " << booksPublished << " books. ";
    }

  6. Begin the main() function.

    int main() {

  7. Create a new instance of Company in memory.

    Company *company = new Company("Pearson");

  8. Call the printInfo() method.

    company->printInfo();

    Note that we have to use the -> operator, because company is a pointer. If this were a regular object, you would use company.printInfo() instead.

  9. Delete the company, as it’s no longer needed.

    delete company;
    company = NULL;

    Remember that you should also set the value of the pointer to NULL, as the pointer no longer points to a valid memory block.

  10. Create a new instance of Publisher and assign it to company.

    company = new Publisher ("Peachpit",99999);

    The company pointer was originally defined as a pointer to Company. Here it is being assigned a value as a pointer to Publisher. Because Publisher inherits from Company, there’s no need to cast the value returned by new. Because of inheritance, everything that is a Publisher is also a Company.

  11. Call the method printInfo() on company again.

    company->printInfo();

    Because printInfo() is declared virtual in Company, the correct version (i.e., the version in Publisher) will be called. You’ll see this when you run the application.

  12. Complete the program.

        delete company;
        company = NULL;
        std::cout << "Press Enter or Return to continue.";
        std::cin.get();
        return 0;
    }

    You must use another delete statement here, as you used another new earlier. For each new there must be a matching delete!

  13. Save the file as company.cpp, compile, and run the application (Figure 11.4).

    Figure 11.4. One pointer of a Company type is used to access two new objects. The first object is of type Company; the second is of the inherited type Publisher.

    image

image Tips

• When dynamically working with objects, don’t forget to make your methods virtual. See Chapter 9 for more on this subject.

• If you’re dealing with a base class that doesn’t have any methods, add a virtual destructor, even if it is empty.

• Don’t forget to call delete before reusing your pointer variables (as we did in our example). If you don’t, then the pointer will receive the address in memory of the newly allocated block, and the program will never be able to release the first memory block, as that address would be forgotten.

• The ability to use a pointer to a base class as a pointer to an inherited class also works when a pointer should be passed as an argument. Whenever a pointer to a class X is expected, you can safely pass a pointer to a class that inherits from X. This is possible because if something inherits from X, it also has all the members of X. In other words, a pointer to Y, which inherits X, can only have more members in it (those in X, plus those added in Y), never less.

• Remember that delete only frees the memory to which a pointer variable is pointing. Even after calling delete, the pointer itself is still available, which is why Script 11.2 is able to reuse company.

Allocating Arrays of Dynamic Size

Until now, we have only allocated memory for primitive types (e.g., int) and objects. In any of these examples you could have achieved the same result by simply defining regular (non-dynamic) variables. And, although the memory was allocated at run time, the size was already known at compile time, because each request was made for a specific type, established in the program.

Memory management gets a lot more interesting if you determine the amount of memory to request from the pool at run time, and as the requested amount gets larger. In this section you will allocate room for an array of integers. To make it clear that the size of the array is dynamic, this program will ask the user to choose the size at run time (during the execution of the program).

Before you implement the example, let’s review the relationship between arrays and pointers that we introduced in Chapter 6.

The forthcoming example requires an array whose size is not known when the program is written and therefore cannot be inserted between the brackets in the array definition:

int a[???]; // How many elements?

How can you solve this problem? Recall that the combination of the array’s name and the array subscription operator (the square brackets) can be replaced with pointer arithmetic using the array’s base address. For example:

int a[20];
int *x = a;

Both a[0] and *x (where x is a pointer containing a’s address) refer to the array’s first element. Using pointer arithmetic from there, a[1] is equivalent to *(x + 1), a[2] to *(x + 2), and so on.

What helps you here is that this also works in reverse. Just pass an array declaration to new, and it will return a pointer to the array’s base type. Then, you can use the array subscription operator on the pointer variable’s name and treat the chunk of memory exactly like an array. So, if you define x as a block of memory large enough to store ten integers:

int *x = new int[10];

then you’re allowed to treat x like an array (Figure 11.5):

x[1] = 45;
x[2] = 8;

Of course, you can also use a variable that holds the number of elements in the array:

int count = 10;
int *x = new int[count];

Figure 11.5. Using pointers, you can allocate memory for an array and then assign values to its elements.

image

Deleting an array is a little different than deleting other dynamically requested memory blocks, though. Because the variable holding the address of the array is a simple pointer (x), we need to tell the compiler that it should delete an array. Do this by adding brackets directly after the delete:

delete[] x;

The following example uses this concept.

To allocate a dynamic array

  1. Create a new file or project in your text editor or IDE (Script 11.3).

    // array.cpp - Script 11.3
    #include <iostream>
    #include <string>

  2. Begin the main() function:

    int main() {

  3. Get the array’s size from the user.

    unsigned int count = 0;
    std::cout << "Number of elements to allocate? ";
    std::cin >> count;

    The first line creates the variable and initializes it as 0. Then the user is prompted on the second line. The third line reads an integer from the terminal and stores it in count.

    You can also, if you want, add some checks to this process so that a valid integer value is entered. We do declare the integer to be unsigned because we won’t want to allocate an array with a negative number of elements!

  4. Define an integer pointer variable and allocate the array.

    int *x = new int[count];

    This is a key difference between defining a variable at compile time versus run time. In every other example, the array has been a set size. Now its size won’t be known until the program runs.

    Script 11.3. The new statement can also be used to allocate arrays, but you have to use delete[] (not just delete) to free the memory.

    image

    image

  5. Using a loop, store a number in every element of the array.

    for (int i = 0; i < count; i++) {
        x[i] = count-i;
    }

    This loop runs from 0 to count-1 iterations to access every element of the array. Within the loop itself, a value is assigned to each element in the array.

    Notice how this code uses the array subscription operator on the pointer variable holding the memory block’s base address (x[i]).

  6. Using another loop, print the array’s elements.

    for (int i = 0; i < count; i++) {
        std::cout << "The value of x["
        << i << "] is " << x[i] << " ";
    }

    The syntax of the cout statement may seem a little strange, but all it is doing is printing a statement like The value of x[2] is 12.

  7. Free the memory that was used for the array using the delete[] statement.

    delete[] x;
    x = NULL;

  8. Complete the program.

        std::cin.ignore(100,' '),
        std::cout << "Press Enter or Return to continue.";
        std::cin.get();
        return 0;
    }

    Because this example takes user input, the std::cin.ignore() function is called to get rid of garbage that may still be in the input buffer.

  9. Save the file as array.cpp, compile, and run the application (Figure 11.6).

    Figure 11.6. The number of elements to be stored in an array will be determined by the value the user enters at the prompt.

    image

    Enter any positive integer when asked for it by the program.

  10. Run the application again, using a different numeric value (Figure 11.7).

    Figure 11.7. An array of varying lengthdetermined by the user-submitted numberis populated with values and then printed.

    image

    You can repeat this as many times as you want with different values. A different amount of memory is requested and returned each time.

image Tips

• You could have used pointer arithmetic on the memory block’s base address instead of using the array subscription notation. Both pointer arithmetic and array subscription will work.

• You might be surprised by how large an array you can dynamically create with this application. Even if you enter 100,000, that would create an array that’s probably 400,000 bytes in size (assuming integers require 4 bytes on your computer). But 400,000 bytes is less than half a megabyte. If you don’t have that much memory available on your computer, you’ve got problems!

Returning Memory from a Function or Method

Another common use of dynamic memory involves returning from a function pointers to memory blocks. This is especially important when you’re working with external libraries written by someone else.

Without this technique, the only things you can return from a function to the calling code are simple scalar values, such as an integer, a floating-point number, or a character. You cannot return more than one value or more complex data structures, like arrays. Thus you need dynamic memory if you want to return something other than a simple value.

To accomplish this, a function will allocate memory for an object or primitive type using new and then return the pointer to that memory to the main part of the application. The main part of the application would then use the memory as needed and free the memory (call delete, that is) as soon as it is no longer needed. For example:

int *newInt(int value = 0); //
Prototype
int main () {
    int *x = newInt(20); // Request
    std::cout << *x; // Print value
    delete x; // Delete
    x = NULL; // Nullify
}
int *newInt(int value) {
    int *myInt = new int; // Allocate
    *myInt = value; // Assign value
    return myInt; // Return pointer
}

The following example will demonstrate this concept. We’re going to enhance the previous company example by adding a function that creates—depending on the arguments passed—an instance of either Company or Publisher. You’ll quite often find such functions, called factory functions, in object-oriented programs.

To allocate and return an object from a function

  1. Open the file company.cpp (Script 11.2) in your text editor or IDE.
  2. Just before the main() function, add the following prototype (Script 11.4):

    Company *createCompany(std::string name, int booksPublished = -1);

    This function will return an instance of Company if booksPublished is less than 0, or an instance of Publisher if booksPublished is greater than or equal to 0. The default value of booksPublished is -1, so if we want to create a Company, we can just pass a name.

  3. Rewrite the main() function so that it creates new objects by calling the createCompany() function. The assignments to the variable company should look like this:

    Company *company = createCompany("IBM");
    company = createCompany("Peachpit",99999);

    Script 11.4. Use factory functions to instantiate and return different objects depending on some parameters.

    image

    image

    image

    image

    image

  4. Implement the createCompany() function after main().

    Company *createCompany(std::string
    name, int booksPublished /* = -1 */)
    {
        if (booksPublished < 0) {
            return new Company(name);
        } else {
            return new Publisher(name, booksPublished);
        }
    }

    The function takes two arguments: the name of the company and, optionally, how many books the company has published. If booksPublished is less than zero, a new instance of Company is created and returned, else a Publisher. Note that the function can return a pointer to Publisher, even though the declared return type is a pointer to Company. This is allowed only because Publisher inherits from Company, in other words, because Publisher is-a Company.

  5. Save the file as company2.cpp, compile, and run the application (Figure 11.8).

    Figure 11.8. The results are the same as in Figure 11.4 (albeit with different company names), but we’re using a factory function now.

    image

    The output should look exactly as it did in the company.cpp example.

image Tips

• You might wonder what we achieved with the factory function, since it’s even more code than before. The main advantage is that we were able to put the code that creates a Company or Publisher into one function. Now, whenever we need a new instance, we can just call this factory function. Even if you decide to add another class ReallyHugePublisher (e.g., for Publishers that have more than a million books in their catalog), you only have to change the code in one function to allow for this.

• Always use factory functions (or methods) if you need to instantiate different classes according to some criteria (like the numbers of published books in our example). Create such functions even if you think that you’re going to create the objects in only one program.

• Factory functions or methods are most useful when you’re creating objects based on some stored data, like a file.

• You have to watch for memory leaks in situations like this where one function allocates the memory (createCompany(), using new) but another function has to release the memory (main(), using delete).

The Copy Constructor and the Assignment Operator

In Chapter 7, “Introducing Objects,” it was mentioned that you can assign one object to another variable of the same type. The compiler will then generate code that assigns every attribute value in the one to the corresponding member variable of the “target.” This is known as a bitwise copy.

While this is fine in most cases, you’ll run into problems when an object has member variables that are pointers. With a bitwise copy of the members, you’ll end up having two instances of a class that both contain pointers to the same address. If an object is deleted, it will delete the pointers as well. This could be a problem if the other object refers to that pointer or when the other object is destroyed, as it would attempt to free the same block of memory for the second time (resulting in a crash).

What can we do to solve this problem? It’d be ideal if the programmer could specify exactly what should be done when a copy of a class is needed. The C++ language designers foresaw this problem and provided a solution for it. Unfortunately, they made the solution a little more complicated than it should be, but we’ll guide you through it slowly.

Look at the following lines of code:

MyClass obj1;
MyClass obj2;
obj2 = obj1;

The first two lines are plain and simple: Two instances, obj1 and obj2, of MyClass are created. On the third line, the value of obj1 is assigned to obj2, possibly leading to the pointer problem already discussed. So, how can we intercept the assignment and dictate how the pointers should be handled? The answer is operator overloading! In Chapter 9 you learned that almost any operator in C++ can be overloaded, and this is also true for the assignment (=) operator. The signature of this operator looks a bit busy:

MyClass &operator=(const MyClass &rhs);

This signature tells us the following:

  1. The method expects as its argument a constant reference to MyClass. We’re using a reference here so that compiler does not create a copy when passing the parameter (not doing so would result in bad performance, and could also lead to endless recursion). Because we’ll only be reading from the parameter (and not changing its value), we mark the reference to be constant.
  2. The method returns a reference to an object of class MyClass. This is not really necessary, but good style. Returning a reference allows the programmer to chain assignments (a = b = c), which the programmer may be used to when working with primitive types (a = b = c = 2). As we saw in Chapter 9, overloaded operators should behave like their built-in cousins, so we better prepare for chaining.

    Unfortunately, just overloading the assignment operator is not yet the perfect or complete solution. As we mentioned, the language designers made our lives more complicated than they should have (in our humble opinion). Let’s rewrite the earlier three lines to see how this plays out:

    MyClass obj1;
    MyClass obj2 = obj1;

    The difference from the previous code is in the details. Instead of creating two instances and then assigning obj1 to obj2, we’re now creating just one instance, obj1. Then instance obj2 is created and at the same time initialized with value of obj1. Although this looks like a simple assignment, the compiler will generate totally different code. It looks for a so-called copy constructor in MyClass, and if there is none, it will create one that does a bitwise copy of obj1, even if we overloaded the assignment operator already. In simplest terms, even though we have indicated how assignments should work with this class, the problematic bitwise copy still comes into play in this case. So we need to define a copy constructor to handle this potential situation.

    The signature of the copy constructor is:

    MyClass(const MyClass &rhs);

    The constructor expects a constant reference to MyClass as an argument, just as the assignment operator did. Because it is a constructor, it doesn’t have a return type (remember constructors and destructors never have a return type).

    Let’s see how this all works with our next example. To keep it simple and to be able to concentrate on the important stuff, we will create a class that contains only a pointer to an integer, a copy constructor, and an overloaded assignment constructor.

To add a copy constructor and an overloaded assignment operator

  1. Create a new file or project in your text editor or IDE (Script 11.5).

    // copyctor.cpp - Script 11.5
    #include <iostream>
    #include <string>

  2. Begin the declaration of the class MyClass.

    class MyClass {
    public:

  3. Declare a regular constructor that takes a pointer to int.

    MyClass(int *p);

  4. Declare the copy constructor.

    MyClass(const MyClass &rhs);

    This syntax follows the examples indicated before. This is just an overloaded method: a second constructor with the same name that takes different arguments.

  5. Declare a destructor.

    ~MyClass();

    Because the class stores a pointer and owns the associated memory, we also need a destructor to free that memory.

  6. Declare the assignment operator.

    MyClass &operator=(const MyClass &rhs);

    Again, the prototype uses the syntax already suggested.

  7. Declare the attribute and complete the class.

    private:
        int *ptr;
    };

    The single attribute is a pointer to an integer. This is just for simple demonstration purposes.

    Script 11.5. This program shows why you must always implement a copy constructor and an assignment operator if your classes have pointer attributes and own the allocated memory.

    image

    image

    image

    image

    image

    image

    image

  8. Implement the regular constructor.

    MyClass::MyClass(int *p) {
        std::cout << "Entering regular constructor of object " << this << " ";
        ptr = p;
        std::cout << "Leaving regular constructor of object " << this << " ";
    }

    The constructor and all of the other methods will use lots of printed messages so that you’ll later see exactly what happens and when.

  9. Implement the copy constructor.

    MyClass::MyClass(const MyClass &rhs) {
        std::cout << "Entering copy constructor of object " << this << " ";
        std::cout << "rhs is object " << &rhs << " ";
        *this = rhs;
        std::cout << "Leaving copy constructor of object " << this << " ";
    }

    Because we know that the class also contains an assignment operator, we choose the “easy option” in the copy constructor and just assign rhs to *this when a copy is made. Remember that this is a pointer to the current object, so it needs to be dereferenced to be able to use the assignment. Again, add lots of printout.

  10. Create the destructor.

    MyClass::~MyClass() {
        std::cout << "Entering destructor of object " << this << " ";
        delete ptr;
        std::cout << "Leaving destructor of object " << this << " ";
    }

    The destructor is easy: Just free the memory that ptr is pointing to. For each use of this in these cout lines, the program will print the address in memory where the object is stored.

  11. Begin defining the overloaded assignment operator.

    MyClass &MyClass::operator=(const MyClass &rhs) {
        std::cout << "Entering assignment operator of object " << this << " ";
        std::cout << "rhs is object " << &rhs << " ";

    The most complex method is the assignment operator. It will start by printing out where we are in the code.

  12. Add a conditional that checks if the programmer has attempted to assign the object to itself.

    if (this != &rhs) {

    The assignment operator should only start doing something if the objects involved are not the same. To check for this, we compare the addresses of both objects (one of which is stored in the this pointer and the other of which was passed by reference to this method).

  13. If the objects are different, delete the current ptr and create a copy of the other pointer.

    std::cout << "deleting this->ptr ";
    delete ptr;
    std::cout << "allocate a new int and assign value of *rhs.ptr ";
    ptr = new int;
    *ptr = *rhs.ptr;

    As we said when we introduced this topic, the problem with copying objects is that they can both end up having pointers with the same value. The solution is to first delete the existing pointer (which is the problematic copy) and then create a new one. To this new one is assigned the existing value of the other pointer.

  14. If the objects are the same, do nothing and tell the user as much.

    } else {
        std::cout << "this and rhs are the same object, we're doing nothing! ";
    }

  15. Complete the operator and return a reference to the object.

        std::cout << "Leaving assignment operator of object " << this << " ";
        return *this;
    }

  16. Begin the main() function.

    int main() {

  17. Write some code, within a block, that tests the assignment operator.

    std::cout << "--------------------------------------------- ";
    {
        MyClass obj1(new int(1));
        MyClass obj2(new int(2));
        obj2 = obj1;
    }

    This first test will create two separate objects, then assign the one to the other. When the object is created, its constructor is automatically called. The constructor expects to receive a pointer to an integer. To do that, the code new int(1) is used. The first part should be familiar—it creates a pointer to an integer—and the 1 in parentheses is just another way of assigning a value to that integer.

    This code is placed within a dummy block—defined by the curly bracket—so that the objects will be deleted when the program hits the closing bracket.

  18. Now test the copy constructor.

    std::cout << "--------------------------------------------- ";
    {
        MyClass obj3(new int(1));
        MyClass obj4 = obj3;
    }

    This is a repeat of the second chunk of code used in the introduction to this section. It creates one object (whose integer value is 1) and then creates a second, initializing it at the same time.

  19. And now try to assign an object to itself.

    std::cout << "--------------------------------------------- ";
    {
        MyClass obj5(new int(1));
        obj5 = obj5;
    }

    This is just to see what will happen.

  20. Complete the main() function:

    std::cout << "--------------------------------------------- ";
        std::cout << "Press Enter or Return to continue.";
        std::cin.get();
        return 0;
    }

  21. Save the file as copyctor.cpp, compile, and run the application (Figure 11.9).

    Figure 11.9. There’s a lot of text here, but follow the addresses to observe how copy constructors and the assignment operator properly handle the pointers when assigning one object to another.

    image

image Tips

• Whenever you declare a class that has pointer attributes and frees that memory in the destructor, you need to implement a copy constructor and an assignment operator.

Never ever write a copy constructor without an assignment operator and vice versa. Having only one of them will lead to nasty problems that are very hard to debug.

• Always make sure that that the copy constructor copies over every attribute, not only the pointers. Also, always call the copy constructor from the base class, when one exists.

Static Object Type Casts

Let us recall the example company2.cpp (Script 11.4). In it we introduced a factory function that created an instance of Company, or an instance of a subclass of Company, based on the parameters the function was passed. This is fine if you only work with members of Company, but what happens when you are sure that the returned object is a Publisher (because you called createCompany("Peachpit", 99999), for example) but you need to access members that are not part of the base class? In other words, if you have company, which is at first a pointer of type Company and then a pointer of type Publisher, and the Publisher class has its own method called listAuthors(), then how would you call that method?

You might think that you could assign company to a pointer of type Publisher, like this:

Company *company = createCompany ("Peachpit",99999);
Publisher *publisher = company; // WRONG

That won’t work, though, because a Company is not a Publisher, so you can’t assign one type of object to another type of object. What you need in such a situation is a way to tell the compiler that it should assume company points to a Publisher. By doing so, you could assign company to publisher, because you’ve said they are both of the same type.

The mechanism for giving the compiler such a hint is called a cast or type cast. You’ve been introduced to the concept time and again, but now let’s focus on casting pointers to objects.

The syntax used for a pointer cast is a pair of parentheses with the desired pointer type between them, followed by the address value:

Company *company = createCompany("Peachpit",99999);
Publisher *publisher = (Publisher*)company;

This tells the system that the address stored in the company pointer variable should be interpreted as the address of an instance of Publisher. Let’s try an example so that this idea will make better sense.

To use the type cast operator

  1. Open the file company.cpp (Script 11.4) in your text editor or IDE.
  2. Add a new method to the Publisher class (Script 11.6).

    int getNumberOfPublishedBooks();

    This function takes no arguments and returns an integer, specifically, the number of books put out by that publisher.

  3. After the class declaration, implement the new method.

    int Publisher::getNumberOfPublishedBooks() {
        return booksPublished;
    }

    Very simple: just return the booksPublished attribute.

    Script 11.6. In this application, a static type cast is used in order to change the type of a pointer variable.

    image

    image

    image

    image

    image

  4. Remove the existing contents of main(), up until the Press Enter or Return... line.

    We’ll come up with new primary behavior for the main() function.

  5. Create a publisher and store it in a pointer to Company.

    Company *company = createCompany("Peachpit", 99999);

    Now we have one pointer, company, that can call any method in the Company class, but not the newly added getNumberOfPublishedBooks() method, which is defined only in Publisher.

  6. Cast the pointer to a pointer to Publisher.

    Publisher *publisher = (Publisher*)company;

    Using the casting syntax described already, a new pointer is created and initialized using the existing company pointer.

  7. Print out how many books this publisher has published.

    std::cout << "This publisher has published " << publisher->getNumberOfPublishedBooks() << " books. ";

    Because getNumberOfPublishedBooks() is a method of Publisher, but not of Company, you have to use the pointer variable publisher to call it. Calling it using company would result in an error message during compilation.

  8. Free up the memory.

    delete company;
    company = NULL;

    Note that you must not delete both company and publisher. The type cast didn’t create a copy; it merely told the compiler to assume a different type. Both company and publisher contain the same address.

    Another way of looking at the cleanup is this: the program used new only once, so it needs only one delete.

  9. Save the file as company3.cpp, compile, and run the application (Figure 11.10).

    Figure 11.10. Type casting is used to change the type of a pointer variable so that its own methods can be invoked.

    image

Performing Dynamic Object Type Casts

Although the preceding program seems to work perfectly, there’s still a potential pitfall. What happens if the pointer returned by createCompany() does not actually point to a Publisher? The compiler will still do as we told it to: it will assume that the object is a Publisher and will attempt to call the method. But because the object wouldn’t have such a method, the program will crash. You can easily verify this by removing the book count in the createCompany() call (Figure 11.11).

Figure 11.11. The application crashed (and rather cryptically at that) because it attempted to call a function that was not part of the object (regardless of the cast).

image

Because navigating in class hierarchies (and therefore type casting among those hierarchies) is important in object-oriented programming, C++ introduced a bunch of new type cast operators, which are listed in Table 11.1 (you already saw these, albeit briefly, in Chapter 2, “Simple Variables and Data Types,” and in Chapter 6). The operators are more advanced than the type cast you just saw, and most of them are quite seldom used. We’re going to demonstrate how and why you might use them with the most important one, the dynamic type cast.

Table 11.1. Although you can use the old C-style casting operators in C++, these operators provide important type checking, which will improve the reliability of your programs.

image

The syntax for a dynamic type cast is very different from the one you just learned, looking more like a function call:

Company *company = createCompany("Peachpit",99999);
Publisher *publisher = dynamic_cast<Publisher*>(company);

The desired pointer type is written between two angle brackets, followed by the value you want to cast.

In contrast to the traditional cast, the dynamic type cast actually checks if the value to be cast is of a valid type, which would be Publisher (or any subclass thereof) in our example. If the value can’t be safely cast, dynamic_cast will return NULL.

Let’s rewrite the last example to use this feature.

To use dynamic_cast

  1. Open the file company3.cpp (Script 11.6) in your text editor or IDE, if it is not already.
  2. Change the pointer casting to use dynamic_cast (Script 11.7).

    Publisher *publisher = dynamic_cast<Publisher*>(company);

  3. Before attempting to print the number of published books, check if the cast was successful.

    if (publisher != NULL) {
        std::cout << "This publisher has published " << publisher->getNumberOfPublishedBooks() << " books. ";

    If publisher couldn’t be cast, it will have a NULL value and you shouldn’t try to do anything else with it.

    Script 11.7. The dynamic_cast operator returns a non-null value only if the cast was successful.

    image

    image

    image

    image

  4. Complete the conditional.

    } else {
        std::cout << "company does not point to a Publisher! ";
    }

  5. Save the file as company4.cpp, compile, and run the application (Figure 11.12).

    Figure 11.12. If the value to be cast is of a valid type, dynamic_cast behaves like a traditional type cast, allowing you to access the methods.

    image

  6. Remove the number of books from the createCompany() call, compile, and run the program again (Figure 11.13).

    Figure 11.13. Since the dynamic_cast actually checks if a value can be cast to the desired type, you can check its return value to avoid program crashes (compare with Figure 11.11).

    image

    As you can see, the application doesn’t crash if the pointer variable points to an object of the wrong type. Instead, an error message is shown.

image Tips

• Use dynamic_cast when you’re dealing with objects. But don’t forget to actually check if the result is NULL before proceeding.

• In more complex expressions involving type casts and the dereferencing operator, you might want to add a pair of parentheses: *((int *)x). It looks awkward but ensures that the expression is evaluated the way you meant it to be.

Avoiding Memory Leaks

You already read that it is an error if a block of memory is allocated but never returned to the pool. Such a block will be freed only when the program terminates. If the program runs for a long time and continuously allocates new blocks while forgetting to give old, unused blocks back to the pool, then the program will run out of memory at some point, causing subsequent new requests to fail. If a program has such a bug, it is said to have a memory leak, because the pool of available memory leaks out.

The address returned by new is the only way to access the memory block, and it is also the only way to hand it back to the pool, using the delete statement:

int *x;
x = new int[1000];
delete[] x;
x = NULL;

This means that if this address value (stored in x) is lost, a memory leak has occurred.

The address value can be lost in many ways, for example by being overwritten in a pointer variable:

int *x;
x = new int[3000]; // block 1
x = new int[4000]; // block 2
delete[] x;
x = NULL;

After the second new statement, the result of the first statement—the address of block 1—is lost because the only copy of this address was stored in x, which has been overwritten with the address of the second block. The second block can be returned to the pool using delete[] x, but the first block cannot be freed because its address is no longer known. This is one cause of a memory leak.

Memory leaks can also occur if a pointer variable holding the block’s address does not have the proper scope:

void foo() {
    MyClass *x;
    x = new MyClass();
}

When this foo() function terminates, the pointer variable x goes out of scope, which means it no longer exists and its value is lost. There are two ways to prevent such a leak.

The first option is to call delete x, inserted somewhere before the final return statement:

void foo() {
    MyClass *x;
    x = new MyClass();
    delete x;
    x = NULL;
    return;
}

The second possibility is to return the address to the function’s caller, as the company2.cpp example did.

image Tips

• Memory leaks are very common in C++ programs. You have to be careful about where memory is allocated and freed in your own code, but you also have to study the documentation for any third-party libraries of code you use. The use of external libraries is common in C++, so you should understand the memory management philosophy or style of such external libraries. Sometimes they even have memory leak bugs themselves.

• Some programmers recommend writing the new and delete statements at the same time as you program, or at least insert reminder source code comments. You’re less likely to forget the delete call this way.

• When working with a lot of classes that pass pointers around, develop a scheme of ownership and stick to it. For example, you can state that whenever you pass a pointer to a constructor, that object becomes the owner of the received memory and is responsible for freeing it. Using such a scheme makes it easier for you to remember when a delete is necessary.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.36.38