Lab 2

Contents


Objectives

In this laboratory, you will:

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: 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

debug1 example

First, fix a compilation error: Next, let's try out some debugger commands: 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: 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.