Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]-1

.pdf
Скачиваний:
108
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

Debugging Techniques

else if(names[i] == names[j]) // Superfluous - but it calls operator==() phrase = “ equal to “;

jName = new char[names[j].getNameLength()+1]; // Array to hold second name cout << endl << names[i].getName(iName) << “ is” << phrase

<< names[j].getName(jName);

}

}

cout << endl; return 0;

}

To reduce output further, you could switch off the trace output by commenting out the control symbols in the DebugStuff.h header:

// DebugStuff.h - Debugging control #pragma once

#ifdef _DEBUG

 

 

 

//#define CONSTRUCTOR_TRACE

// Output constructor call trace

//#define FUNCTION_TRACE

// Trace function calls

#endif

 

You can recompile the example and run it again.

How It Works

It works just as expected. You get a report that your program does indeed have memory leaks, and you get a list of the objects in the free store at the end of the program. The output generated by the free store debug facility starts with:

Detected memory leaks! Dumping objects ->

{143} normal block at 0x00355F08, 15 bytes long.

Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD {142} normal block at 0x00355EC8, 15 bytes long.

Data: <Emily Steinbeck> 45 6D 69 6C 79 20 53 74 65 69 6E 62 65 63 6B {141} normal block at 0x00355E90, 12 bytes long.

Data: <Emily Miller> 45 6D 69 6C 79 20 4D 69 6C 6C 65 72

...

and ends with:

...

{120} normal block at 0x003559D8, 8 bytes long. Data: <Dickens > 44 69 63 6B 65 6E 73 00 {119} normal block at 0x003559A0, 8 bytes long. Data: <Charles > 43 68 61 72 6C 65 73 00

{118} normal block at 0x00355968, 11 bytes long. Data: <Ivor Horton> 49 76 6F 72 20 48 6F 72 74 6F 6E {117} normal block at 0x00355930, 7 bytes long. Data: <Horton > 48 6F 72 74 6F 6E 00

{116} normal block at 0x003558F8, 5 bytes long. Data: <Ivor > 49 76 6F 72 00

Object dump complete.

599

Chapter 10

The objects reported as being left in the free store are presented with the most recently allocated first, and the earliest last. It is obvious from the output that the Name class is allocating memory for its data members, and never releasing it. The last three objects dumped correspond to the pName array allocated in main(), and the data members of the object, myName. The blocks for the complete names are allocated in main(), and they too are left laying about. The problem our class has is that we forgot the fundamental rules relating to classes that allocate memory dynamically; they should always define a destructor, a copy constructor, and the assignment operator. The class should be declared as:

class Name

 

 

{

 

 

public:

 

 

Name();

 

// Default constructor

Name(const char* pFirst, const char* pSecond);

// Constructor

Name(const Name& rName);

 

// Copy constructor

 

 

 

~Name();

 

// Destructor

char* getName(char* pName) const;

// Get the complete name

int getNameLength() const;

// Get the complete name length

// Comparison operators for names

 

 

bool operator<(const Name& name) const;

 

 

bool operator==(const Name& name) const;

 

 

bool operator>(const Name& name) const;

 

 

 

 

 

Name& operator=(const Name& rName);

 

// Assignment operator

private:

 

 

char* pFirstname;

 

 

char* pSurname;

 

 

};

 

 

You can define the copy constructor as:

Name:: Name(const Name& rName)

{

pFirstname = new char[strlen(rName.pFirstname)+1];

// Allocate space for 1st name

strcpy(pFirstname, rName.pFirstname);

// and copy it.

pSurname = new char[strlen(rName.pSurname)+1];

// Same for the surname...

strcpy(pSurname, rName.pSurname);

 

}

 

The destructor just needs to release the memory for the two data members:

Name::~Name()

{

delete[] pFirstname; delete[] pSurname;

}

600

Debugging Techniques

In the assignment operator, you must make the usual provision for the left and right sides being identical:

Name& Name::operator=(const Name& rName)

 

