Inheritance, Polymorphism and Virtual Functions
(Ref. Lippman 2.4, 17.1-17.6)
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;
}