| 1.124 Lecture 6 | 9/26/2000 |
Inheritance allows us to specialize the behavior of a class. For example, we might write a Shape class, which provides basic functionality for managing 2D shapes. Our Shape class has member variables for the centroid and area. We may then specialize the Shape class to provide functionality for a particular shape, such as a circle. To do this, we write a class called Circle, which inherits the properties and methods of Shape.
class Circle : public Shape {
...
};
The Circle class adds a new member variable for the radius. Now, when we create a Circle object, it will have centroid and area variables (the Shape part) in addition to the radius variable (the Circle part). The Circle object can also call methods associated with class Shape, such as get_Centroid(). We refer to the Shape class as the base class and we refer to the Circle class as the derived class.
Members of class Shape that are private (e.g. mCentroid) cannot be directly accessed from within the Circle class definition. However, they can be accessed indirectly through the Shape class's public interface (e.g. get_Centroid()). If we wish to allow class Circle to directly access members of class Shape, those members should be made protected (e.g. mfArea). To the outside world, i.e. in main(), protected members behave in exactly the same way as private members.
It is possible to use a base class pointer to address a derived class object, e.g.
Circle *pc
Shape *ps;
pc = new Circle();
ps = pc;
This feature is known as polymorphism. We can use the Shape pointer to access those methods of the Circle object that are inherited from Shape. e.g.
ps->get_Centroid()
The Circle class can also override functions that it inherits from the Shape class, as in the case of print(). To make this work, we must declare print() as a virtual function in class Shape. Then, when we use the Shape pointer to access the print() function, as in
ps->print();
we will invoke the print() function in the underlying Circle object. In the example below, we have used an array of Shape pointers, sa, to store a heterogeneous collection of Circle and Rectangle objects. In the code fragment
for (i = 0; i < num_shapes; i++) {
sa[i]->print();
// This will call either Circle::print() or Rectangle::print(), as appropriate.
}
the decision to call the print() function in class Circle or the one in class Rectangle must be made at run-time. The mechanism by which virtual function calls are resolved is known as dynamic binding.
The implementation of the print() function in class Shape serves as a default implementation, which will be used if the derived class chooses not to provide an overiding implementation. It is possible, however, for the Shape class to require all derived classes to provide an overriding implementation, as in the case of draw(). The draw() function is known as a pure virtual function, because it does not have an implementation in class Shape. Pure virtual functions have a declaration of the form
virtual void draw() = 0;
Since we have not implemented draw() in class Shape, the class is incomplete and we cannot actually create Shape objects. The Shape class is therefore said to be an abstract base class.
We must take care when deleting the objects stored in the array of Shape pointers. In the code fragment
for (i = 0; i < num_shapes; i++)
delete sa[i]; //
This will call either Circle::~Circle() or Rectangle::~Rectangle(), as
appropriate,
// before calling Shape::~Shape().
we have called delete on sa[i], which is a Shape
pointer, even though the object that it points to is really a Circle
or a Rectangle. To ensure that the appropriate Circle
or Rectangle destructor is called, we must make the Shape
destructor a virtual destructor.
shape.h
#ifndef _SHAPE_H_
#define _SHAPE_H_
#include <iostream.h>
#include "point.h"
#ifndef DEBUG_PRINT
#ifdef _DEBUG
#define DEBUG_PRINT(str) cout << str << endl;
#else
#define DEBUG_PRINT(str)
#endif
#endif
class Shape {
// The private members of the Shape class are
only accessible within
// the definition of class Shape. They
are not accessible within
// the definitions of classes derived from the
Shape class, e.g. Circle,
// or within main().
private:
Point mCentroid;
// The protected members of the Shape class are
accessible within the
// definition of class Shape. They are
also accessible within the
// definitions of classes derived immediately
from the Shape class, e.g.
// Circle. However, they are not accessible
within main().
protected:
float mfArea;
// The public members of the Shape class are accessible
everywhere i.e. in
// the Shape class definition, in derived class
definitions and in main().
public:
Shape(float fX, float fY);
virtual ~Shape();
// A virtual destructor.
virtual void print();
// A virtual function.
virtual void draw() = 0; //
A pure virtual function.
const Point& get_centroid() {
return mCentroid;
}
};
#endif
shape.C
#include "shape.h"
Shape::Shape(float fX, float fY) : mCentroid(fX, fY) {
// We must use an initialization list to initialize
mCentroid.
// Here in the body of the constructor would
be too late.
DEBUG_PRINT("In constructor Shape::Shape(float,
float)")
}
Shape::~Shape() {
DEBUG_PRINT("In destructor Shape::~Shape()")
}
void Shape::print() {
DEBUG_PRINT("In Shape::print()")
cout << "Centroid: ";
mCentroid.print();
cout << "Area = " << mfArea <<
endl;
}
circle.h
#ifndef _CIRCLE_H_
#define _CIRCLE_H_
#include "shape.h"
class Circle : public Shape {
private:
float mfRadius;
public:
Circle(float fX=0, float fY=0, float fRadius=0);
~Circle();
void print();
void draw();
};
#endif
circle.C
#include "circle.h"
#define PI 3.1415926536
Circle::Circle(float fX, float fY, float fRadius) : Shape(fX, fY)
{
// We must use an initialization list to initialize
the Shape part of the Circle object.
DEBUG_PRINT("In constructor Circle::Circle(float,
float, float)")
mfRadius = fRadius;
mfArea = PI * fRadius * fRadius; // mfArea
is a protected member of class Shape.
}
Circle::~Circle() {
DEBUG_PRINT("In destructor Circle::~Circle()")
}
void Circle::print() {
DEBUG_PRINT("In Circle::print()")
cout << "Circle Radius: " << mfRadius
<< endl;
// If we want to print out the Shape part of the
Circle object as well,
// we could call the base class print function
like this:
Shape::print();
}
void Circle::draw() {
// Assume that this draws the circle.
DEBUG_PRINT("In Circle::draw()")
}
rectangle.h
#ifndef _RECTANGLE_H_
#define _RECTANGLE_H_
#include "shape.h"
class Rectangle : public Shape {
private:
float mfWidth, mfHeight;
public:
Rectangle(float fX=0, float fY=0, float fWidth=1,
float fHeight=1);
~Rectangle();
void print();
void draw();
};
#endif
rectangle.C
#include "rectangle.h"
Rectangle::Rectangle(float fX, float fY, float fWidth, float fHeight)
: Shape(fX, fY) {
// We must use an initialization list to initialize
the Shape part of the Rectangle object.
DEBUG_PRINT("In constructor Rectangle::Rectangle(float,
float, float, float)")
mfWidth = fWidth;
mfHeight = fHeight;
mfArea = fWidth * fHeight; // mfArea is
a protected member of class Shape.
}
Rectangle::~Rectangle() {
DEBUG_PRINT("In destructor Rectangle::~Rectangle()")
}
void Rectangle::print() {
DEBUG_PRINT("In Rectangle::print()")
cout << "Rectangle Width: " << mfWidth
<< " Height: " << mfHeight << endl;
// If we want to print out the Shape part of the
Rectangle object as well,
// we could call the base class print function
like this:
Shape::print();
}
void Rectangle::draw() {
// Assume that this draws the rectangle.
DEBUG_PRINT("In Rectangle::draw()")
}
myprog.C
#include "shape.h"
#include "circle.h"
#include "rectangle.h"
int main() {
const int num_shapes = 5;
int i;
// Create an automatic Circle object.
Circle c;
// We cannot instantiate a Shape object because
the Shape class has a pure virtual function
// i.e. a virtual function without a definition
within class Shape. Class Shape is therefore said
// to be an abstract base class.
// Shape s; // This is not allowed.
// We are allowed to have Shape pointers, however.
Shape *sa[num_shapes];
// Create an array of Shape pointers.
// C++ allows us to use a base class pointer to
point to a derived class object. This is known
// as polymorphism. We can thus store a
heterogeneous collection of Circles and Rectangles
// using the array of Shape pointers.
sa[0] = new Circle(2,3,1);
sa[1] = new Rectangle(0,2,2,3);
sa[2] = new Circle(7,6,3);
sa[3] = new Circle(0,2,2);
sa[4] = new Rectangle(4,3,1,1);
// Print out all of the objects. We have
made the print function virtual
// in class shape. This means that it can
be overridden by print functions
// with a similar signature that are specific
to the derived classes. If
// a derived class does not provide an implementation
of print, then the
// Shape::print function will be called by default.
for (i = 0; i < num_shapes; i++) {
sa[i]->print();
// This will call either Circle::print() or Rectangle::print(), as appropriate.
}
// Delete the objects. Note that we have
called delete on Shape pointers,
// even though the objects that we created using
new were derived class
// objects. To ensure that the appropriate
destructor for the derived
// object is called, we must make the Shape destructor
virtual.
for (i = 0; i < num_shapes; i++)
delete sa[i]; //
This will call either Circle::~Circle() or Rectangle::~Rectangle(), as
appropriate,
// before calling Shape::~Shape().
return 0;
}