C++ Memory Management: From Fear to Triumph, Part 2
Pages: 1, 2
Using member objects is a much more basic technique which can improve your code in several ways. With a member object, there is no longer a need to allocate memory in the constructor or free it in the destructor. If an exception leaves your constructor, the member object will be properly destroyed. As an added bonus, the default copy constructor and assignment operator work correctly (unless the member object's class contains bugs related to copying or assignment). On the other hand, a member object becomes a fixed part of your own object; you cannot delete it separately and then allocate another copy.
If you need to use an object temporarily inside a method call, declare it with an automatic (i.e., local) variable. Automatic variables are, as the name implies, automatically created and destroyed with no need for any intervention by you. Using such variables for transient objects is much less prone to error than explicit allocation and deallocation. In addition, automatic variables are created on the stack, which is typically a much more efficient process than getting memory dynamically from the heap. Most importantly, these stack-allocated variables do not surprise your users. After all, you are expected to create local variables for loop counters, array indices, etc. A local object, unless it is very big, is really the same sort of thing.
One important general consideration is to avoid requesting a lot of
memory where it is not expected. This applies both to dynamic allocation
and to the alternatives described here. For example, while objects of a
BigBuffer may be ten megabytes in size, such
Character objects are rarely a good design. A ten
Character, even if it does not perform dynamic
memory allocation itself, still requires at least ten megabytes of
storage. A user would be very confused if creating a thousand
Characters caused an out-of-memory condition.
Keep in mind that memory allocation is a side effect, and try to avoid surprising your users. In some cases, it is even worthwhile to make users allocate their own memory, either directly by requiring a buffer or indirectly by mandating an allocator. At other times, it is better to provide default behavior (e.g., a default allocator) while allowing other options to be specified. Often, using member objects to avoid additional allocation (although the member objects themselves might allocate) is the best solution due to its inherent simplicity. Transient objects that are local to a method call are best declared as automatic variables. If you really must allocate behind the scenes, then do so, but consider how it will affect your users and whether at least allowing an alternative would be of benefit.
With C++, You Design Memory Management Just Like Anything Else
When working with C++, memory management is just another part of the system you are building. All features of C++ are available to implement your memory management scheme. Allocator objects, mentioned previously, are an example of delegating memory allocation to a specific component of the overall program. Smart pointers are another useful tool; they are really objects that act like pointers (smart pointers are covered in the next article).
The key concept is that memory allocation, although critical, is not
special. You don't need to call
new directly every time you
need memory. It is far better to create a set of classes that will be
responsible for supplying memory to the rest of your program. Thus, memory
management becomes just another subsystem that you build. Not only does
this allow you to easily change the way you use memory without affecting
the rest of your code, but it is also much easier to debug. If there is a
memory leak, or you suspect a dangling reference, there is one clearly
defined, specific place for you to search for errors.
Both this article and the next present a number of techniques concerning memory management. Don't look at these techniques in isolation, as little code snippets to spread throughout your program. Rather, consider them as building blocks for your own memory management subsystem. With this attitude, you are well on your way toward turning C++ memory management from unwelcome drudgery into a powerful tool to improve your code.
The methods and principles of C++ memory management are readily applicable to other types of resources. Common examples include open files, network connections, and locks on shared data in a multithreaded program. These resources can also leak, such as when a program creates sockets but forgets to close them. Likewise, errors similar to dangling references can happen with other resources as well (e.g., corrupting a shared data structure because the lock on it has inadvertently been released).
These generalizations extend beyond C++. Memory-managed languages (such as Java) will mostly take care of the memory, but you must typically handle other resources (such as open files) by yourself. Thus, consistency of ownership, the side effects of resource allocation, and the need to specifically address resource management in your design are all relevant, even in memory managed languages.
With regards to C++ itself, its tools for resource management are exceptionally powerful. After all, C++ is a complex, highly expressive language which nevertheless makes you responsible for managing your own memory. It needs a powerful toolset to allow you to deal effectively with such a fundamental resource.
For example, memory-managed languages typically lack a reliable
destructor. Since the language takes care of the memory, you should not
often worry about what happens to unused objects. They are simply cleaned
up by the system eventually. Unfortunately, if you want the unused object
to close a file, the most common recourse is to write an explicit
close method, and then remember to call it. This solution is
hardly elegant and is also highly prone to errors. On the other hand, the
destructor of a carefully written C++ class will take care of the open
Powerful resource handling primitives is one reason why C++ is such a great choice for user-space servers. These servers are often very complicated, having to deal with multiple network connections, open files, and a number of subsystems shared by several threads. Given an environment where correct operation requires precise, efficient control over so many different resources, C++ has a distinct advantage over the other mainstream languages.
The next article continues this discussion by presenting several key resource management techniques that you can incorporate into your own designs. While the presentation focuses on memory management, the techniques are, of course, applicable to many other resource types.
This information appears in the previous article. It is also included here for your convenience.
A number of very useful resources are available regarding C++. Notes on these resources are provided here (the Bibliography itself follows).
First, you need a book with broad coverage, which can serve as an introduction, a reference, and for review. Ira Pohl's C++ by Dissection [Poh02] (for which the author of this article was a reviewer) is an example of such a book. It features a particularly gentle ramp-up into working with the language.
In addition to a book with broad coverage, you will need books that focus specifically on the most difficult aspects of the language, and present techniques to deal with them. Three titles that you should find very valuable are Effective C++ [Mey98], More Effective C++ [Mey96] (both by Scott Meyers) and C++ FAQs [Cli95] (by Marshall P. Cline and Greg A. Lomow). There is also an online version of the last title.
The key to reading all three books is not to panic. They contain a great deal of difficult technical details, and are broken up into a large number of very specific topics. Unless you are merely reviewing material with which you are already familiar, reading any of these books from cover to cover is unlikely to be useful.
A good strategy is to allocate a little time (even as short as 15 minutes) each day to work with any one of the Meyers' books or with C++ FAQs. Begin your session by looking over the entire table of contents, which, in all three books, has a very detailed listing of all the items covered. Don't ignore this important step; it will take you progressively less time as you become familiar with each particular book.
Next, try to read the items that are most relevant to the current problem that you are trying to solve, ones where you feel that you are weak, or even those that seem most interesting to you. An item that looks completely unfamiliar is also a good candidate: it is likely an important aspect of C++ that you are not yet aware of.
Finally, when you want insights into bureaucracy, tips on what to do with your icewater during NASA meetings (answer: dip booster rocket O-ring material into it), or just a good laugh when you are frustrated with C++, try Richard P. Feynman's "What Do You Care What Other People Think?" [Fey88].
[Cli95] Marshall P. Cline and Greg A. Lomow. C++ FAQs: Frequently Asked Questions. Addison-Wesley Publishing Co., Inc.. Copyright © 1995. 0-201-58958-3.
[Fey88] Richard Feynman and Ralph Leighton. "What Do You Care What Other People Think?": Further Adventures of a Curious Character. W.W. Norton & Company, Inc.. Copyright © 1998 Gweneth Feynman and Ralph Leighton. 0-393-02659-0.
[Mey96] Scott Meyers. More Effective C++: 35 New Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc.. Copyright © 1996. 020163371X.
[Mey98] Scott Meyers. Effective C++: 50 Specific Ways to Improve Your Programs and Designs. Second. Addison-Wesley. Copyright © 1998. 0-201-92488-9.
[Poh02] Ira Pohl. C++ by Dissection: The Essentials of C++ Programming. Addison-Wesley. Copyright © 2002. 0-201-74396-5.
George Belotsky is a software architect who has done extensive work on high-performance internet servers, as well as hard real-time and embedded systems.
Return to the Linux DevCenter.