Courses/CS 2124/Lab Manual/Inheritance

From A-State Computer Science Wiki
Jump to: navigation, search

Introduction

This laboratory uses inheritance to eliminate redundancies in program code and provide pointer compatibility between different (but related) object types. To illustrate these concepts, a list of geometric shapes will be constructed. The shapes will have some features in common (e.g., a reference point) while others features are disparate (e.g., circle radius versus square side length).

Createproject.pngCreate a project oop09 with the empty C++ header and source files listed below; upon completion of the in-lab portion of this assignment, submit all of these files in zip file oop09iL.zip.
File or Files Description
main.cpp
Shape.h & .cpp base class definition
Circle.h & .cpp descendant class definition
Square.h & .cpp descendant class definition
ShapeList.h & .cpp base class list class definition
oop09in.txt sample data

Inheritance and Elimination of Redundancies

Consider (but do NOT enter) the following class definitions for Circle and Square objects.

class Circle {
public:
    Circle( double x, double y, double r );

private:
    double ref_x;
    double ref_y;
    double radius;
};

class Square {
public:
    Square( double x, double y, double s );

private:
    double ref_x;
    double ref_y;
    double side;
};

Note that while each Circle object will have a radius, each Square object will have a side measure. On the other hand, both Circles and Squares have a reference location for unique identification of an object in the Cartesian coordinate plane. This duplication of data specification (and subsequent methods for manipulation) can be eliminated with inheritance. The common data and methods are removed from the sibling types Circle and Square to be placed in a new ancestral type Shape. The two previous class definitions are replaced with the following three.

Place each of the following definitions in the appropriate header file (“Shape.h”, “Circle.h”, “Square.h”).

// Class Shape should be placed in Shape.h
class Shape {
public:
    Shape( double x, double y );

private:
    double ref_x;
    double ref_y;
};

// Class Circle should be placed in Circle.h
class Circle : public Shape {
public:
    Circle( double x, double y, double r );

private:
    double radius;
};

// Class Square should be placed in Square.h
class Square : public Shape {
public:
    Square( double x, double y, double s );

private:
    double side;
};

Note the use of ’‘:public Shape’‘to indicate that both object types Circle and Square are descendants of type Shape, which contains the data and methods common to each. Conversely, the class Shape is said to be the ancestor of classes Circle and Square. The public qualifier indicates that while the descendants’‘contain’’ the private data of the ancestor, they can only access it with public methods.

In Object-Oriented terminology, we say that Shape is the base class; Circle and Square are derived classes.

Base and Derived Class Constructor Calls

When an object of type Circle is created, a constructor for Circle will be called as expected; however, a constructor for Shape will also be called, as each Circle object is built around a Shape object. The Shape constructor is called first; the Shape type does not “know about” the Circle type but Circle does “know about” Shape and could therefore utilize its data. A default constructor for the base class can be called implicitly. The Circle constructor can make an explicit call to the Shape constructor as follows. Add the Circle constructor to the project in an implementation file “Circle.cpp”.

Circle::Circle( double x, double y, double r ) : Shape{x, y} {
    cout << "DEBUG: Executing Circle constructor...\n";
    radius = r;
}

Of course, you could also use the constructor initialization list to sink the radius value into the attribute. This implementation would look like the following (choose the implementation style you prefer).

Circle::Circle( double x, double y, double r ) : Shape{x, y}, radius{r} {
    cout << "DEBUG: Executing Circle constructor...\n";
}

In this version, it is important that the base class is constructed before the attribute radius is constructed: All base classes constructor calls must be placed before constructions of attributes of the current object, which must be constructed in the order they are listed.

The constructor for the Shape object type is responsible for the private data of that object; add it to the project in the appropriate implementation file.

Shape::Shape( double x, double y ) {
    cout << "DEBUG: Executing Shape constructor...\n";
    ref_x = x;
    ref_y = y;
}

If you prefer using constructor initialization lists, the implementation would look like the following. Choose which version you prefer. From this point on, the more verbose version will be shown in this material, but you are welcome to use whichever style you prefer. Please be consistent with your choice.

Shape::Shape( double x, double y ) : ref_x{x}, ref_y{y} {
    cout << "DEBUG: Executing Shape constructor...\n";
}

When the program is executed, the cout statements will illustrate how the ancestor’s constructor is called before that of the descendant. Perform the following steps.

  • Write the constructor for class Square.
  • Write a Shape class method write() to display the reference point of the Shape object.
  • Construct a Circle class method write() to display the radius and the reference point of the Circle object.
  • Construct a Square class method write() to display the side and the reference point of the Square object.
  • Overload operator<< for Circle objects to invoke the Circle class method write().
  • Overload operator<< for class Shape to invoke the Shape class method write().
  • Do NOT overload operator<< for class Square.
  • Add the following lines to function main().

    Circle c1{0, 0, 1};
    Square s1{1, 0.5, 2.3};
    cout << "c1: " << c1 << endl;
    cout << "s1: " << s1 << endl;
    

Execute the program and verify the execution order of the constructors; observe that operator<< for class Shape is invoked in lieu of an operator<< for Square objects.


Labcheckpoint.png FOR IN-LAB CREDIT: Demonstrate the program for the lab instructor and explain precisely what operator<< does with the Square object s1.