{

 

if(this == &rName)

// If lhs equals rhs

return *this;

// just return the object

delete[] pFirstname;

pFirstname = new char[strlen(rName.pFirstname)+1]; // Allocate space for 1st name

strcpy(pFirstname, rName.pFirstname);

// and copy it.

delete[] pSurname;

 

pSurname = new char[strlen(rName.pSurname)+1];

// Same for the surname...

strcpy(pSurname, rName.pSurname);

 

return *this;

 

}

 

You also should make the default constructor work properly. If the default constructor doesn’t allocate memory in the free store, you have the possibility that the destructor will erroneously attempt to delete memory that was not allocated in the free store. You need to modify it to:

Name::Name()

{

#ifdef CONSTRUCTOR_TRACE

// Trace constructor calls

cout << “\nDefault Name constructor called.”; #endif

// Allocate array of 1 for empty strings pFirstname = new char[1];

pSurname = new char[1];

pFirstname[0] = pSurname[0] = ‘\0’;

// Store null character

}

If you add statements to main() to delete the memory that is allocate dynamically there, the program should run without any messages relating to memory leaks. In main() you need to add the following statement to the end of the inner loop that is controlled by j:

delete[] jName;

You also need to add the following statement to the end of the outer loop that is controlled by i:

delete[] iName;

Finally, you still have to release the memory for pName after the loops in main():

delete[] pName;

601

Chapter 10

Debugging C++/CLI Programs

Life is simpler with C++/CLI programming. None of the complications of corrupted pointers or memory leaks arise in programs written for the CLR, so this reduces the debugging problem substantially compared to native C++ at a stroke. You set breakpoints and tracepoints in a CLR program exactly the same way as you do for a native C++ code. You have a specific option that applies to C++/CLI code for preventing the debugger from stepping through library code. If you select the Tools > Options menu item a dialog is displayed, and if you select the Debugging/General set of options the dialog looks as shown in Figure 10-19.

Figure 10-19

Checking the option highlighted in Figure 10-19 ensures that the debugger only steps through your source statements and executes the library code normally.

Using the Debug and Trace Classes

The Debug and Trace classes in the System::Diagnostics namespace are for tracing execution of a program for debugging purposes. The capabilities provided by the Debug and Trace classes are identical; the difference between them is that Trace functions are compiled into release builds, whereas Debug functions are not. Thus you can use Debug class functions when you are just debugging your code, and Trace class functions when you want to obtain Trace information in release versions of your code for performance monitoring or diagnostic and maintenance purposes. You also have control over whether the compile includes trace code in your program.

Because the functions and other members in the Debug and Trace classes are identical, I’ll just describe the capability in terms of the Debug class.

602

Debugging Techniques

Generating Output

You can produce output using the Debug::WriteLine() and Debug::Write() functions that write messages to an output destination; the difference between these two functions is that the WriteLine() function writes a newline character after the output whereas the Write() function does not. They both come in four overloaded versions; I’ll use the Write() function as the example but WriteLine() versions have the same parameter lists:

Function

 

Description

 

 

 

Debug::Write(String^

message)

Writes message to the output destination.

Debug::Write(String^

message,

Writes categoryname followed by message to the

String^

category)

output destination. A category name is used to

 

 

organize the output.

Debug::Write(Object^

value)

Writes the string returned by value->ToString()

 

 

to the output destination.

Debug::Write(Object^

value,

Writes categoryname followed by the string

String^

category)

returned by value->ToString() to the output

 

 

destination.

 

 

 

The WriteIf() and WriteLineIf() are conditional versions of the Write() and WriteLine() functions in the Debug class:

Function

Description

 

 

Debug::WriteIf(bool condition,

Writes message to the output destination if

String^ message)

condition is true; otherwise, no output is

 

produced.

Debug::WriteIf(bool condition,

Writes categoryname followed by message to the

String^ message,

output destination if condition is true;

String^ category)

otherwise, no output is produced.

Debug::WriteIf(bool condition,

Writes the string returned by value->ToString()

Object^ value)

