Community Pick: Many members of our community have endorsed this article.
Editor's Choice: This article has been selected by our editors as an exceptional contribution.

Watch, Memory, Stack Tips: C/C++ Visual Studio Debugger - Part 3

phoffricSoftware Engineering and Matlab Analyst
Software Engineering and Matlab Analyst


This article is a continuation of the C/C++ Visual Studio Express debugger series. Part 1 provided a quick start guide in using the debugger. Part 2 focused on additional topics in breakpoints. As your assignments become a little more complex, you may need to use additional techniques. This article focuses on the Watch Pane, examining blocks of memory, and viewing memory at different levels of the Stack Frame.

1.  Watch Pane
1.1.  Adding entries
1.2.  Viewing Pointers as Arrays
1.3.  Watching Expressions
1.4.  Watching Error Status
2.  Viewing Memory Regions
2.1 Opening a Memory Pane
2.2 Debugging with Integer Array Representation
3.  Viewing Memory in the Stack Frame

1. Watch Pane

A Watch Pane, like the Autos and Locals Panes, also shows variable names, values, and types.  It is especially useful for watching global objects which never show up in a Locals Pane. Unlike the Autos and Locals Panes, the Watch Pane is not filled in automatically; you need to insert the names yourself. In addition to variable (or object) names, you can also enter expressions to evaluate.

As you step through your program, if the item you are watching changes its value, then the value will appear in red.

1.1.  Adding entries

There are several ways to enter names or expressions into the Name list.
Figure 1 shows how to use the Add Watch option by right-clicking on the highlighted variable, cstr, to insert into the Watch Pane
You can also select a free entry line, and just type in the variable or expression to be evaluated
You can even highlight a variable or expression in the source code editor and drag it into the Watch Pane
Add Watch Setup
Figure 1  Add Watch Setup

1.2.  Viewing Pointers as Arrays

In Figure 1, we added cstr (a char *) to the Watch Pane.  In the first entry in Figure 2, the value of cstr is the address of the first array element being pointed to. After expanding the +sign, you will see the single value of the char. Bear in mind that the only reason the array of chars was printed out in the first line is that the debugger assumed it was a string. You would not see the array if, instead, cstr were a pointer to a long.

If you know that the pointer represents an array, then you can specify the number of elements to show. By default, the display only shows the first element.  The next entry in Figure 2, cstr,4 indicates that cstr should be treated as an array of 4 elements (in this case, of char type); and now the expansion shows the four array elements.   The next entry is the expression cstr+3, which is just a pointer to the 4th element in the array. And not surprisingly, the expansion shows a 'c'. The last entry is cstr+3,3, which says to treat the pointer to the 4th element in the array as an array of 3 chars; and so we see in the expansion 'c', 'e', and 'n'. Watch Pointers Treated as Arrays
Figure 2 Watch Pointers Treated as Arrays

Note that if you are watching a local variable, then its value has no meaning when you leave the function in which the local variable is defined. But if you return to that function, then the local variable will once again show it meaningful value.

1.3.   Watching Expressions

You can highlight expressions in your source code editor and drag them into the Watch Pane for monitoring. In fact, you can even type in expressions that may not even exist in your program. For example, you could enter   *(cstr+2)+3;  in which case, you are dereferencing the pointer, cstr+2, which yields a number (that is equivalent to the character), and then 3 is added to that number.

1.4.   Watching Error Status

If you enter $err,hr in the Watch Pane, then if a system error is detected, then the value of the error as well as a string interpretation of the error is available. For example, if you dynamically allocate memory that fails because there is not enough heap available to your application, then you should find the value in Figure 3 indicating that there was not enough storage available.
Watching Error Status
Figure 3 Watch Error Status

2.  Viewing Memory Regions

2.1.  Opening a Memory Pane

You can view up to four separate Memory Panes. To bring up these panes:

Enter <Ctrl+Alt+M, 1> or   <Ctrl+Alt+M, 2> or   <Ctrl+Alt+M, 3> or   <Ctrl+Alt+M, 4>. (Note that the <Ctrl+Alt+M> is considered as one keystroke; after typing it, the status line in the bottom left hand side of the screen says " (Ctrl+Alt+M) was pressed. Waiting for second key of chord...". Then enter a number 1..4.)
Alternatively, Figure 4 shows how to bring up Memory Panes from the menu.
Opening a Memory Pane
Figure 4  Opening a Memory Pane

In the address bar in a memory pane, you could put an array name since that name represents the address of the start of the array. If you have a user defined class object, and wish to monitor the changes within that object, then precede the object name with an & to get the address of the object. Figure 4 shows an example of looking at the cstr char pointer. Notice in this figure that the number of columns per row is set to 8 using the Columns pull-down menu. Memory Region
Figure 4 Memory Region

