Intro stuff
Sometimes it’s very helpful to
print the code of each statement before it is executed, either to cout or
to a trace file. Here’s a preprocessor macro to accomplish
this:
#define TRACE(ARG) cout << #ARG << endl; ARG
Now you can go through and surround the
statements you trace with this macro. Of course, it can introduce problems. For
example, if you take the statement:
for(int i = 0; i < 100; i++) cout << i << endl;
And put both lines inside
TRACE( ) macros, you get this:
TRACE(for(int i = 0; i < 100; i++)) TRACE( cout << i << endl;)
Which expands to this:
cout << "for(int i = 0; i < 100; i++)" << endl; for(int i = 0; i < 100; i++) cout << "cout << i << endl;" << endl; cout << i << endl;
Which isn’t what you want. Thus,
this technique must be used carefully.
A variation on the TRACE( )
macro is this:
#define D(a) cout << #a "=[" << a << "]" << nl;
If there’s an expression you want
to display, you simply put it inside a call to D( ) and the
expression will be printed, followed by its value (assuming there’s an
overloaded operator << for the result type). For example, you can
say D(a + b). Thus you can use it anytime you want to test an
intermediate value to make sure things are OK.
Of course, the above two macros are
actually just the two most fundamental things you do with a debugger: trace
through the code execution and print values. A good debugger is an excellent
productivity tool, but sometimes debuggers are not available, or it’s not
convenient to use them. The above techniques always work, regardless of the
situation.
This code allows you to easily create a
trace file and send all the output that would normally go to cout into
the file. All you have to do is #define TRACEON and include the header
file (of course, it’s fairly easy just to write the two key lines right
into your file):
//: C03:Trace.h // Creating a trace file #ifndef TRACE_H #define TRACE_H #include <fstream> #ifdef TRACEON ofstream TRACEFILE__("TRACE.OUT"); #define cout TRACEFILE__ #endif #endif // TRACE_H ///:~
Here’s a simple test of the above
file:
//: C03:Tracetst.cpp // Test of trace.h #include "../require.h" #include <iostream> #include <fstream> using namespace std; #define TRACEON #include "Trace.h" int main() { ifstream f("Tracetst.cpp"); assure(f, "Tracetst.cpp"); cout << f.rdbuf(); } ///:~
In the Smalltalk tradition, you can
create your own object-based hierarchy, and install pure virtual functions to
perform debugging. Then everyone on the team must inherit from this class and
redefine the debugging functions. All objects in the system will then have
debugging functions available.
Common problems with memory allocation
include calling delete for things you have malloced, calling
free for things you allocated with new, forgetting to release
objects from the free store, and releasing them more than once. This section
provides a system to help you track these kinds of problems
down.
To use the memory checking system, you
simply link the obj file in and all the calls to malloc( ),
realloc( ), calloc( ), free( ), new
and delete are intercepted. However, if you also include the following
file (which is optional), all the calls to new will store information
about the file and line where they were called. This is accomplished with a use
of the placement syntax for operator new (this trick was suggested
by Reg Charney of the C++ Standards Committee). The placement syntax is intended
for situations where you need to place objects at a specific point in memory.
However, it allows you to create an operator new with any number of
arguments. This is used to advantage here to store the results of the
__FILE__ and __LINE__ macros whenever new is
called:
//: C03:MemCheck.h // Memory testing system // This file is only included if you want to // use the special placement syntax to find // out the line number where "new" was called. #ifndef MEMCHECK_H #define MEMCHECK_H #include <cstdlib> // size_t // Use placement syntax to pass extra arguments. // From an idea by Reg Charney: void* operator new( std::size_t sz, char* file, int line); #define new new(__FILE__, __LINE__) #endif // MEMCHECK_H ///:~
In the following file containing the
function definitions, you will note that everything is done with standard IO
rather than iostreams. This is because, for example, the cout constructor
allocates memory. Standard IO ensures against cyclical conditions that can lock
up the system.
//: C03:MemCheck.cpp {O} // Memory allocation tester //{-msc} #include <cstdlib> #include <cstring> #include <cstdio> using namespace std; // MemCheck.h must not be included here // Output file object using cstdio // (cout constructor calls malloc()) class OFile { FILE* f; public: OFile(char* name) : f(fopen(name, "w")) {} ~OFile() { fclose(f); } operator FILE*() { return f; } }; extern OFile memtrace; // Comment out the following to send all the // information to the trace file: #define memtrace stdout const unsigned long _pool_sz = 50000L; static unsigned char _memory_pool[_pool_sz]; static unsigned char* _pool_ptr = _memory_pool; void* getmem(size_t sz) { if(_memory_pool + _pool_sz - _pool_ptr < sz) { fprintf(stderr, "Out of memory. Use bigger model\n"); exit(1); } void* p = _pool_ptr; _pool_ptr += sz; return p; } // Holds information about allocated pointers: class MemBag { public: enum type { Malloc, New }; private: char* typestr(type t) { switch(t) { case Malloc: return "malloc"; case New: return "new"; default: return "?unknown?"; } } struct M { void* mp; // Memory pointer type t; // Allocation type char* file; // File name where allocated int line; // Line number where allocated M(void* v, type tt, char* f, int l) : mp(v), t(tt), file(f), line(l) {} }* v; int sz, next; enum { increment = 50 }; public: MemBag() : v(0), sz(0), next(0) {} void* add(void* p, type tt = Malloc, char* s = "library", int l = 0) { if(next >= sz) { sz += increment; // This memory is never freed, so it // doesn't "get involved" in the test: const int memsize = sz * sizeof(M); // Equivalent of realloc, no registration: void* p = getmem(memsize); if(v) memmove(p, v, memsize); v = (M*)p; memset(&v[next], 0, increment * sizeof(M)); } v[next++] = M(p, tt, s, l); return p; } // Print information about allocation: void allocation(int i) { fprintf(memtrace, "pointer %p" " allocated with %s", v[i].mp, typestr(v[i].t)); if(v[i].t == New) fprintf(memtrace, " at %s: %d", v[i].file, v[i].line); fprintf(memtrace, "\n"); } void validate(void* p, type T = Malloc) { for(int i = 0; i < next; i++) if(v[i].mp == p) { if(v[i].t != T) { allocation(i); fprintf(memtrace, "\t was released as if it were " "allocated with %s \n", typestr(T)); } v[i].mp = 0; // Erase it return; } fprintf(memtrace, "pointer not in memory list: %p\n", p); } ~MemBag() { for(int i = 0; i < next; i++) if(v[i].mp != 0) { fprintf(memtrace, "pointer not released: "); allocation(i); } } }; extern MemBag MEMBAG_; void* malloc(size_t sz) { void* p = getmem(sz); return MEMBAG_.add(p, MemBag::Malloc); } void* calloc(size_t num_elems, size_t elem_sz) { void* p = getmem(num_elems * elem_sz); memset(p, 0, num_elems * elem_sz); return MEMBAG_.add(p, MemBag::Malloc); } void* realloc(void* block, size_t sz) { void* p = getmem(sz); if(block) memmove(p, block, sz); return MEMBAG_.add(p, MemBag::Malloc); } void free(void* v) { MEMBAG_.validate(v, MemBag::Malloc); } void* operator new(size_t sz) { void* p = getmem(sz); return MEMBAG_.add(p, MemBag::New); } void* operator new(size_t sz, char* file, int line) { void* p = getmem(sz); return MEMBAG_.add(p, MemBag::New, file, line); } void operator delete(void* v) { MEMBAG_.validate(v, MemBag::New); } MemBag MEMBAG_; // Placed here so the constructor is called // AFTER that of MEMBAG_ : #ifdef memtrace #undef memtrace #endif OFile memtrace("memtrace.out"); // Causes 1 "pointer not in memory list" message ///:~
OFile is a simple wrapper around a
FILE*; the constructor opens the file and the destructor closes it. The
operator FILE*( ) allows you to simply use the OFile object
anyplace you would ordinarily use a FILE* (in the fprintf( )
statements in this example). The #define that follows simply sends
everything to standard output, but if you need to put it in a trace file you
simply comment out that line.
Memory is allocated from an array called
_memory_pool. The _pool_ptr is moved forward every time storage is
allocated. For simplicity, the storage is never reclaimed, and
realloc( ) doesn’t try to resize the storage in the same
place.
All the storage allocation functions call
getmem( ) which ensures there is enough space left and moves the
_pool_ptr to allocate your storage. Then they store the pointer in a
special container of class MemBag called MEMBAG_, along with
pertinent information (notice the two versions of operator new; one which
just stores the pointer and the other which stores the file and line number).
The MemBag class is the heart of the system.
You will see many similarities to
xbag in MemBag. A distinct difference is realloc( ) is
replaced by a call to getmem( ) and memmove( ), so that
storage allocated for the MemBag is not registered. In addition, the
type enum allows you to store the way the memory was allocated;
the typestr( ) function takes a type and produces a string for use
with printing.
The nested struct M holds the
pointer, the type, a pointer to the file name (which is assumed to be statically
allocated) and the line where the allocation occurred. v is a pointer to
an array of M objects – this is the array which is dynamically
sized.
The allocation( ) function
prints out a different message depending on whether the storage was allocated
with new (where it has line and file information) or
malloc( ) (where it doesn’t). This function is used inside
validate( ), which is called by free( ) and
delete( ) to ensure everything is OK, and in the destructor, to
ensure the pointer was cleaned up (note that in validate( ) the
pointer value v[i].mp is set to zero, to indicate it has been cleaned
up).
The following is a simple test using the
memcheck facility. The MemCheck.obj file must be linked in for it to
work:
//: C03:MemTest.cpp //{L} MemCheck //{-msc} // Test of MemCheck system #include "MemCheck.h" int main() { void* v = std::malloc(100); delete v; int* x = new int; std::free(x); new double; } ///:~
The trace file created in
MemCheck.cpp causes the generation of one "pointer not in memory list"
message, apparently from the creation of the file pointer on the heap. [[ This
may not still be true – test it
]]
Including a set of methods (such as
trace/print/dump) in your library’s base class to enable easy
debugging.