to the output destination if condition is true;

 

otherwise, no output is produced.

Debug::WriteIf(bool condition,

Writes categoryname followed by the string

Object^ value,

returned by value->ToString() to the output

String^ category)

destination if condition is true; otherwise, no

 

output is produced.

 

 

As you see, the WriteIf() and WriteLineIf() functions have an extra parameter of type bool at the beginning of the parameter list for the corresponding Write() or WriteLine() function and the argument for this determines whether or not output occurs.

603

Chapter 10

You can also write output using the Debug::Print() function that comes in two overloaded versions:

Function

Description

 

 

Print(String^ message)

This writes message to the output destination fol-

 

lowed by a newline character.

Print(String^ format,

This works in the same way as formatted output

...array<Object^>^ args)

with the Console::WriteLine() function. The

 

format string determines how the arguments that

 

follow it are presented in the output.

 

 

Setting the Output Destination

By default the output messages are sent to the output window in the IDE, but you can change this by using a listener, and a listener is an object that directs debug and trace output to one or more destinations. Here’s how you can create a listener and direct debug output to the standard output stream:

TextWriterTraceListener^ listener = gcnew TextWriterTraceListener( Console::Out);

Debug::Listeners->Add(listener);

The first statement creates a TextWriterTraceListener object that directs the output to the standard output stream, which is returned by the static property, Out, in the Console class. (The In and Error properties in the Console class return the standard input stream and standard error stream respectively.) The Listeners property in the Debug class returns a collection of listeners for debug output so the statement adds the listener object to the collection. You could add other listeners that additionally directed output elsewhere (to a file perhaps).

Indenting the Output

You can control the indenting of the debug and trace messages. This is particular useful in situations where functions are called at various depths. By indenting the output at the beginning of a function and removing the indent before leaving the function, the debug or trace output is easily identified and you’ll be able to see the depth of function call from the amount of indentation of the output.

To increase the current indent level for output by one (one indent unit is four spaces by default), you call the static Indent() function in the Debug class like this:

Debug::Indent();

// Increase indent level by 1

To reduce the current indent level by one you call the static Unindent() function:

Debug::Unindent();

// Decrease indent level by 1

The current indent level is recorded in the static IndentLevel property in the Debug class so you can get or set the current indent level through this property. For example:

Debug::IndentLevel = 2*Debug::IndentLevel;

This statement doubles the current level of indentation for subsequent debug output.

604

Debugging Techniques

The number of spaces in one indent unit is recorded in the static IndentSize property in the Debug class. You can retrieve the current indent size and change it to a different value. For example:

Console::WriteLine(L”Current indent unit = {0}”, Debug::IndentSize);

Debug::IndentSize = 2;

// Set indent unit to 2 spaces

The first statement simply outputs the indent size and the second statement sets it to a new value. Subsequent calls to Indent()increases the current indentation by the new size, which is two spaces.

Controlling Output

Trace switches provide you with a way to switch any of the debug or trace output on and off. You have two kinds of trace switches you can use:

The BooleanSwitch reference class objects provide you with a way to switch segment of output on or off depending on the state of the switch.

The TraceSwitch reference class objects provide you with a more sophisticated control mechanism because each TraceSwitch object has four properties that correspond to four control levels for output statements.

You could create a BooleanSwitch object to control output as a static class member like this:

public ref class MyClass

{

private:

static BooleanSwitch^ errors =

gcnew BooleanSwitch(L”Error Switch”, L”Controls error

output”);

public:

void DoIt()

{

// Code...

if(errors->Enabled) Debug::WriteLine(L”Error in DoIt()”);

// More code...

}

// Rest of the class...

};

This shows the errors object as a static member of MyClass. The first argument to the BooleanSwitch constructor is the display name for the switch that is used to initialize the DisplayName property and the second argument sets the value of the Description property for the switch. There’s another constructor that accepts a third argument of type String^ that sets the Value property for the switch.

