MODULE 6
Virtual Function Hierarchy, Pure Virtual
Functions and Abstract Classes
|
Learning
Objectives After completing this module,
students should be able to: •
Predict which virtual function runs in a class hierarchy. •
Declare pure virtual functions and abstract classes, and explain
what a derived class must do. •
Define a pure virtual function and a pure virtual destructor with
a body, and explain why. •
Use abstract classes as interfaces, including multiple
interfaces. •
Explain what happens when virtual functions are called in
constructors and destructors. •
Use polymorphic containers of smart pointers. |
1. Virtual Functions and the
Class Hierarchy
In a hierarchy, a virtual function can be redefined at any level. When it
is called through a base-class pointer or reference, C++ uses the most
derived override that is available for the object’s real class. If the real
class (or the nearest class above it) has no override, the next version up the
chain is used.
|
Situation |
Version
that runs |
|
The object’s class overrides the function |
The version in the object’s own class |
|
The object’s class does not override it, but a parent class
does |
The nearest override above it |
|
No class below the base overrides it |
The base-class version |
|
The function is pure virtual in the base class |
Every concrete class must provide an override (Section 2) |
Program 6.1: A hierarchy Person → Student
and Teacher.
#include <iostream>
using namespace std;
class Person {
public:
virtual
void displayRole() { cout << "General person" << '\n'; }
virtual
~Person() = default;
};
class Student : public Person {
public:
void
displayRole() override { cout << "University student" <<
'\n'; }
};
class Teacher : public Person {
public:
void
displayRole() override { cout << "University teacher" <<
'\n'; }
};
int main()
{
Student s;
Teacher t;
Person *p =
&s;
p->displayRole();
p = &t;
p->displayRole();
return 0;
}
Output:
University student
University teacher
The same pointer variable p first
refers to a Student and then to a Teacher; each time the call p->displayRole()
reaches the correct override. The earlier example with three levels (Program
5.4, Module 5) shows the “nearest override” rule.
Sometimes the base class has no sensible default behaviour. A
generic Shape cannot compute an area, and
a general Employee cannot calculate a
salary. For such cases C++ provides pure virtual functions.
2. Pure Virtual Functions and
Abstract Classes
|
Definition: A
pure virtual function is a virtual function declared with =
0 and normally without a body: virtual
double area() = 0;. A class that has at least one pure
virtual function is an abstract class. |
Figure 6.1: Shape
is abstract; Circle and Rectangle are concrete classes.
A pure virtual function says: “every concrete class derived from
me must provide this function”. It defines an interface and leaves the
implementation to the derived classes.
Program 6.2: An abstract class Shape and
two concrete classes.
#include <iostream>
using namespace std;
class Shape {
public:
virtual
double area() = 0; //
pure virtual function
virtual
~Shape() = default;
};
class Rectangle : public Shape {
private:
double
length, breadth;
public:
Rectangle(double l, double b) : length(l), breadth(b) {}
double
area() override { return length * breadth; }
};
class Circle : public Shape {
private:
double
radius;
public:
Circle(double r) : radius(r) {}
double
area() override { return 3.14159 * radius * radius; }
};
int main()
{
Rectangle
r(5, 4);
Circle
c(3);
Shape *ptr
= &r;
cout
<< "Rectangle area: " << ptr->area() << '\n';
ptr =
&c;
cout
<< "Circle area: " << ptr->area() << '\n';
return 0;
}
Output:
Rectangle area: 20
Circle area: 28.2743
•
Shape
is abstract: area() is pure virtual. Rectangle and Circle
override it and are concrete.
•
The pointer ptr
has the type Shape *, yet ptr->area() computes the area of the real object: 5 ×
4 = 20 and 3.14159 × 3 × 3 = 28.2743.
•
The abstract class gives all shapes the same interface,
so code written for Shape works with
every shape added later.
2.1 Rules for abstract classes
|
Rule |
Explanation |
|
No objects |
An object of an abstract class cannot be created, nor can
one be passed or returned by value |
|
Pointers and references are allowed |
Shape
* and Shape & are the way abstract classes are
used |
|
Must override to be concrete |
A derived class that overrides all pure virtual
functions is concrete; otherwise it is abstract too |
|
May contain anything else |
Data members, constructors, ordinary and virtual functions,
and a destructor |
|
Constructors |
Allowed; they run when a derived object is created, to
initialise the abstract part |
|
Destructor |
Should be virtual, because objects are deleted through base
pointers |
Trying to create an object of an abstract class is a compile-time error:
error: cannot declare variable ‘s’ to be of abstract
type ‘Shape’
A derived class that overrides only some of the pure virtual functions is
still abstract:
error: cannot declare variable ‘h’ to be of abstract
type ‘Half’
Program 6.3: A payroll hierarchy: one pure
virtual and one ordinary virtual function.
#include <iostream>
using namespace std;
class Employee {
public:
virtual
double calculateSalary() = 0;
virtual
void display() { cout << "Employee information" << '\n';
}
virtual
~Employee() = default;
};
class PermanentEmployee : public Employee {
private:
double
monthlySalary;
public:
PermanentEmployee(double salary) : monthlySalary(salary) {}
double
calculateSalary() override { return monthlySalary; }
void
display() override { cout << "Permanent employee" <<
'\n'; }
};
class ContractEmployee : public Employee {
private:
double
hourlyRate;
int
hoursWorked;
public:
ContractEmployee(double rate, int hours)
:
hourlyRate(rate), hoursWorked(hours) {}
double
calculateSalary() override { return hourlyRate * hoursWorked; }
void
display() override { cout << "Contract employee" << '\n';
}
};
int main()
{
PermanentEmployee p(50000);
ContractEmployee c(500, 80);
Employee
*employees[] = {&p, &c};
for
(Employee *e : employees) {
e->display();
cout
<< "Salary: " << e->calculateSalary() << '\n';
}
return 0;
}
Output:
Permanent employee
Salary: 50000
Contract employee
Salary: 40000
calculateSalary() is pure virtual
because the salary rule differs in every kind of employee. display() is an ordinary virtual function with a default
body that derived classes may keep or replace. Both are called through Employee *, in a loop, without any if on the type of the employee.
3. Pure Virtual Functions with a
Body; Pure Virtual Destructors
Although a pure virtual function normally has no body, C++ allows a
definition outside the class. The class stays abstract, but derived classes
can call the shared code with the qualified name, for example Logger::log().
Program 6.4: A pure virtual function that
has a body.
#include <iostream>
using namespace std;
class Logger {
public:
virtual
void log() = 0; //
pure virtual, but defined below
virtual
~Logger() = default;
};
// a pure virtual function may have a body
void Logger::log()
{
cout
<< "[common log header] ";
}
class FileLogger : public Logger {
public:
void log()
override
{
Logger::log();
// reuse the shared code explicitly
cout
<< "writing to file" << '\n';
}
};
int main()
{
// Logger
l; //
error: Logger is abstract
FileLogger
f;
f.log();
return 0;
}
Output:
[common log header] writing to file
3.1 Pure virtual destructor
Sometimes a class must be abstract but has no other function that is
naturally pure. A pure virtual destructor makes the class abstract.
Unlike other pure virtual functions it must be defined, because every
derived destructor calls it at the end of its execution.
Program 6.5: A pure virtual destructor with
a definition.
#include <iostream>
using namespace std;
class Interface {
public:
virtual
~Interface() = 0; //
pure virtual destructor
};
Interface::~Interface() // it MUST be defined
{
cout
<< "Interface destructor" << '\n';
}
class Impl : public Interface {
public:
~Impl()
override { cout << "Impl destructor" << '\n'; }
};
int main()
{
Interface
*p = new Impl;
delete p;
return 0;
}
Output:
Impl destructor
Interface destructor
Deleting through Interface *
runs ~Impl() first and then the body
of ~Interface(). If the body of ~Interface() were missing, the program would fail to
link.
4. Abstract Classes as
Interfaces
An interface is an abstract class whose members are all pure
virtual (and usually a virtual destructor). It describes what an
object can do and not how. A class can implement several interfaces
by inheriting them all; since interfaces have no data, multiple inheritance of
interfaces avoids most problems of Module 2.
Figure 6.2: Report
implements two interfaces, Drawable and Printable.
Program 6.6: A class that implements two
interfaces.
#include <iostream>
using namespace std;
/* Interfaces: classes with only pure virtual
functions */
class Drawable {
public:
virtual
void draw() const = 0;
virtual
~Drawable() = default;
};
class Printable {
public:
virtual
void print() const = 0;
virtual
~Printable() = default;
};
class Report : public Drawable, public Printable {
public:
void draw()
const override { cout << "Drawing the report" << '\n'; }
void
print() const override { cout << "Printing the report" <<
'\n'; }
};
void render(const Drawable &d) { d.draw(); }
void output(const Printable &p) { p.print(); }
int main()
{
Report r;
render(r); //
used through the Drawable interface
output(r); //
used through the Printable interface
return 0;
}
Output:
Drawing the report
Printing the report
render() knows only about Drawable and output()
only about Printable; both work with a Report because it is-a Drawable
and is-a Printable.
|
Note: C++ has no interface
keyword. By convention an “interface” is a class with only pure virtual
functions. Languages such as Java and C# have a separate keyword for the same
idea. |
5. Virtual Functions in
Constructors and Destructors
While the constructor of a base class runs, the derived part of
the object does not exist yet, so the object is treated as an object of the
base class. A virtual call made inside a constructor therefore calls the
version of the class whose constructor is running, not the derived
override. The same holds in destructors: the derived part has already been
destroyed.
Figure 6.3: During
construction and destruction a virtual call follows the part of the object that
is currently active.
Program 6.7: Virtual calls during
construction and destruction.
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor: "; whoAmI(); }
virtual
void whoAmI() const { cout << "I am Base" << '\n'; }
virtual
~Base() { cout << "Base destructor: "; whoAmI(); }
};
class Derived : public Base {
public:
Derived() {
cout << "Derived constructor: "; whoAmI(); }
void
whoAmI() const override { cout << "I am Derived" << '\n';
}
~Derived()
override { cout << "Derived destructor: "; whoAmI(); }
};
int main()
{
Derived d;
cout
<< "In main: ";
d.whoAmI();
return 0;
}
Output:
Base constructor: I am Base
Derived constructor: I am Derived
In main: I am Derived
Derived destructor: I am Derived
Base destructor: I am Base
•
In the Base
constructor whoAmI() prints “I am Base”,
although the object being built is a Derived.
•
In main(),
after construction, the call reaches Derived::whoAmI().
•
In the destructors the order is reversed: first the Derived destructor (Derived version), then the Base destructor (Base version).
|
Important: Never call a pure
virtual function (directly or indirectly) from a constructor or destructor of
the abstract class: no override exists at that time, and the behaviour is
undefined (a typical program crashes with a “pure virtual function called”
message). |
6. Polymorphic Containers
Because polymorphism needs pointers or references, a collection of
objects of different classes is stored as a collection of base-class
pointers. Modern C++ uses smart pointers, so that the objects are deleted
automatically.
Program 6.8: A vector of unique_ptr<Shape>.
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Shape {
public:
virtual
double area() const = 0;
virtual
const char *name() const = 0;
virtual
~Shape() = default;
};
class Rectangle : public Shape {
double l,
b;
public:
Rectangle(double l, double b) : l(l), b(b) {}
double
area() const override { return l * b; }
const char
*name() const override { return "Rectangle"; }
};
class Circle : public Shape {
double r;
public:
explicit
Circle(double r) : r(r) {}
double
area() const override { return 3.14159 * r * r; }
const char
*name() const override { return "Circle"; }
};
int main()
{
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Rectangle>(5, 4));
shapes.push_back(make_unique<Circle>(3));
shapes.push_back(make_unique<Rectangle>(2, 2.5));
double
total = 0;
for (const
auto &s : shapes) {
cout
<< s->name() << " area = " << s->area()
<< '\n';
total
+= s->area();
}
cout
<< "Total area = " << total << '\n';
return
0; // unique_ptr
deletes the objects through ~Shape()
}
Output:
Rectangle area = 20
Circle area = 28.2743
Rectangle area = 5
Total area = 53.2743
•
The vector owns the shapes through unique_ptr; there is no delete
in the program.
•
The loop calls area()
and name() without knowing the real
class of each element. Total: 20 + 28.2743 + 5 = 53.2743.
•
Deleting a Shape
through ~Shape() is safe because the
destructor is virtual.
|
Version Note: unique_ptr
and shared_ptr
(header <memory>) are available from C++11;
make_unique
from C++14. In C++11 write unique_ptr<Shape>(new Circle(3));
in C++98 use raw pointers and delete them in a loop (the old auto_ptr
was removed in C++17). Range-based for
and auto,
used in the loop, are C++11. |
7. Comparison Tables
|
Feature |
Ordinary
function |
Virtual
function |
Pure
virtual function |
|
Declaration |
void
f(); |
virtual
void f(); |
virtual
void f() = 0; |
|
Body in the base class |
Required |
Required |
Not required (optional) |
|
Overriding in derived class |
Hides |
Overrides |
Must be overridden to be concrete |
|
Binding |
Early |
Late (through pointer/reference) |
Late |
|
Makes the class abstract |
No |
No |
Yes |
|
Feature |
Concrete
class |
Abstract
class |
|
Objects can be created |
Yes |
No |
|
Pointers and references |
Yes |
Yes |
|
Can be a base class |
Yes |
Yes (that is its purpose) |
|
Contains pure virtual functions |
None (or all overridden) |
At least one |
8. Quick Revision
•
Virtual call through a base pointer ⇒ the most derived
override for the object’s class runs; if none, the nearest one above it.
•
Pure virtual function: virtual
R f() = 0;. A class with one is abstract: no objects, but pointers and
references allowed.
•
A derived class must override all pure virtual
functions to become concrete.
•
A pure virtual function may have a body (call it with Class::f()); a pure virtual destructor must have one.
•
An interface is a class with only pure virtual
functions; a class may implement several.
•
In constructors and destructors virtual calls resolve
to the class being constructed or destroyed; never call a pure virtual function
there.
•
Store polymorphic objects as unique_ptr
or shared_ptr to the base class.
9. Important Examination
Questions
Short-answer questions
1.
What is a pure virtual function? Write its syntax.
2.
What is an abstract class? Can we create its object?
Can we create a pointer to it?
3.
What happens if a derived class does not override a
pure virtual function?
4.
Can a pure virtual function have a body? Can a pure
virtual destructor?
5.
What is an interface in C++?
6.
What happens when a virtual function is called inside a
constructor?
7.
Differentiate between a virtual function and a pure
virtual function.
Long-answer and
programming questions
1.
Explain abstract classes and pure virtual functions
with a program for geometric shapes.
2.
Write a program for an Employee
hierarchy with calculateSalary() pure virtual.
3.
Explain how a class can implement two interfaces, with
a program.
4.
Explain virtual function calls inside constructors and
destructors with a program and output.
5.
Write a program that stores objects of different
derived classes in a vector of
base-class smart pointers and calls a virtual function for each.
6.
Compare ordinary, virtual and pure virtual functions in
a table.
No comments:
Post a Comment