Virtual functions in C++
Class derivation and virtual functions let you define a common interface for related classes with similar, yet distinct, behaviors.
Over the past couple of years, I've written numerous articles about representing and manipulating memory-mapped devices in C and C++. Some readers commented that the C++ techniques I presented were lacking in that they failed to use more advanced features such as inheritance and virtual functions.
Classes with virtual functions can be very useful, but they aren't the solution to every problem. Classes that represent memory-mapped devices, such as the ones I've presented, work with real hardware specifically because they don't use virtual functions.
Last month (“Discriminated unions,”March 2012, p. 9. www.eetimes.com/4237055), I looked at the sort of problem that virtual functions are good at solving. I described a typical C solution using discriminated unions and discussed the limitations of that approach. This month, I'll explain how to use virtual functions in C++ to solve the same problem. In the not-too-distant future, I'll also show you how C++ typically implements virtual functions by showing how you can emulate them in C.
Although my primary focus here is virtual functions, of necessity I'll discuss class derivation as well. However, I'll cover only those aspects of derivation that I need to explain virtual functions.
Deriving a class
The problem I presented last month involved representing a collection of two-dimensional geometric shapes such as circles, rectangles, and triangles. Each shape object contains some linear or angular distances sufficient to characterize the physical extent of the shape. For example, a circle has a radius, a rectangle has a height and a width, and a triangle has two sides and an angle. Each shape also had common attributes, such as position (planar coordinates), or outline and fill colors.
In C++, you can represent the various shapes as a collection of classes. You start by defining a shape
class which captures the properties common to all shapes:
class shape {public: shape(); // constructor double area() const; double perimeter() const; ~~~private: coordinates position; color outline, fill;};
The class declares several functions as public members, including area
and perimeter
. The intent is that every shape–whether it's a circle, rectangle, triangle, or anything else–will provide these functions. In effect, class shape
defines a common interface shared by every specific type of shape.
The shape
class also declares data members position
, outline
, and fill
. The intent is that every shape
object–no matter what kind of shape it is–will contain storage for these members.
Now you can define classes for each specific shape by deriving them from class shape
. For example, you can define the circle
class as:
class circle: public shape {public: circle(double r); // constructor double area() const; double perimeter() const; ~~~private: double radius;};
In the class definition above, the class heading:
class circle: public shape
specifies that class circle
is derived from class shape
. The “derived from” class is commonly called the “base” class.
The keyword public
in the class heading indicates that the derivation is public. C++ also permits private and protected derivation, but I'm going to ignore those variants here, and make the simplifying assumption that all derivation is public. It's by far the most common usage.
A derived class inherits all the data members of its base class. That is, the data members declared in the base class occupy storage in the derived class as well. Members declared private in the base class won't be accessible in the derived class, but they will still occupy storage in derived class objects.
For example, the base class shape
has three data members, declared as:
coordinates position; color outline, fill;
The derived class circle
declares one more data member:
double radius;
Thus, the data storage for a circle
object is essentially the same as that of a structure defined as:
struct circle { coordinates position; color outline, fill; double radius;};
A derived class also inherits member functions from its base class. However, special member functions such as constructors, destructors, and copy assignment operators aren't inherited.
If the circle
class didn't declare area
and perimeter
as member functions, circle
would inherit the functions exactly as they're defined in the shape
base class. For example, I have yet to show the definition for shape
's area
functions, but whatever it is, it can't be right for circle
. The problem is that a circle's area depends on its radius
, but the radius data member is defined in class circle
, not in class shape
. Although a derived class may have access to data members inherited from a base class, a base class normally can't see the members declared in its derived classes. Thus, the shape
class doesn't know that a circle
has a radius
.
The definition for the circle
's area
function is very simple:
double circle::area() const { return pi * radius * radius;}
where pi
is presumably a previously-defined constant representing π. The keyword const
appearing after the function parameter list indicates that area
function treats each circle
as a constant object. That is, computing the area of a circle doesn't alter the circle.
A hierarchy of shapes
You can easily create derived classes for additional shapes. For example, here are class definitions for rectangle:
class rectangle: public shape {public: rectangle(double h, double w); double area() const; double perimeter() const; ~~~private: double height, width;};
and for triangle:
class triangle: public shape {public: triangle(double s1, double s2, double a); double area() const; double perimeter() const; ~~~private: double side1, side2, angle;};
All of these shapes can be viewed as a hierarchy, typically drawn with the base class at the top, shown in Figure 1.
The base class doesn't know that it has any derived classes. Each derived class knows of its base class, but doesn't know about any of the other derived classes.
The is a relationship
Derivation defines an “is a” or “is a kind of” relationship between the derived and base class. That is, a circle
is a shape
, a rectangle
is a shape
, and a triangle
is a shape
. This relationship is only in the direction of the arrows in the diagram–from derived to base, but not from base to derived or from derived to derived. For example, a shape
is not necessarily a circle
, and a circle
is certainly not a rectangle
.
In C++, the is a relationship manifests itself as a built-in conversion from “pointer to derived class” into “pointer to base class”. This lets you create a collection of assorted shapes as an array of pointer to shapes, and populate that array with pointers to circles, rectangles, and triangles. For example:
shape *sc[4];~~~sc[0] = new circle (2);sc[1] = new triangle (5, 6, asin(0.8));sc[2] = new rectangle (3, 4);sc[3] = new circle (3);
Here, the new-expression:
new circle (2)
creates a circle
initialized with radius 2 and returns the address of that circle
as a value of type “pointer to circle
“. The assignment:
sc[0] = new circle (2);
assigns that “pointer to circle
” to a “pointer to shape
“, but this is fine because a circle
is a shape
.
Last month, I presented a function called largest
which determines the shape with the largest area in a collection of shapes. Here is a version of that function, written in C++ to work with the hierarchy of shapes:
shape const *largest(shape const *s[], size_t n) { shape const *p = NULL; double max = -1; for (size_t i = 0; i < n; ++i) { double area = s[i]->area(); if (area > max) { max = area; p = s[i]; } } return p;}
Here, s[i]
is a pointer to a shape in the collection. The expression s[i]->area()
is supposed to call the area
function for each shape
. Unfortunately, with the class definitions as is, it doesn't yet work correctly. Here's why…
Static vs. dynamic binding
Again, an assignment such as:
sc[0] = new circle (2);
assigns a “pointer to circle
” to a “pointer to shape
“. Such assignments make it harder to talk about the type of sc[0]
: it's declared to point to a shape
, but it actually points to a circle
. Later in the same program, another assignment such as:
sc[0] = new rectangle (3, 4);
assigns a “pointer to rectangle
” to a “pointer to shape
“. Pointer sc[0]
is still declared to point to a shape
, but now it points to a rectangle
.
Here's some terminology that makes it easier to discuss such schizophrenic behavior. The static type of an expression is its type based on the declared type of the operands in that expression. The compiler determines an expression's static type during compilation. That type doesn't change as long as the source program doesn't change. The dynamic type of an expression is the type of the object that the expression actually designates at some point during program execution. The dynamic type of an expression may change during execution.
For example, the expression sc[0]
always has static type “pointer to shape
“. After the assignment:
sc[0] = new circle (2);
the expression sc[0]
still has static type “pointer to shape
“, but now it has dynamic type “pointer to circle
“. After the assignment:
sc[0] = new rectangle (3, 4);
the expression sc[0]
still has static type “pointer to shape
“, but now it has dynamic type “pointer to rectangle
“.
Now, here's why the largest
function doesn't yet work correctly. The expression s[i]->area()
is supposed to compute the area based on the dynamic type of s[i]
. That is, if s[i]
actually points to a circle
, then s[i]->area()
should call the circle
's area
function. If s[i]
actually points to a rectangle
, then s[i]->area()
should call the rectangle
's area
function. And so on. Unfortunately, it doesn't work that way, yet.
By default, C++ uses static binding for member function calls. That is, s[i]
has static type “pointer to shape
“, so s[i]->area()
always calls the area
function for the static type of *s[i]
, which is the base class shape
. However, the largest
function needs the call to use dynamic binding: s[i]->area()
should call the area
function for the dynamic type of *s[i]
, be it circle
, rectangle
, triangle
, or any type derived from shape
.
Virtual functions
To get the area
functions in all the shapes to bind dynamically, all you have to do is declare area
as a virtual function in the base class:
class shape {public: shape(); // constructor virtual double area() const; virtual double perimeter() const; ~~~};
I want the perimeter
function to be dynamically bound as well, so I declared it virtual, too. That's all I had to do. Now the largest
function works just as it should.
The “virtual” property is an inherited trait: a function in a derived class with the same name and parameter type(s) as a function in its base class is virtual by default. That is, in:
class rectangle: public shape {public: rectangle(double h, double w); double area() const; ~~~};
the area
function is virtual, even though it doesn't say so explicitly. If you prefer to be explicit, you can write:
class rectangle: public shape {public: rectangle(double h, double w); virtual double area() const; ~~~};
and it behaves just the same.
A lot more to come
I'll have more to say in future columns about how virtual functions work and how you can emulate them in C.
Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at .
This content is provided courtesy of Embedded.com and Embedded Systems Design magazine.
See more content from Embedded Systems Design and Embedded Systems Programming magazines in the magazine archive.
Sign up for subscriptions and newsletters.
Copyright © 2012
UBM–All rights reserved.
“Thanks Dan! great article, I enjoy reading your article.nQuestion please:nn”A derived class inherits all the data members of its base class. That is, the data members declared in the base class occupy storage in the derived class as well. Members dec