Line 47 in Figure 1 implies that the words variable is a string. If you entered words instead of cstr, then you will get this message:
"Unable to evaluate the expression"
Since words is a string (and not a pointer), to view words, you enter &words in the address bar.

2.2.  Debugging with Integer Array Representation

Suppose you define a one dimensional unsigned long array, Qmat, to realize a 4x18 matrix.  Then, you can define a macro function, QMATRIX, that takes two arguments (row, column) and translates it into the one dimensional array, Qmat.

Let’s try to fill up the 2d matrix so that a cell has a value equal to the row number plus the column number.

(Note: Following code has bugs, so review it just to get the gist of what is intended. It would benefit you to jot down what the 2d matrix values should be before looking at the code in detail.)
N=4; P=18;
                      unsigned long *Qmat = (long *)malloc( N * P * sizeof(long) );
                      #define QMATRIX(a,b) Qmat[a*N+b]
                         for(int r=0; r<P; r++)
                            for( int c=0; c<N; c++ )
                               QMATRIX(r,c) = r+c;

Open in new window

Let’s look at the results in a Memory Pane. You can adjust the width to show the 18 columns per row (if you have a really wide screen), or you can just show 6 columns and then every 3 lines in the pane represents one row of the matrix. (You can set the Columns option to Auto in order to drag the pane edge wide enough to show exactly six array elements.)

After running through the initialization of Qmat, Figure 5 is the memory snapshot, which I have annotated with horizontal lines to emphasize the 4 rows by 18 columns. Also, notice in this figure that by right-clicking on the pane, you get the popup menu where you can select 4-byte Integer represented as an Unsigned Display. Showing a 4 x 18 unsigned long matrix
Figure 5  Showing a 4 x 18 unsigned long matrix
The results in Figure 5 do not look good – you expected that the first row would increment from 0...17. Looking at the code, you see that you mistakenly have the for loop tests incorrect. You make these changes:
for(int r=0; r<N; r++)
                            for( int c=0; c<P; c++ )

Open in new window

But now you get a snapshot that does not even cover the entire array. So, you set a breakpoint, and repetively hit F5 to observe the changes. It becomes apparent when you get the results shown in Figure 6 that the definition of the QMATRIX macro is wrong. 2nd attempt 4 x 18 unsigned long matrix
Figure 6   The 4 x 18 unsigned long matrix still wrong

After fixing the macro as follows:
#define QMATRIX(a,b) Qmat[a*P+b]

Open in new window

You get the correct matrix shown in Figure 7. 4 x 18 maxtrix with correct values
Figure 7  The 4 x 18 maxtrix with correct values

3.  Viewing Memory in the Stack Frame

Suppose you are debugging a function that is many function levels deep from the main function. And perhaps the function you are in can be reached through multiple paths (not uncommon). In the throes of debugging, you may have forgotten for the moment which function path you took to get to your current location (it happens when you are debugging); or, perhaps you forgot what the values were in condition tests that got you to this point. What you need for both these issues, the path, and the values, is a way to back trace the path you took, and be able to check the values on the earlier stack.

There is a Call Stack tab in the bottom right-half of your debugger window (it should be near the Watch 1 tab). See Figure 8 for a Call Stack example. I switched projects to make the stack a little more interesting. This program converts a Binary Tree Array representation into a linked Binary Tree and vice-verse. The program uses a recursive approach, so as seen in the Figure 4, the createArray function appears multiple times. Call Stack Showing Recursion
Figure 8  Call Stack Showing Recursion
The top-most entry is the function currently being executed. Notice that each entry provides the input parameter values and the line number. From one recursive call to the next, notice that the value of levelNum changes. In the code pane the yellow arrow points to line 127 (consistent!). The value of *pRoot shows the left and right children as NULL and the data is 'H'. So, you know this node is a leaf in the Binary Tree.

Now if you double-click the next entry, as done in Figure 9, a green arrow appears on that entry as a reminder that this is just the stack frame that you are currently viewing in the code listing. Notice also the green arrow at line 125 in the code. Notice that *pRoot has a value of 'D'; and that it has two children with values, 'H' and 'I'. (BTW - in case you are concerned, this is not a Sorted Binary Tree.) The 'H' value for the left node should not surprise you since we just moved up one level in the stack frame; and you see in the code that the recursion always travels to the left child first. Call Stack - Up One Level
Figure 9  Call Stack - Up One Level

Closing Remarks

I hope these series of articles on the debugger make your life even easier when trying to find that nasty bug. Being able to isolate where a program begins to break down is a good part of the battle. Using the debugging techniques afforded by the very powerful Microsoft Visual Studio 2008 Express Debugger should remove the need to add debug statements. Perhaps, in the past, when you added debug statements, you had to spend additional time debugging them. No longer will that be the case!
phoffricSoftware Engineering and Matlab Analyst
Software Engineering and Matlab Analyst

Comments (0)

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.