Often when designing a program you need to add new objects that resemble objects that already exist. For example in your library management software you have collections of books. Then someone decides that you are going to start renting out CDs, DVDs etc. You could easily just copy the book code and tweak it for the new requirements, but that means that:
C++ allows you to define classes that are variations on existing classes, so that you can reuse the basic methods of the Book class (date lent out, IDnumber), and add other data fields and methods that are specific to CDs, DVDs etc. (artist, composer, conductor, and so on). In C++ terminology the book class is the base class, and the CD class is a derived class. The derived class inherits from the base class. It's possible to have a base class which doesn't correspond to any real world object and which is never instantiated (e.g. LendableObject), i.e. you never have an object in your program of type LendableObject - the class only exists so that you can derive other classes (Book, CD etc) from it. This is known as an abstract class.
We'll develop a very simple system for an estate agent. The agent deals with properties for rent as well as properties for sale, so the system has a base class Property
and derived classes ForRent
and ForSale
. The Property
class is simple:
class Property { private: string address; int rooms; // number of rooms char condition; // 'A' = very good condition, 'B' not so good etc public: Property(string, int rms = -1, char cond = 'U'); // defaults indicate "unknown" void set_rooms(int); void set_condition(char); void print() const; }; Property::Property(string addr, int rms, char cond) : address(addr), rooms(rms), condition(cond) { }I have used here a field initializer list. I introduced this in an earlier lecture (with the
Safevec
class). We don't have to use this method. If you preferred, you could use assignments:
Property::Property(string addr, int rms, char cond) { address = addr; rooms = rms; condition = cond; }Now the member function definitions:
void Property::set_rooms(int rms) { rooms = rms; // you might want to include some validation } void Property::set_condition(char cond) { condition = cond; } void Property::print() const { cout << "Address: " << address << endl; cout << "Number of rooms: " << rooms << endl; cout << "Condition: " << condition << endl; }
Now we can define a ForRent
class derived from our Property
class:
class ForRent : public Property { private: int rent; int max_occupants; public: ForRent(string, int rms = -1, char cond = 'U', int rnt = -1, int nocc = -1); void set_rent(int); void set_occupants(int); void print() const; };Note the word "public" in
class ForRent : public Property
I'll come back to that later.
ForRent::ForRent(string addr, int rms, char cond, int rnt, int nocc) : Property(addr, rms, cond) { rent = rnt; max_occupants = nocc; }Here the use of the field initializer for
Property(addr, rms, cond)
is obligatory. It is the only way to initialize the inherited data (the Property
part) of a ForRent
object. If we called the Property
constructor inside the definition of the ForRent
constructor, we would create a local Property
variable, i.e. local to the ForRent
constructor. If we tried to initialize separately the data items of the Property
part of the ForRent
object with statements such as address = addr;
we would find that we were denied access to these data items.
This last point seems surprising. ForRent
has inherited address, rooms
and condition
from Property
yet it has no access to them. This is because these data items are private to the Property
class. Access control is enforced at the level of the class, and the Property
class is a different class from the ForRent
class, even though one is derived from the other.
We could, if we wanted, do the rest of the initialization in the field initializer list also:
ForRent::ForRent(string addr, int rms, char cond, int rnt, int nocc) : Property(addr, rms, cond), rent(rnt), max_occupants(nocc) { }
The other thing to notice about the ForRent
class definition is that it contains a prototype for the print
function that is identical to the one in Property
. This means that ForRent
does not want to simply inherit the print
function from Property
but will override it with a print
function of its own.
void ForRent::set_rent(int rnt) { rent = rnt; }Let's suppose, just for the sake of an example, that we decide to include some validation in the
set_occupants
function; if the number of occupants exceeds the number of rooms, we output an error message. We might try to write it like this:
void ForRent::set_occupants(int nocc) { if (nocc > rooms) cerr << "Too many occupants for the rooms" << endl; else max_occupants = nocc; }But we can't do that. As we've just discussed, we have no access to
rooms
from inside a ForRent
function definition.
There are two things we can do. One is to go back to our definition of the Property
class and add a public accessor function get_rooms
. Another is to move rooms
out of the private section and into a protected section, thus (and we might as well move condition
while we are about it):
class Property { private: string address; protected: int rooms; // number of rooms char condition; // 'A' = very good condition, 'B' not so good etc public: Property(string, int rms = -1, char cnd = 'U'); // defaults indicate "unknown" void set_rooms(int); void set_condition(char); void print() const; };Items in the protected part of a base class are accessible to classes derived from that base class, but not to the rest of the world. So, in this example,
ForRent
and ForSale
can now access rooms
and condition
.
Finally the print
procedure:
void ForRent::print() const { Property::print(); cout << "Max n of occupants: " << max_occupants << endl; cout << "Monthly rent: " << rent << endl; }Since the
Property
class already has a print
procedure, it makes sense to use it to output the first few lines of data, but if we simply made a call to print()
, this would be a recursive call to ForRent
's print
. So we have to specify that we mean the print
procedure in the Property
class Property::print()
.
We can define the ForSale
class along the same lines.
Having done all that, we can now use objects of these classes:
int main( ) { Property xp("1, Malet Street, Bloomsbury"); // we can have objects of the base class // (we're using the defaults for the other data items) ForRent xr("The Penthouse, Park Drive, Mayfair"); // we can also have objects of the derived classes, as you would expect ForSale xs("23 Railway Cuttings, Cheam"); xr.set_rent(3000); // we can call a derived class function, obviously xs.set_price(65000); xp.set_condition('B'); // we can call a base class function on a base class object xp.set_rooms(12); xr.set_condition('A'); // and we can also call a base class function on a derived class object xs.set_rooms(8); xp.print(); // calls Property::print xr.print(); // calls ForRent::print xs.print(); // calls ForSale::print }Note the lines
xr.set_condition('A');
and xs.set_rooms(8);
The ForRent
class does not have a set_condition
function of its own, nor does ForSale
have a set_rooms
. These are functions defined in the Property
class and they have been inherited by the derived classes. What is public in the base class is also public in the derived classes. This is, of course, the whole point of this inheritance, so that is what you would expect. But it only works because of the word "public" in the first line of the ForRent
(and ForSale
) class definitions:
class ForRent : public PropertyIt is possible to specify private inheritance (just replace "public" with "private"), in which case what is public in the base class is private in the derived classes. If we did that here, it would mean that
ForRent
could make use of, say, set_condition
within its own function definitions, but we could not say xr.set_condition('A')
from within main
. It is hard to think of a plausible reason why you would want this, and examples in text books are pretty strained. Annoyingly, private is the default (if you just write "class ForRent : Property
", private is what you get), so you have to remember to specify public inheritance.
Suppose we wanted a procedure that printed out the details of a property with a header and a footer, and suppose that we wrote a non-member function to do this, taking a property as its parameter:
void fancy_print(Property x) { cout << "Pricey and Ripoff, Estate Agents" << endl; x.print(); cout << "For more information see www.exorbitant.com" << endl; }If we supplied an object of the base class
Property
as the argument, as, for example:
fancy_print(xp);this would work OK. But, if we supplied an object of one of the derived classes, say a
ForRent
object:
fancy_print(xr);it would be only the
Property
part of the ForRent
object that would be printed out the extra items, special to ForRent
, would be sliced off in the process of parameter passing.
But we can arrange for it to print derived class objects, as well as base class ones, by the following:
virtual
before the print
prototype:
class Property { private: string address; int rooms; // number of rooms char condition; // 'A' = very good condition, 'B' not so good etc public: Property(string, int rms = -1, char cond = 'U'); // defaults indicate "unknown" void set_rooms(int); void set_condition(char); virtual void print() const; };
void fancy_print(const Property& x) { cout << "Pricey and Ripoff, Estate Agents" << endl; x.print(); cout << "For more information see www.exorbitant.com" << endl; }
Normally, it is the compiler that decides, when given a function call, which function it is that is being called (and, since you can have several functions with the same name, this is not always a trivial matter, as we will see in a later lecture). This process, of linking up the call with the definition, is known as binding, and, if it is performed at compile time, it is static binding. What we have just done, however, is to delay the process of choosing the appropriate function till run-time. When it encounters x.print();
it is now the run-time system, rather than the compiler, that decides which function to call. This is known as dynamic binding.
So, if we call fancy_print
and supply a ForRent
object as the argument say, fancy_print(xr);
it will be the ForRent::print()
function that will be called. If we supplied a ForSale
object fancy_print(xs);
it would be the ForSale::print()
that would be called.
If you want, you can put the word virtual
in front of the print
prototypes in the derived class definitions also, but this would only be for documentation, a reminder that the corresponding function in the base class is virtual. It is the one in the base class that is important.
Just as a base-class reference parameter can accept an argument of a derived class, so a base-class pointer can point at a derived-class object, and, again, you get dynamic binding:
Property* ppt = &xr; // Note Property*, not ForRent* ppt->print(); // calls ForRent::print() ppt = &xs; ppt->print(); // calls ForSale::print() vector<Property*> vp; vp.push_back(new ForRent("10 Downing Street")); vp.push_back(new ForSale("Dome, Greenwich")); // and so on, filling vp with pointers to, variously, ForRent and ForSale objects for (int i = 0; i < vp.size(); i++) vp[i]->print(); // calls the appropriate print() for the type of *(vp[i])
This feature of the language the ability to treat objects of different but related types in the same way, without worrying precisely which type each one is is known as polymorphism; objects of the same basic class can take on different forms.
It can happen that you want the derived class objects to have a function (like
print
in our example) that has this character but there is no function in the base class for you to label virtual. For example, you might want a commission
function that calculates the commission when a client buys a property or rents one, perhaps 2% of the sale price for a ForSale
and six months' rent for a ForRent
. But there is no commission
function in the Property
class. In that case, you have to include one anyway, even if the body is empty. (Some compilers complain if a function (as opposed to a procedure) has an empty body, in which case you can include return 0;
or something.) So the public part of our Property
class would contain the following line:
virtual int commission() const {} // or { return 0; }
Let's look at a different example. Suppose we wanted to manipulate shapes circles, rectangles etc. We might define a Shape
base class and define Circle
, Rectangle
and so forth as derived classes:
class Shape { public: virtual double area() const {} virtual void draw(int x_coord, int y_coord) const {} // imagine we have use of some graphics library .... }; class Circle : public Shape { private: double rad; static const double PI; public: Circle(double r) : rad(r) {} double area() const { return rad * rad * PI; } void draw(int x, int y) { draw_circle(x, y, rad); } // imagine this draws circle of radius rad at co-ordinates x, y .... }; const double Circle::PI = 3.14159; class Rectangle : public Shape { private: double hei, wid; public: Rectangle(double h, double w) : hei(h), wid(w) {} double area() const { return hei * wid ; } void draw(int x, int y) { draw_rectangle(x, y, hei, wid); } .... };
As it stands, it would be possible to define an object of type Shape
. However, Shape
has no constructor (since it has no data, it has no need of one), and, if you called one of its functions, it is not clear what would happen. In any case, it hardly makes sense to have a shape that is not any particular shape. We might decide to rule out objects of type Shape
, and we can do this by making the functions into pure virtual functions, as follows:
class Shape { public: virtual double area() const = 0; virtual void draw(int x_coord, int y_coord) const = 0; .... };Note the addition of
= 0
after the function prototypes. This makes a function a pure, virtual function. A pure, virtual function does not need to have a body, though it may do. Its purpose is solely to provide a basis for it to be overridden in the derived classes.
The addition of this = 0
is not only to make the functions pure virtual, but it also makes the class an abstract base class. That is to say, we cannot now have objects of type Shape
. The purpose of the Shape
class is solely to provide a basis for the derivation of Circle
, Rectangle
etc. Essentially it defines the interface that will be common to all the derived shapes.
It is enough to make just one function pure virtual to make the entire class abstract. If we made just, say, the commission
function in our Property
class pure virtual, then the Property
class would become abstract and we could no longer define objects of the base type Property
.
Generally a pure virtual function has no body, but it may do. Suppose that the print
procedure in the Property
class was made pure virtual. It would no longer be possible to call it on a Property
object since Property
would now be an abstract base class, but it would still be possible to call it in the derived classes as Property::print();
(If a pure virtual function has a body, the function definition has to be outside the class definition; the class definition contains just the prototype with "= 0" on the end.)
Our Shape
class has no data and so no constructor, but, if we made our Property
class abstract, it would still have the data items address
, rooms
and condition
and would still have a constructor for them. But, since this constructor could only ever be called from inside the derived classes, it could be protected
rather than public
.
Does a derived class have to override all the functions of an abstract base class? No, but if it fails to override a pure virtual function, then it will itself become an abstract class.
Click here for a full listing of the Shape example showing the use of an abstract base class.
Notes on R. Mitton's lectures by S.P. Connolly, edited by R. Mitton, 2000/08