September, 1994 - Vol. 1 No. 9
One aspect of C and C++ programming that seems to always cause problems for beginners is dynamic memory allocation. If you declare an object within a function, you know it exists within stack memory and that the compiler will delete it when the function leaves scope. However, if you allocate memory from the heap by using malloc (for C programs) or new (for C++ programs), you're responsible for freeing or deleting the object.
In situations where the creation or destruction of an object occurs at consistent, logical places, this isn't a problem. However, if you write a function that contains multiple return statements or uses Exception Handling, destroying these dynamically allocated objects is much more complex.
In this article, we'll show how you can use smart pointer
classes to simplify the cleanup of dynamically allocated objects.
First, we'll describe the basic characteristics of smart
pointers; then, we'll apply a smart pointer class in an
example application.
A smart pointer is a stack-based object (an object you create locally in a function) that controls your interaction with a heap-based object (an object you create via new). To create a smart pointer, you create a class that defines (for stack-based objects) the operations you'd normally perform via a pointer.
For example, if you want to create a smart pointer class for accessing character strings, you'll need to overload the operators you typically use with character pointers: for example, [] and =. (Obviously, the Borland C++ String class implements much of this functionality, and you'll probably want to use it for any production code. However, we'll use this example to demonstrate a simple smart pointer class.)
In memory, each smart pointer object will have a corresponding heap object, as shown in Figure A. However, since the smart pointer class overloads the standard pointer operators, the class lets you manipulate the heap object indirectly by passing those operator function calls to the corresponding heap object.
Figure A - The smart pointer object will pass all pointer operator calls to its heap object.
Because C++ programs always destroy stack-based objects when the
enclosing function exits, you can control the allocation and deallocation
of the character strings from the smart pointer's constructor
and destructor. To see how using smart pointers can simplify the
process of deleting dynamic objects, let's look at a simple
example.
Maintaining someone else's code is a fact of life for most programmers. Unfortunately, not all programmers write perfect programs or even use good judgment in designing individual functions.
For example, few experienced programmers will write a function that contains multiple return statements, unless there's a very good reason to do so. One reason is that memory leaks can become difficult to locate when the logical flow of a function isn't obvious. Figure B contains a simple program that fails to delete the variable aptr if the user calls the program with no arguments.
Figure B - If a function contains multiple return statements, memory leaks can become difficult to find.
#include <iostream.h> #include <string.h> int main(int argc, char* argv[]) { char* aptr = new char[80]; strcpy(aptr, argv[0]); char* bptr = 0; char* cptr = 0; if(argc >= 2) { bptr = new char[strlen(argv[1]) + 1]; strcpy(bptr, argv[1]); } if(argc >= 3) { cptr = new char[strlen(argv[2]) + 1]; strcpy(cptr, argv[2]); } cout << "Program - " << aptr << endl; switch(argc) { case 1: cout << "No arguments" << endl; return 1; case 3: cout << "Argument 2 - " << bptr << endl; default: cout << "Argument 1 - " << aptr << endl; } delete aptr; delete bptr; delete cptr; return 0; }
delete aptr;
immediately above the statement
return 1;
Unfortunately, if you add additional return statements,
you'll need to add the same line, and possibly others,
in order to delete one of the other character strings. Now, let's
rewrite this program by using a simple smart character pointer
class to delete the dynamically allocated memory.
To begin, launch the Borland C++ 3.1 DOS Integrated Development Environment (IDE). When the IDE's menu bar and desktop appear, choose New from the File menu and enter the code from Listing A in the empty data-file window that appears.
Listing A: smart.cpp
#include <iostream.h> #include <string.h> class smartCharPtr { char* ptr; public: smartCharPtr() { ptr = new char[1]; ptr[0] = '\0'; } smartCharPtr(char* p) : ptr(p) {} ~smartCharPtr() { cout << "deleting smartCharPtr for "; if (ptr) cout << ptr; else cout << " -"; cout << endl; delete [] ptr; } operator char*() // for strcpy() { return ptr; } operator const char*() // for cout { return ptr; } smartCharPtr& operator=(const char* newPtr) { delete [] ptr; ptr = new char[strlen(newPtr) + 1]; strcpy(ptr, newPtr); return *this; } }; int main(int argc, char* argv[]) { // store program name (argv[0]) smartCharPtr aptr(new char[80]); strcpy(aptr, argv[0]); // set up two other pointers smartCharPtr bptr = 0; smartCharPtr cptr = 0; // give program name cout << "Program - " << aptr << endl; // save others (if present) if (argc > 1) { bptr = argv[1]; cout << "Argument 1 = " << bptr << endl; } if (argc > 2) { cptr = argv[2]; cout << "Argument 2 = " << cptr << endl; } // if no args, get out if (argc == 1) { cout << "No Arguments" << endl; return 1; } return 0; }
When you finish entering the code, choose Save from the File menu. When the Save File As dialog box appears, enter SMART.CPP as the file's name and click OK.
In the smartCharPtr class, we provide two constructors, a destructor, two conversion operators, and an overloaded assignment operator (=) function. Let's look at each of these functions individually.
The constructors allow us to build a smartCharPtr object with or without the corresponding pointer. However, you'll notice that if we create a smartCharPtr object without the pointer, we initialize the ptr data member to 0. This allows us to safely call delete on this pointer at a later time.
In the destructor, we first display a message that includes the character string (if the pointer is valid). Then, we delete the ptr data member pointer.
Next, we provide two conversion operator functions. The operator char*() function allows us to pass a smartCharPtr object to a function that's expecting a char pointer. The operator const char*() function performs the same task for functions that expect a const char pointer.
Finally, we overload the assignment operator with the operator=() function. The compiler will call this function whenever we try to initialize a smartCharPtr object with a char pointer.
In the body of the main() function, we simply added the
smartCharPtr objects in place of the char pointers
and changed the associated initialization code. Not only do these
changes eliminate the memory leak that existed before, they also
make the flow of the function easier to follow.
To build and run the application, choose Run from the Run menu. When Borland C++ finishes building the application, it will shell out to DOS, run the application, and then return to the IDE.
To view the results of the program, choose User Screen from the
Window menu. When the results of the program appear, they should
be similar to
Program - C:\BORLANDC\SMART.EXE No arguments deleting smartCharPtr for - deleting smartCharPtr for - deleting smartCharPtr for C:\BORLANDC\SMART.EXE
Next, press any key to return to the IDE screen and then choose
Arguments... from the Run menu. In the Program Arguments
dialog box, enter
1 2
and click OK.
Now, run the program again. When the program returns to the IDE,
redisplay the user screen. This time, you should see
Program - C:\BORLANDC\SMART.EXE Argument 1 - 1 Argument 2 - 2 deleting smartCharPtr for - 2 deleting smartCharPtr for - 1 deleting smartCharPtr for C:\BORLANDC\SMART.EXE
You'll notice that the program deletes the smartCharPtr objects in reverse order. The program does this because it places (or pushes) variables on the program stack as you create them and then removes (or pops) them off the program stack in a first-in-last-out manner.
When you're finished viewing the output of the SMART.EXE
program, press any key to return to the IDE screen. To quit the
IDE, choose Quit from the File menu.
Here, we're simply using a smart pointer to prevent memory leaks when we exit a function that uses heap objects. While this is an important use of smart pointer classes, it's by no means the only use for them.
One simple change we could make to the class is adding code that
lets you use standard pointer syntax on the smart pointer object.
For example, if you add the function
smarCharPtr& operator *() { return *ptr; }
you can apply the dereference operator (*) to the smart
pointer (even though it's a local object) in order to return
the dereferenced pointer's value.
In the example above, we wrote a very simplistic smart pointer class for controlling the memory allocation for character pointers. However, for smart pointer classes to be really useful, you'll want to write a smart pointer template class.
By writing a generic smart pointer class as a template class,
you'll be able to easily reuse the overloaded versions
of many standard pointer operators (=, &,
*, and so on). In a future issue, we'll show how
you can write your own smart pointer template class, discuss some
other uses for smart pointers, and describe some design decisions
you'll need to make.
Designing programs that allocate and deallocate dynamic memory
correctly can be an annoying and error-prone undertaking. By using
smart pointers to automate these tasks, you can avoid memory leaks
that would otherwise be difficult to overcome.
Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.