Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |
The following straightforward debugging techniques are
explained in Volume 1:
1. For
array bounds checking, use the Array template in C16:Array3.cpp
of Volume 1 for all arrays. You can turn off the checking and increase
efficiency when you re ready to ship. (Although this doesn t deal with the case
of taking a pointer to an array.)
2. Check
for non-virtual destructors in base classes.
Tracking new/delete and malloc/free
Common problems with memory allocation include mistakenly
calling delete for memory that s not on the free store, deleting the
free store more than once, and, most often, forgetting to delete a pointer.
This section discusses a system that can help you track down these kinds of
problems.
As an additional disclaimer beyond that of the
preceding section: because of the way we overload new, the following
technique may not work on all platforms, and will only work for programs that
do not call the function operator new( ) explicitly. We have been quite careful in this book to only present code that fully conforms to the C++
Standard, but in this one instance we re making an exception for the following
reasons:
1. Even though it s technically illegal, it works on many compilers.
2. We illustrate some useful thinking along the way.
To use the memory checking system, you simply include the
header file MemCheck.h, link the MemCheck.obj file into your
application to intercept all the calls to new and delete, and
call the macro MEM_ON( ) (explained later in this section) to
initiate memory tracing. A trace of all allocations and deallocations is
printed to the standard output (via stdout). When you use this system,
all calls to new store information about the file and line where they were called. This is accomplished by using the placement syntax for operator
new. Although you
typically use the placement syntax when you need to place objects at a specific
point in memory, it can also create an operator new( ) with any
number of arguments. This is used in the following example to store the results
of the __FILE__ and __LINE__ macros whenever new is
called:
//: C02:MemCheck.h
#ifndef MEMCHECK_H
#define MEMCHECK_H
#include <cstddef> // For size_t
// Usurp the new operator (both scalar and array
versions)
void* operator new(std::size_t, const char*, long);
void* operator new[](std::size_t, const char*, long);
#define new new (__FILE__, __LINE__)
extern bool traceFlag;
#define TRACE_ON() traceFlag = true
#define TRACE_OFF() traceFlag = false
extern bool activeFlag;
#define MEM_ON() activeFlag = true
#define MEM_OFF() activeFlag = false
#endif // MEMCHECK_H ///:~
It is important to include this file in any source file in
which you want to track free store activity, but include it last (after
your other #include directives). Most headers in the standard library
are templates, and since most compilers use the inclusion model of
template compilation (meaning all source code is in the headers), the macro
that replaces new in MemCheck.h would usurp all instances of the new
operator in the library source code (and would likely result in compile
errors). Besides, you are only interested in tracking your own memory errors,
not the library s.
In the following file, which contains the memory tracking
implementation, everything is done with C standard I/O rather than with C++
iostreams. It shouldn t make a difference, since we re not interfering with
iostreams use of the free store, but when we tried it, some compilers
complained. All compilers were happy with the <cstdio> version.
//: C02:MemCheck.cpp {O}
#include <cstdio>
#include <cstdlib>
#include <cassert>
#include <cstddef>
using namespace std;
#undef new
// Global flags set by macros in MemCheck.h
bool traceFlag = true;
bool activeFlag = false;
namespace {
// Memory map entry type
struct Info {
void* ptr;
const char* file;
long line;
};
// Memory map data
const size_t MAXPTRS = 10000u;
Info memMap[MAXPTRS];
size_t nptrs = 0;
// Searches the map for an address
int findPtr(void* p) {
for(size_t i = 0; i < nptrs; ++i)
if(memMap[i].ptr == p)
return i;
return -1;
}
void delPtr(void* p) {
int pos = findPtr(p);
assert(pos >= 0);
// Remove pointer from map
for(size_t i = pos; i < nptrs-1; ++i)
memMap[i] = memMap[i+1];
--nptrs;
}
// Dummy type for static destructor
struct Sentinel {
~Sentinel() {
if(nptrs > 0) {
printf("Leaked memory at:\n");
for(size_t i = 0; i < nptrs; ++i)
printf("\t%p (file: %s, line %ld)\n",
memMap[i].ptr, memMap[i].file,
memMap[i].line);
}
else
printf("No user memory leaks!\n");
}
};
// Static dummy object
Sentinel s;
} // End anonymous namespace
// Overload scalar new
void*
operator new(size_t siz, const char* file, long line) {
void* p = malloc(siz);
if(activeFlag) {
if(nptrs == MAXPTRS) {
printf("memory map too small (increase
MAXPTRS)\n");
exit(1);
}
memMap[nptrs].ptr = p;
memMap[nptrs].file = file;
memMap[nptrs].line = line;
++nptrs;
}
if(traceFlag) {
printf("Allocated %u bytes at address %p
", siz, p);
printf("(file: %s, line: %ld)\n", file,
line);
}
return p;
}
// Overload array new
void*
operator new[](size_t siz, const
char* file, long line) {
return operator new(siz, file, line);
}
// Override scalar delete
void operator delete(void* p) {
if(findPtr(p) >= 0) {
free(p);
assert(nptrs > 0);
delPtr(p);
if(traceFlag)
printf("Deleted memory at address
%p\n", p);
}
else if(!p && activeFlag)
printf("Attempt to delete unknown pointer:
%p\n", p);
}
// Override array delete
void operator delete[](void* p) {
operator delete(p);
} ///:~
The Boolean flags traceFlag and activeFlag are
global, so they can be modified in your code by the macros TRACE_ON( ),
TRACE_OFF( ), MEM_ON( ), and MEM_OFF( ). In
general, enclose all the code in your main( ) within a MEM_ON( )-MEM_OFF( )
pair so that memory is always tracked. Tracing, which echoes the activity of
the replacement functions for operator new( ) and operator
delete( ), is on by default, but you can turn it off with TRACE_OFF( ).
In any case, the final results are always printed (see the test runs later in this
chapter).
The MemCheck facility tracks memory by keeping all
addresses allocated by operator new( ) in an array of Info
structures, which also holds the file name and line number where the call to new
occurred. To prevent collision with any names you have placed in the global
namespace, as much information as possible is kept inside the anonymous
namespace. The Sentinel class exists solely to call a static object
destructor as the program shuts down. This destructor inspects memMap to
see if any pointers are waiting to be deleted (indicating a memory leak).
Our operator new( ) uses malloc( )
to get memory, and then adds the pointer and its associated file information to
memMap. The operator delete( ) function undoes all that work
by calling free( ) and decrementing nptrs, but first it
checks to see if the pointer in question is in the map in the first place. If
it isn t, either you re trying to delete an address that isn t on the free
store, or you re trying to delete one that s already been deleted and removed
from the map. The activeFlag variable is important here because we don t
want to process any deallocations from any system shutdown activity. By calling
MEM_OFF( ) at the end of your code, activeFlag will be set
to false, and such subsequent calls to delete will be ignored. (That s
bad in a real program, but our purpose here is to find your leaks; we re
not debugging the library.) For simplicity, we forward all work for array new
and delete to their scalar counterparts.
The following is a simple test using the MemCheck
facility:
//: C02:MemTest.cpp
//{L} MemCheck
// Test of MemCheck system.
#include <iostream>
#include <vector>
#include <cstring>
#include "MemCheck.h" // Must appear last!
using namespace std;
class Foo {
char* s;
public:
Foo(const char*s ) {
this->s = new char[strlen(s) + 1];
strcpy(this->s, s);
}
~Foo() { delete [] s; }
};
int main() {
MEM_ON();
cout << "hello" << endl;
int* p = new int;
delete p;
int* q = new int[3];
delete [] q;
int* r;
delete r;
vector<int> v;
v.push_back(1);
Foo s("goodbye");
MEM_OFF();
} ///:~
This example verifies that you can use MemCheck in
the presence of streams, standard containers, and classes that allocate memory
in constructors. The pointers p and q are allocated and
deallocated without any problem, but r is not a valid heap pointer, so
the output indicates the error as an attempt to delete an unknown pointer:
hello
Allocated 4 bytes at address 0xa010778 (file:
memtest.cpp, line: 25)
Deleted memory at address 0xa010778
Allocated 12 bytes at address 0xa010778 (file:
memtest.cpp, line: 27)
Deleted memory at address 0xa010778
Attempt to delete unknown pointer: 0x1
Allocated 8 bytes at address 0xa0108c0 (file:
memtest.cpp, line: 14)
Deleted memory at address 0xa0108c0
No user memory leaks!
Because of the call to MEM_OFF( ), no subsequent
calls to operator delete( ) by vector or ostream are
processed. You still might get some calls to delete from reallocations
performed by the containers.
If you call TRACE_OFF( ) at the beginning of the
program, the output is
hello
Attempt to delete unknown pointer: 0x1
No user memory leaks!
Thinking in C++ Vol 2 - Practical Programming |
Prev |
Home |
Next |