Pointer Compatibility

Now consider how a list of such objects can be manipulated using the following list class definition.

// [...] other necessary headers not shown for brevity
#include <list>

class ShapeList : protected std::list<Shape*> {
public:
    ShapeList( ) = default;
    void add( Shape* newShape );
    void write( ostream& strm_out ) const;

    friend ostream& operator<<( ostream& strm_out, const ShapeList& shapes );
};

The ShapeList is inheriting from std::list with protected inheritance; as a result, it is necessary to friend the stream insertion operator so that it may have access to all of the std::list’s attributes and methods (you will need this access to iterate through the list in the function implementation).

As the Circle and Square classes are descendants of the Shape class, the pointers of the former are compatible with those of the latter and so can be passed to the ShapeList class method add() above. Method add() should in turn call the std::list::push_back() method to add it to the list. Method write() and overloaded operator<< for class ShapeList will not be aware of the descendant type of an object once it is stored in the list, however; all operator<< can do for a ShapeList object is invoke operator<< for the Shape component of each object. Likewise, the write() method will hand off to the write() method for each object in the ShapeList. Write the add() method now, and continue reading for information about write() and operator<<.

Since ShapeList is inheriting from std::list, we can use some of the features from std::list to easily print out the list of Shapes. The range-based for loop is able to allow direct access to values inside any standard container. And since ShapeList is a std::list, it will work just fine with the range-based for. One detail worth mentioning is that the item being accessed in the loop may be accessed either by value, by reference, or by constant reference. We will use a constant reference to avoid any copies and still fulfill the write() method’s const-ness guarantee.

Use the following code as the body of your implementation for ShapeList::write(). (This implementation should be placed in “ShapeList.cpp”.)

for ( const auto& item : *this ) {
    item->write( strm_out );
    strm_out << '\n';
}

Notice that the item is a pointer (so the arrow must be used) because the list values are of type Shape*.

Implement operator<< now, by handing off to the ShapeList's write() method you just implemented.

Add the following code to function main() and execute it to make sure that all of the methods and functions are working properly.

cout << "Press <enter> to continue...";
cin.get( );  // waits on another <enter> key
ShapeList shapes;
shapes.add( new Circle{1, 2.3, 3} );
shapes.add( new Square{5, 5, 10.2} );
cout << "List via stream insertion operator:\n";
cout << shapes << endl;
cout << "\nList via write method:\n";
shapes.write( cout );
cout << endl;

Overloaded Constructors

Entry of data for objects will now be automated.

Define additional constructors for the Circle, Square, and Shape classes to accept an istream& parameter and read the same information as before, but from the specified file.

Create a data file oop09in.txt with the following contents.

Square 3 4 1
Circle 3 5 11
Circle 3 6 12
Square 3 7 2
Circle 3 8 13

Add the following code to function main() and execute it after replacing the TODO comments with the code necessary to do what they describe.

cout << "Press <enter> to continue...";
cin.get( );  // waits on another <enter> key

ifstream fin( "oop09in.txt" );
if ( !fin ) {
    cout << "Input file could not be opened!  Exiting!\n";
    exit( 1 );
}

ShapeList shapes2;
while ( fin.good( ) ) {
    string object_type;
    fin >> object_type;
    if ( object_type == "Square" ) {
        // TODO: add a new Square object (constructed from the stream)
        //       to the `shapes2` list.
    } else if ( object_type == "Circle" ) {
        // TODO: add a new Circle object (constructed from the stream)
        //       to the `shapes2` list.
    } else {
        cout << "Unexpected object type: " << object_type << endl;
        exit( 2 );
    }
}

cout << "Object locations:\n" << shapes2 << endl;

Execute the program with various combinations of data in the input file to be sure that it is working correctly.


Labcheckpoint.png FOR IN-LAB CREDIT: Add an unexpected type such as Triangle to the input data file, along with appropriate data (e.g., point, height, and base) and demonstrate the program for the lab instructor.


Introduction to Destructors and Inheritance

Information Icon.png

The documentation for std::list will be useful for this section.

Define an erase() method for ShapeList. It must de-allocate the memory owned by each pointer in the list before removing it. Define a destructor for ShapeList that will utilize erase() to free all of the memory associated with the shapes in the list.

Add destructors to the Shape, Circle and Square classes; each should contain a cout statement indicating the name of the class for which the destructor was called, e.g. “executing Shape destructor”. These destructors are not required, but are useful for visualizing the program’s control flow.

Invoke method erase() for shapes2 at the end of function main() and execute the program. Observe that the only object destructors called are those for the Shape class. Had the Circle or Square constructors allocated dynamic memory, it would have been lost at this point!

NOTE: It is possible that your program will crash now when the erase() method is called. We will learn to fix this issue, which is related to the possible memory leak described above, in a future exercise.


Labcheckpoint.png FOR IN-LAB CREDIT: Explain to the lab instructor how, and in what situations, a memory leak could occur here.


This problem will be considered again in the next lab.


Labsubmitsinglefile.png FOR IN-LAB CREDIT: Zip up these files: All files necessary to build and run your project.
Name the file oop09iL.zip and upload to CSCADE.