Lab 2
Contents
Objectives
In this laboratory, you will:
-
learn more about dynamic memory allocation,
-
learn more about copy constructors,
-
learn more about operator overloading,
-
learn basic debugging techniques,
-
work with a dynamic array implementation of the IntSet class.
Prelab Overview
Introduction
In this lab, you will learn basic debugging techniques using the MSVC debugger.
You will be provided with buggy programs and you will be asked to use the
debugger and correct them. You will be shown how to use the debugger and
some of its commands. The buggy programs will be found in four files:
-
Debug1.zip -
contains a simple one file
program;
-
Debug2.zip - a simple program that
uses a class Bag, which is also included;
In the next part of the lab, you will be given a new version of
the IntSet class that uses dynamic arrays and is not limited to
100 items. Do not use the old version of the IntSet class for
this lab. The new version of IntSet will have all the functions
from the first version, but it will also have a copy constructor, assignment
operator (=), and a union operator (+). This version of the IntSet
class will again consist of three files:
You will be asked to implement the intersection operator (*) during the
lab.
Debugger
MSVC provides an integrated source level debugger TIP:
MSVC debugger documentation. A debugger is a powerful
tool you can use to track down runtime and logic errors in your program. Even
though a debugger does not seem useful at the moment, the debugger will
definitely help you locate problems when you write more complex programs.
These problems could make your program not work as expected or produce
a runtime error (meaning that everything compiles correctly, but something goes
wrong when you run it and the program stops running).
Types of errors
Errors can occur at each stage of the compile-link-run process. The errors
reported by the compiler usually fall into the category of syntax errors.
These errors are often caused by typographical errors like misspelled words
and missing punctuation. The compiler also will report errors if you forgot
to include a header file or if you use variables that you forgot to declare.
During the linking stage, the linker will alert you to missing or unimplemented
functions. Errors at this stage are usually caused by forgetting to link
an object file and are easily remedied. Errors that occur at runtime, however,
are probably the most frustrating and difficult to debug because they are
often logical errors in the implementation of your program.
The main cause of runtime errors is the incorrect use of pointers, which
results in improper memory access and stops the programming from running.
Examples of common pointer problems include attempting to access an array
out of bounds, trying to dereference a null pointer, and trying to delete
a pointer more than once.
A debugger helps you find and debug your errors by showing you which line
of code is causing the crash, showing you the functions that were called to
get to that line of code, and allowing you to view variables' values.
Dynamic memory allocation
We know that all our C++ programs have memory allocated to them as they
are running. In fact, every variable we create needs some location in memory
(called its address) to store and maintain its value. The compiler does
this allocation for us automatically whenever we declare a new variable.
The compiler is also responsible for freeing a variable's memory when the
variable is destroyed. Although this is very convenient, there are times
when we want to control the allocation and destruction of memory for our
variables explicitly. This is where the C++ built-in operators, new
and delete come in.
The C++ operator new is used to allocate dynamic memory blocks.
The C++ operator delete is used to free dynamic memory blocks.
Dynamic memory is allocated while the program runs. This means we can choose
the size of the block we want based on the current value of one or more
variables in our program. The new operator implicitly takes the
size of its operand as an argument and attempts to allocate a block of
memory of that size or bigger. The delete operator frees a block
of memory pointed to by its operand which should be a pointer to dynamic
memory allocated by new.
Copy constructors
When dealing with dynamic data, copy constructors are required because
dynamic data is accessed exclusively through pointers. By default when
the system copies a pointer, only the address pointed to gets copied and
not the data located at the address in memory. Let's say we have a class
called someClass which contains two integer fields and a pointer
to a dynamically allocated array of integers. Foo is an instance
of someClass and contains the integers 5 and 2 and a pointer to
an array that begins with 11, 20, and 15. To make a copy of Foo
in another instance of someClass called Bar, the default
copy will copy the integers 5 and 2, and the memory address pointed to
by the field in Foo into the corresponding fields in Bar,
and the result will be that Foo and Bar will point to
the same dynamic array. This is called a shallow copy because it only copies
the pointer value and not the values of the array pointed to. Notice that
the integers in the first level are copied correctly, but the array of
integers is not copied but shared. Shallow copies do not work when there
is pointer indirection.
Figure 2-1. Shallow copy of Foo into Bar
If we want to copy the whole object and make a new one that is the same
as the first, we need to define new copy operators for these objects. The
two operators we usually need to define are a copy constructor and the
assignment operator. A copy constructor will perform a deep copy which
means that the class object will be duplicated in some other place in memory.
Using the same example as before, this time a second copy of the array
is made and the two instances of the class do not share any references.
Figure 2-2. Deep copy of Foo into Bar
A copy constructor is used automatically by the system when an object is
initialized by another object of the same type. For instance, when we declare:
Point a( 3, 4 );
Point b = a;
then Point b is initialized with the components of a.
The same copy constuctor is also used when values are passed back and forth
between a function and a calling program.
Operator overloading
Operator overloading is when the programmer redefines the meaning of a
operator symbol. For example, the operator + normally means arithmetic
addition of two numbers. In the IntSet class the operator + is redefined
to mean the set union of two IntSets. Operator functions can be
member or non-member functions. When a binary operator is a member function,
the second operand is passed as an argument to the first operand's member
operator function. Non-member operators have two arguments and usually
have to be declared as friends if they take objects as arguments. Examples
of member and non-member operators are seen in the Complex class:
Complex operator+( const Complex& c ) const;
// Member operator function
// PRE: c is a valid Complex.
// POST: a new Complex is returned that equals the sum of this number and
// the number c.
Complex operator+( const double d ) const;
// Member operator function
// PRE: d is a valid double.
// POST: a new Complex is returned that equals the sum of this number and
// the number d.
friend Complex operator+( const double d, const Complex& c );
// Non-member operator function
// PRE: d is valid double and c is valid Complex
// POST: a new Complex is returned that equals the sum of d and c.
The first + operator is defined as a member and adds two Complex
objects together. The second + operator adds a double to a Complex
object. The third operator is not a member of the Complex class
and has two arguments. The first argument is the operand before the operator
and the second argument is the other operand. The third + operator is declared
a friend because in order to add the double and Complex together,
the operator function needs to access the private fields of the Complex
argument.
Assignment operator
A common operator to overload is the assignment operator (=). This operator
often needs to perform deep copies to properly replicate the object and
assign it to a variable. A common mistake is to perform only a shallow
copy, which results in two references to the same object. The IntVector
class has an example of a deep copies used in a copy constructor and assignment
operator:
IntVector::IntVector( const IntVector& someVector )
// Copy constructor that intializes the newly constructed IntVector
// with the contents of an existing IntVector 'someVector'
{
copy( someVector );
}
IntVector&
IntVector::operator=( const IntVector& someVector )
// Member operator function that overloads the = operator to assign an
// IntVector object the state of IntVector someVector
{
if ( &someVector != this )
{
delete value;
copy( someVector );
}
return *this;
}
void
IntVector::copy( const IntVector& someVector )
// Private member helper function that copies the contents of IntVector
// 'someVector' into the current IntVector
{
low = someVector.low;
high = someVector.high;
value = new int[ high - low + 1 ];
for ( int i = 0; i <= high - low; i++ )
{
value[i] = someVector.value[i];
}
}
The copy function is a private function that is used by both the
copy constructor and the assignment operator. It performs the deep copy
for the copy constructor and assignment operator. Notice that the assignment
operator has to free the dynamic memory allocated to the storage array
before calling copy, and that the operator function returns a
reference to itself using the this keyword. Also notice that the
assignment operator has to handle the special case where someVector
is the same variable being assigned because we do not want/need to free
memory or copy anything in that situation. TIP: copy
In-lab Exercises
Lab setup
- First make a directory for this lab called
lab2 in your
cics216/labs
directory.
- Copy Debug1.zip and
Debug2.zip in the lab2 directory and unzip them there. This should
create two subdirectories: Debug1,
Debug2.
- Copy IntSet2.zip in the lab2 directory
and unzip it there. This should create a subdirectory
IntSet2.
debug1 example
First, fix a compilation error:
Next, let's try out some debugger commands:
- Step Into
This command "steps into" a function or method so you can see the execution
of the code inside the function or method. Before the debugger has started,
this command is invoked with:
- [Debug -> Step Into] or
- F11
Try this command and you will start the debugger. You will see a yellow
arrow on the left margin of your code indicating where in the program
your debugger is at. Once the debugger has started, there are several
ways to invoke the "Step Into" command:
- [Debug -> Step Into]
- F11
- the "Step Into" icon in the Debug window
(the Debug window can be shown or hidden by selecting or deselecting the
"Debug" option in the pop-up menu that you get when you right click on the
toolbars at the top of MSVC).
Try this command a few more times until you get to line #49 (line numbers are shown
at the bottom right of MSVC):
cout << "myArray1 contains: {";
Execute "Step Into" again and you will end up inside the operator<< method
inside file ostream. Since you don't want to debug this code (and the code
is confusing to read), you want to "Step Out" of this function, which is the next
command to learn:
- Step Out
The "Step Out" command allows us to go right to the end of a block of code (e.g.
a function or method). This command can be invoked with:
- [Debug -> Step Out]
- Shift-F11
- the "Step Out" icon in the Debug window
Execute the "Step Out" command now to step out of the operator<< method.
You will end up back in the main function. Continuing using the "Step Into" command
to step into function printArray. When you get to the next call to
operator<<, we don't want to step into it, so we "Step Over" it:
- Step Over
The "Step Over" command goes to the next line of code and does NOT step into functions.
This command can be invoked with:
- [Debug -> Step Over]
- F10
- the "Step Over" icon in the Debug window
Use the "Step Over" command to avoid stepping into functions that you don't want to
step into (such as operator<< methods).
- Breakpoints
The above debugger commands allows a user to navigate through code, but does so slowly.
You can run the program until a certain line with breakpoints. A breakpoint on a line
of code will cause the debugger to stop the program from running at that line, which
gives the debugger control so you can continue stepping from that line on or view
variables' values, etc.
You can set a breakpoint by right-clicking on the line of code that you want to put a
breakpoint on and then selecting the "Insert/Remove Breakpoint" option in the pop-up
menu to set the breakpoint. After setting a breakpoint, you will see a tiny red
octagon (symbolizing a stop sign) to the left of the line of code that you set a
breakpoint for. Next, try inserting a breakpoint in the program and then running the
program until the breakpoint is reached by using the "Start" command,
which can be invoked with:
Finally, run the program using the "Start" command again. The program will crash on line 33.
You can see how you got to that line of code by viewing the call stack:
- Call Stack
The "Call Stack" shows you what functions were called to get to the current line of
code that the program is on. You can show and hide the call stack window by selecting
and deselecting the "Call Stack" option in the pop-up menu that you get by right-clicking
on the toolbars at the top of MSVC. Any line in the call stack can be double-clicked on
to show the code for that frame of the call stack.
- Viewing variables' values
The "Variables" window shows all local variables' values. By default, the "Variables" window
should be in the bottom left of MSVC. You can show and hide the "Variables" window by
selecting and deselecting the "Variables" option in the pop-up menu that you get by right-clicking
on the toolbars at the top of MSVC.
You can also view variable values by moving your mouse pointer over a variable in your code
and leaving the mouse pointer there until a pop-up with the variable's value is displayed.
Now you should practise these debugger commands and use them to debug the problem in debug1. Fix
the problem in debug1, rebuild it and run it successfully.
debug2 example
Create a project for debug2 and add the Debug2.cpp file into it. When you try to build
it, you will get a bunch of unresolved external
symbol errors since you have not added the Bag files into the project. Add Bag.cpp by right-clicking on the "Source Files" folder
in the SolutionExplorer and choosing the "Add -> Add Existing Item..." option.
Next, build and run the Debug2 program. The program does not cause a fatal error, but it doesn't
produce correct results. Use the debugging techniques that you learned above to fix this logic error.
Keep in mind that real world bugs will be much harder to solve than this;
look at this as practice for future debugging
IntSet class v2.0
The IntSet class header provided to you has the intersection (*)
operator function commented out in the class declaration because it is
not yet implemented. Your tasks are to do the following:
-
Add the implementation of the intersection (*) operator function to the
IntSet class
-
Uncomment the references to the intersection operator function in the test
driver
-
Build the IntSet project.
-
Test the IntSet class with the test driver to test your new function
Your modified IntSet class should not change any of the existing
member functions or internal data representation.
Helpful Tips
MSVC debugger documentation - See the MSDN Library under [Visual Studio.NET Documentation ->
Visual C++ Documentation -> Using Visual C++ -> Visual C++ User's Guide -> Debugger].
(see Lab #1's Helpful Tips to find out how to use MSDN Library)
copy - since copy constructors and assignment operators often
do similar work, creating a private copy function is a common
approach to simplifying the implementation.