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 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;
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.
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.
// memory.cpp - Script 11.1
#include <iostream >
main()
function:
int main() {
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.
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
.
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.
delete
statement.
delete i;
i = NULL;
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.
main()
function:
std::cout << "Press Enter or Return to continue.";
std::cin.get();
return 0;
}
memory.cpp
, compile, and debug as necessary.So far you didn’t actually use the memory, but you’ll be getting to that in the next sections.
• 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 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 struct
s, 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.
// company.cpp - Script 11.2
#include <iostream>
#include <string>
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.
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.
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.
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.
";
}
main()
function.
int main() {
Company
in memory.
Company *company = new Company("Pearson");
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.
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.
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.
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.
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
!
company.cpp
, compile, and run the application (Figure 11.4).
• 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
.
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];
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.
// array.cpp - Script 11.3
#include <iostream>
#include <string>
main()
function:
int main() {
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!
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.
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]
).
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.
delete[]
statement.
delete[] x;
x = NULL;
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.
array.cpp
, compile, and run the application (Figure 11.6).
Enter any positive integer when asked for it by the program.
You can repeat this as many times as you want with different values. A different amount of memory is requested and returned each time.
• 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!
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.
company.cpp
(Script 11.2) in your text editor or IDE.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.
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);
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
.
company2.cpp
, compile, and run the application (Figure 11.8).
The output should look exactly as it did in the company.cpp
example.
• 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
).
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:
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.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.
// copyctor.cpp - Script 11.5
#include <iostream>
#include <string>
MyClass
.
class MyClass {
public:
int
.
MyClass(int *p);
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.
~MyClass();
Because the class stores a pointer and owns the associated memory, we also need a destructor to free that memory.
MyClass &operator=(const MyClass &rhs);
Again, the prototype uses the syntax already suggested.
private:
int *ptr;
};
The single attribute is a pointer to an integer. This is just for simple demonstration purposes.
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.
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.
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.
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.
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).
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.
} else {
std::cout << "this and rhs are the same object, we're doing nothing!
";
}
std::cout << "Leaving assignment operator of object " << this << "
";
return *this;
}
main()
function.
int main() {
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.
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.
std::cout << "---------------------------------------------
";
{
MyClass obj5(new int(1));
obj5 = obj5;
}
This is just to see what will happen.
main()
function:
std::cout << "---------------------------------------------
";
std::cout << "Press Enter or Return to continue.";
std::cin.get();
return 0;
}
copyctor.cpp
, compile, and run the application (Figure 11.9).
• 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.
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.
company.cpp
(Script 11.4) in your text editor or IDE.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.
int Publisher::getNumberOfPublishedBooks() {
return booksPublished;
}
Very simple: just return the booksPublished
attribute.
main()
, up until the Press Enter or Return... line.
We’ll come up with new primary behavior for the main()
function.
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
.
Publisher
.
Publisher *publisher = (Publisher*)company;
Using the casting syntax described already, a new pointer is created and initialized using the existing company
pointer.
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.
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
.
company3.cpp
, compile, and run the application (Figure 11.10).
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).
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.
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.
company3.cpp
(Script 11.6) in your text editor or IDE, if it is not already.dynamic_cast
(Script 11.7).
Publisher *publisher = dynamic_cast<Publisher*>(company);
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.
} else {
std::cout << "company does not point to a Publisher!
";
}
company4.cpp
, compile, and run the application (Figure 11.12).
createCompany()
call, compile, and run the program again (Figure 11.13).
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.
• 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.
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.
• 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.