Programming large software projects
There are a number of differences between the kind of programming that students do for courses such as this and kind of programming that is done in the ``real world''.
This also means that it should be possible to make a minor change in a component without such a change having wide ranging effects on many other components. The best way to do this is to have each component be relatively independent. This is the reason why you should avoid using global variables, because global variables increase the interconnectivity between components.
Good Programming Practices
It is important to develop good programming habits right from the start. Here are some guidelines that you should use whenever you write software, either for a course or for a ``real world'' application.
<cntl> c).
As you start to write larger programs, the process of debugging (finding and fixing logic errors) becomes more complex. A traditional way of debugging a program involves getting more output (via cout statements) to determine just what exactly the program is doing and to inspect the values of variables. Using cout to print out values and notify you of where the program is running is still a good idea, but it is often faster and easier to use the debugger to track down your logic errors.
A debugger is a tool which lets you stop and resume program execution and examine the values stored in memory (like your variables). While it takes a while to learn how to use it, in the long run, it will save you a good deal of time.
Programming errors (bugs) come in a variety of flavors. This worksheet will consider two important categories:
Using the MS Visual C++ debugger
To run a debugger effectively, you should learn the following:
Starting the debugger
There are two ways of starting the MS Visual C++ debugger. One way is to run a program until it crashes. When this happens, a dialog window will come up giving you the option of terminating the run or starting the debugger. If you choose to debug, note that a new instance of Visual Studio is started. The program will be stopped where the bug was detected, not necessarily where the bug occurred. Once you see where the error occurred, you should immediately exit this second running copy of Visual Studio. (The debugger does strange things if you continue debugging in this second copy.)
Another way to run the debugger is to start it yourself, rather than waiting for the program to crash. Unless you have a program that runs a long time before crashing, this is the preferred method. Choose Start Debug from the Build Menu. A pop-up menu will appear with four choices:
Go F5 Step Into F11 Run to Cursor Cntl+F10 Attach to Process ...The Go command will start executing your program, and it will run to completion, until a memory exception error occurs, or until a breakpoint is reached. (Breakpoints are explained below.) The Step Into command starts executing your program but it will stop at the first executable line in main. This will allow you to step through your program one line at a time. The Run to Cursor command is like the Go command except that it will stop at the line where your cursor is in the source file.
Exiting The Debugger Once you have started the debugger, you can exit the debugger using the short cut shift+F5, or by selecting the exit option in the pull down menu structure.
Setting Breakpoints To tell the debugger that you want it to halt your program at a particular statement, you should set a breakpoint at that statement. The debugger will always halt your program at the breakpoint (even if you choose Go). To set a breakpoint, move the cursor to the line where you want to set the breakpoint and hit the F9 key. A red dot will appear in the left margin. (You can also click on the line with the right mouse button and a popup menu will appear. One of the choices is Insert Breakpoint). Now, when you choose Go from the debug menu, execution will stop whenever the line with the breakpoint is executed. To remove a breakpoint, move your cursor to the line and hit F9.
If you select the Run to Cursor option, the debugger will (temporarily) insert a breakpoint at the line where the cursor is in the source code and will run your program until it reaches that breakpoint. Once there the program will stop and the breakpoint will be automatically removed; thus, you will not stop there again. This can be useful if you want to start a program and set breakpoints to check the program before an error occurs (e.g., errors not triggering a crash).
Running The Program in the Debugger The Go command will either run your program from the beginning if the program is not halted or resume execution from the place where the program was halted. Execution continues until a breakpoint (if you have not set any breakpoint, it will run to completion or a memory exception error). You should NOT attempt to Go after your program has a memory exception; you should fix the problem first and restart the program.
The Step commands will run your program one statement at a time (the debugger automatically inserts a breakpoint after the current statement and removes it after stepping). Usually it is very easy to know what is meant by the next statement, but if the current statement is a function call you may either want to debug the function or just ``skip over'' it. Since this is so useful the debugger supports:
Try hitting F11 a few times with a program. Note that the yellow debugger arrow points to the current line, and note that a new window will appear at the bottom with the names of all of the currently active variables and their current values. Try hitting F11 a few times and notice what is happening. Pay particular attention to the values of the variables. (When the program is at a particular statement, that statement has not yet been executed, so the values of the variables are those prior to the execution of the statement.)
Stepping through a long program is pretty tedious; you can immediately go to a particular statement in your code by moving the cursor to that line and then hitting Run to cursor (Cntl+F10).
Alert: Avoid stepping into a cin, cout or new statement or a statement that calls a library function like strcpy; the debugger will not be able to find the code and will start displaying machine code, which is probably incomprehensible to you. You can recover from this by hitting shift+F11 to step out or if you can't figure out what the debugger is doing, you can quit debugging by hitting shift+F5. You can also advance to the next step by opening the window that displays your source code (use the window menu to open the window with your source code), moving the cursor to the line after the line that caused the problem and hitting Cntl+F10 (Run to cursor).
Setting Watch on a Variable
If you overwrite array bounds, you can inadvertently change the
value of a variable, and such errors are often very difficult to find.
Consider this program.
#include <iostream.h>
int main()
{
int i, x, y[3];
x = 17;
for (i=0;i<=3;i++)
y[i] = i * 100;
cout << x << endl;
return 0;
}
Viewing the Call Stack
The call stack is a list of all of the currently active functions in your program. For example, suppose you are running a program in which the function main() calls FctnOne() and FctnOne calls FctnTwo(), and FctnTwo calls FctnThree(). Each of these functions has its own arguments and its own variables. There are times when you want to view the value of variables of functions in the call stack. To do this, bring up the Call Stack window. There are several ways to do this. One way is to choose Debug Windows from the View menu. One of the options on this menu is called Call Stack. The keyboard shortcut to invoke the call stack window is Alt 7. Once you have the call stack window, you can examine the values of any of the variables by moving the cursor to the appropriate function and clicking the right mouse button. A pop-up menu appears. Choose Go to Code (it's the top choice).
This is particularly useful when your program crashes in a library function like strcat. When the program crashes, the assembler code for the library function appears on your window. This is normally of little use to you, but you want to see where in your code the problem occurred, so you work your way down the call stack until you come to code that you wrote.
Exercise 1: Copy the following code into a project and
run it. You can find the code in /dept/cs/cs2/download/debugger.cpp
#include <iostream>
using namespace std;
int FctnOne();
void FctnTwo(char *);
int main()
{
int x;
x = FctnOne();
cout << "x is " << x << endl;
return 0;
}
int FctnOne()
{
char *a = "Hello";
FctnTwo(a);
return 0;
}
void FctnTwo(char *s)
{
char *t = " World";
strcat(s,t);
cout << s << endl;
}
Run it in the debugger until it crashes. The code for strcat() will pop up. Open the Call Stack window if it is not already open. it should look something like this.
strcat() line 169 FctnTwo(char * 0x0046c024 ??_C@_05DPEH@Hello?$AA@) line 21 + 13 bytes FctnOne() line 15 + 9 bytes main() line 8 + 5 bytes mainCRTStartup() line 206 + 25 bytes KERNEL32! 77f1ba06()If you right click on the line FctnTwo(char... and choose Go To Code, the debugger will display the code for the function FctnTwo and the current values of all of the variables will be displayed at the bottom. Note that pointers values are displayed in hexidecimal, but you can tell if a pointer is NULL because its value will be 0x00000000. Uninitialized pointers often seem to have the value 0xcccccccc. You can display the value of each variable in each function at the time of the crash. This includes s and t in FctnTwo, a in FctnOne and x in main.
Alert: The quiz for this worksheet involves your demonstrating use of the debugger without looking at the worksheet. You should be able to do all of the following quickly and easily, using the appropriate accelerator keys rather than pulling down menus. If you cannot do these, you will fail the quiz.
You must memorize all of the following accelerator keys for the quiz.
| Set a breakpoint | F9 |
| Run to a breakpoint or completion | F5 |
| Step, going into functions | F11 |
| Step, skipping over functions | F10 |
| Step out of a function | Shift-F11 |
| Stopping the debugger | Shift-F5 |
| Restarting the Program | Cntl-Shift-F5 |
| Open Call Stack Window | Alt-7 |
The string class
Many students have had difficulty with the char * implementation of character strings. Students are apt to use the assignment operator = or the equals comparison operator == when they should use the functions strcpy() and strcmp(), and they do not allocate new memory or delete unneeded memory properly. There is a class string which avoids many of these problems. For the remainder of this course, you may use this class if you wish.
To use this class, you need to include the following statement
#include <string>
You can then declare instances of the string class like this:
string s1;
or like this
string s2("This is a string");
Here are some member functions for this class.
void string::assign(char *s)
void string::append(char *s) // like strcat
void string::append(string s)// like strcat
int string::size() // like strlen
int string::length() // like strlen
char *string::data() // returns a pointer to the string value
The == operator works like strcmp() and the = operator works like strcpy()
There are numerous other member functions and operators as well.
Here is some sample code.
#include <string>
#include <iostream.h>
using namespace std;
int main()
{
string s1;
string s2("This is a string");
s1.assign("The quick brown fox");
s2.append(" with another string appended");
cout << s1.data() << endl; // prints The quick brown fox
cout << s2.data() << endl;
// prints this is a string with another string appended
s1.assign("ABCDEFG");
string s3("ABC");
s3.append("DEFG");
if (s1 == s3) cout << "TRUE"; // prints TRUE
else cout << "FALSE";
cout << endl;
int n = s1.size();
cout << "The length of s1 is " << n << endl; // prints 7
s1.append(s2);
cout << s1.data() << endl;
return 0;
}