The Enabled property for a Boolean switch is of type bool and is false by default. To set it to true, you just set the property value accordingly:

errors->Enabled = true;

605

Chapter 10

The DoIt() function in MyClass outputs the debug error message only when the errors switch is enabled.

The TraceSwitch reference class has two constructors that have the same parameters as the BooleanSwitch class constructors. You can create a TraceSwitch object like this:

TraceSwitch^ traceCtrl =

gcnew TraceSwitch(L”Update”, L”Traces update operations”);

The first argument to the constructor sets the value of the DisplayName property, and the second argument sets the value of the Description property.

The Level property for a TraceSwitch object is an enum class type, TraceLevel, and you can set this property to control trace output to any of the following values:

Value

Description

 

 

TraceLevel::Off

No trace output.

TraceLevel::Info

Output information, warning, and error messages.

TraceLevel::Warning

Output warning and error messages.

TraceLevel::Error

Output Error messages.

TraceLevel::Verbose

Output all messages.

 

 

The value you set determines the output produced. To get all messages, you set the property as follows:

traceCtrl->Level = TraceLevel::Verbose;

You determine whether a particular message should be issued by your trace and debug code by testing the state of one of four properties of type bool for the TraceSwitch object:

Property

Description

 

 

TraceVerbose

Returns the value true when all messages are to be output.

TraceInfo

Returns the value true when information messages are to

 

be output.

TraceWarning

Returns the value true when warning messages are to be

 

output.

TraceError

Returns the value true when error messages are to be

 

output.

 

 

606

Debugging Techniques

You can see from the significance of these property values that setting the Level property is also set the states of these properties. If you set the Level property to TraceLevel::Warning for instance,

TraceWarning and TraceError is set to true and TraceVerbose and TraceInfo are set to false.

To decide whether to output a particular message, you just test the appropriate property:

if(traceCtrl->TraceWarning)

Debug::WriteLine(L”This is your last warning!”);

The message is output only if the TraceWarning property for traceCtrl is true.

Assertions

The Debug and Trace classes have static Assert() functions that provides a similar capability to the native C++ assert() function. The first argument to the Debug::Assert() function is a bool value or expression that causes the program to assert when the argument is false. The call stack is then displayed in a dialog as shown in Figure 10-20.

Figure 10-20

Figure 10-20 shows an assertion that is produced by the next example. When the program asserts the call stack presented in the dialog shows the line numbers in the code and the names of the functions that are executing at that point. Here there are four functions executing, including main().

You have three courses of action after an assertion. Clicking the Abort button ends the program immediately; clicking the Ignore button allows the program to continue; and clicking the Retry button gives you the option of executing the program in debug mode.

607

Chapter 10

You have three overloaded versions of the Assert() function available:

Function

Description

 

 

Debug::Assert(bool condition)

When condition is false, a dialog displays

 

showing the call stack at that point.

Debug::Assert(bool condition,

As above but with message displayed in the

String^ message)

dialog above the call stack information.

Debug::Assert(bool condition,

As the preceding version but with details

String^ message,

displayed additionally in the dialog.

String^ details)

 

 

 

Seeing it working is the best aid to understanding, so put together an example that demonstrates debug and trace code in action.

Try It Out

Using Debug and Trace

This example is just an exercise for some of the trace and debug functions I have described. Create a CLR console project and modify it to the following:

//Ex10_03.cpp : main project file.

//CLR trace and debug output

#include “stdafx.h”

using namespace System;

using namespace System::Diagnostics;

public ref class TraceTest

{

public:

TraceTest(int n):value(n){}

property TraceLevel Level

{

void set(TraceLevel level) {sw->Level = level; } TraceLevel get(){return sw->Level; }

}

void FunA()

{

++value;

Trace::Indent(); Trace::WriteLine(L”Starting FunA”); if(sw->TraceInfo)

Debug::WriteLine(L”FunA working...”); FunB();

Trace::WriteLine(L”Ending FunA”); Trace::Unindent();

608