C++ Week 18

Inheritance

Reusing code

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.

Defining a base 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;
}

Derived class definitions

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.

Using derived-class objects

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 Property
It 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.

Dynamic binding and polymorphism

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:

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; }

A Shape class: pure virtual functions and abstract base classes

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