Archive

C++ - Part 3



Tony Houghton

This month, I will be looking at derived classes which are C++'s mechanism for implementing inheritance, an essential feature of Object Oriented Programming (OOP).

Defining a derived class

Imagine you had written a class in the past to describe an Archimedes:

class Archimedes {
  const int ram;
  const int processor;// eg 2, 3, 250
  const int riscos;// eg 200, 310
  // ...
public:
  Archimedes(int mem, int arm, int os) :
    ram(mem), processor(arm), riscos(os) {}
  int get_ram(void) {return ram;}
  int get_processor(void) {return processor;}
  int get_riscos_version(void) {return riscos;}
  // ...
};

Now you want to write a class to describe a Risc PC; this has a great deal in common with an Archimedes and you just want to add a few members without disturbing the original class or rewriting the same members for a new one. You can do this with a derived class:

class RiscPC : public Archimedes {
  const int vram;
  // ...
public:
  RiscPC(int mem, int arm, int os, int vram) :
    Archimedes(mem, arm, os), RiscPC::vram(vram) {}
  int get_vram(void) return vram;
  // ...
};

The colon after the new class name (RiscPC) signifies that RiscPC is derived from any classes following the colon. Classes after the colon are base classes - each one has its own access specifier (e.g. public) which I will describe below. A derived class inherits all the members of its base classes; they become members of the derived class. When the base class is public, as in this case, the members of the base class can be accessed from 'outside' exactly as if they were defined in the derived class. So here you could call the members get_ram(), get_processor() etc, as well as get_vram() for RiscPC objects.

Derivation can continue for as many levels as you like. For instance, you could define:

class RiscPC700:public RiscPC {/*...*/};

RiscPC is a direct base class of RiscPC700; Archimedes is an indirect base class of RiscPC700.

Friends

Friends are not inherited.

Inheritance and constructors

Constructors are not inherited, so if a base class has a constructor with arguments, it must be called from the constructor of any class derived from it. This must be done in the constructor's initialisation list, i.e. between the colon and the body of the constructor function in its definition (see my previous article). More than one base class can be initialised here along with other members (such as the vram member above), separated by commas. Order of initialisation depends on the order of declaration of the base classes at the top of the derived class' definition, not the order in the derived class' constructor's initialisation list. Base classes are initialised before members.

Access control

There are three access specifiers: public, protected and private. These can be applied to members of a (base) class and to the whole class when it is inherited into a derived class. I will summarise the rules for each specification of a base class:

Public base class

Public members of the base class can be accessed from any other function.

Protected members of the base class can be accessed only from members of the base class or classes derived from it.

Private members of the base class can only be accessed from other members of the base class.

Protected base class

Public and protected members of the base class can be accessed only from members of the base class or classes derived from it; they become protected members of the derived class.

Private members of the base class can only be accessed from other members of the base class.

Private base class

Similar to protected base class, but the public and protected members of the base become private members of the derived class.

The most significant effect of the difference between protected and private base classes occurs when you derive a further class from the derived class. It determines whether the indirectly derived class has access to the original base.

The access specification of a base class' member can be adjusted by a derived class, provided it has access to the member and does not try to reduce access:

class Base {
  int a;
public:
  int b;
};
class Derived : private Base {
  Base::b;  // Error: attempt to reduce access
public:
  Base::a;  // Error: Derived does not have access to Base::a
  Base::b;  // Now external functions can access 
                                          Base::b via Derived
};

Pointer conversion

One thing that makes inheritance so powerful is that a pointer to a base class can also point to a class derived from it, if the function performing the conversion has access to the base class.

class Base {/*...*/};
class PubDerived : public Base {/*...*/};
class ProtDerived : protected Base {
public:
  Base *get_base(void);
};
class PrivDerived : private Base {
public:
  Base *get_base(void);
};
void f(PubDerived *pub, ProtDerived *prot,PrivDerived *priv)
{
  Base *b;
  b = pub;    // OK
  b = prot;   // Error: no global access to protected bases
  b = priv;   // Error: no global access to private bases
}
Base *ProtDerived::get_base()
{
  return this;  // OK: member has access to its protected 
                                                    bases
}
Base *PrivDerived::get_base()
{
  return this;  // According to Stroustrup this should be 
                                                 an error,
                // (PrivDerived theoretically not having 
                                           access to Base)
                // but it is OK with Acorn C++ at least
}

The same rules apply to references to objects.

It is also possible to explicitly cast a pointer to a base class to a pointer to a class derived from it. In either case, the compiler modifies the integral value of the pointer if necessary (a base class will not necessarily be placed at the very start of the derived class in memory). The compiler cannot detect whether a pointer to a base class does in fact point to a sub-object within a derived class, so trying to cast a pointer in this case will not cause a compilation error, but may crash the program at run-time.

Assignment

A derived class can be assigned to one of its base classes:

  Base b;
  PubDerived d;
  b = d;  // OK
  d = b;  // Error

The first assignment assigns b's members with the subset of those from d which are inherited from Base. The second is not allowed because it would potentially leave some of d's members unaccounted for.

Virtual functions

A derived class can have member functions with the same name and arguments as member functions in its base class. The derived class' function will override the base class' function when called from the derived class, but the base class' function will be called when used via a derived object's base sub-object:

#include <iostream.h>
class Base {
public:
  void f(void) {cout << "Base::f()\n";}
};
class Derived {
public:
  void f(void) {cout << "Derived::f()\n";}
};
int main()
{
  Derived *d = new Derived;
  Base *b = new Derived;
  b->f();// Base::f()
  d->f();// Derived::f()
  d->Base::f();// Base::f()
}

Virtual functions provide us with a very powerful mechanism to ensure that a derived class' overriding function is always called even if called from its base sub-object:

#include <iostream.h>
class Base {
public:
  virtual void f(void) {cout <<"Base::f()\n";}
  void g(void) {f();}
};
class Derived {
public:
  void f(void) {cout << "Derived::f()\n";}
};
int main()
{
  Derived *d = new Derived;
  Base *b = new Derived;
  b->f();// Derived::f()
  b->g();// g() calls Derived::f(), 
                                        not Base::f()
  d->f();// Derived::f()
  d->Base::f();// Qualification allows access
                                         to Base::f()
}

If a derived class does not have its own definition of the virtual function, its base class' function is used.

Selecting the correct function is done at run-time. To achieve this, certain implementation-dependent information needs to be stored with each class, so you cannot make assumptions about the storage space needed for objects with virtual functions.

A virtual function may be declared pure; this means that a version is not defined for the base class, but must be provided by a derived class. To declare a pure virtual function, append "=0" to its declaration. For example:

class Base {
public:
  virtual void f(void) = 0;
};

Any class containing a pure virtual function is an abstract class. Objects cannot be created from an abstract class, and classes derived from it are also abstract unless they contain definitions for all virtual functions. A virtual function can be redeclared virtual (optionally pure) in a derived class if you intend to derive further classes from it.

Since a base class' constructor is called before constructing the rest of a derived class, calling a virtual function during a constructor calls the base class' function; the mechanism to find a suitable overriding function in a derived class is not invoked. This is not possible for pure virtual functions, neither is explicit qualification (::) with the name of the base class.

The access specification of a virtual function (public/private/protected) depends only on the initial declaration in the base class; access specification of overriding functions is ignored.

A good way to see how powerful virtual functions are is to consider a collection of shape classes. There would be a base class called Shape and several classes derived from it eg Circle, Square, Triangle. If Shape had a virtual function void draw() which was defined for each derived shape, you could then draw any shape from 'external' functions without needing to know what sort of shape you were dealing with:

class Shape {
  // ...
public:
  virtual void draw(void) = 0;
        // Abstract shape can't be drawn
  // ...
};
// Definitions of derived shapes:
// Circle, Square etc
void update_screen(list_of_shapes &list)
{
  Shape *shape = list.get_first();
  while (shape)
  {
    shape->draw();
    shape = list.get_next();
  }
}

Virtual destructors

Although the default operator delete does not need to be passed the size of the object it is deleting (the size is stored alongside the object), it is possible to redefine delete (this will be the subject of a future article) in ways that do require the size to be passed. This can lead to a crash when deleting derived objects from pointers to their base sub-objects. For example:

void f()
{
  Base *b = new Derived;
  // ...
  delete b;  // Trouble
}

This is because delete, in this case, is passed the size of a Base instead of the size of a Derived.

If a class has a destructor, the actual removal of an object from memory is done as if called from the destructor, and the size is calculated at this point. A virtual destructor has a slightly different meaning to an ordinary virtual function. Declaring a base class' destructor virtual effectively provides all derived classes with a destructor if they do not provide one themselves, and the correct size will always be passed to delete. Virtual destructors are not overridden, but a derived class can have its own destructor which will be called before its base's virtual destructor.

class Base {
  // ...
public:
  // ...
  virtual ~Base() {/* Optional additional clearing up */}
};

It is good practice to give all your classes destructors (even empty ones), and make them virtual if there is any possibility that other classes will be derived from them.

'Virtual constructors'

It is not possible to have a virtual constructor, but sometimes it would be nice to have one so that you can create a new object of the same type as another, even if the original is referred to by a pointer to its base class. This effect can be achieved by something like:

class Base {
  // ...
public:
  // ...
  virtual Base *_new(void) {return new Base;}
};
class Derived {
  // ...
public:
  // ...
  Base *_new(void) {return new Derived;}
};
// etc
void f(Base *p1)
{
  p2 = p1->_new();// If p1 actually 
// points to a Derived, a new 
// Derived will be created instead
// of a Base
  // ...
}

Multiple inheritance

Multiple inheritance is very simple in itself, but the ambiguities that can result cause complications. Consider a modified hierarchy of classes representing computers:

class ComputerWithVRam {/*...*/};
class AcornComputer {/*...*/};
class RiscPC :
  public ComputerWithVRam, public AcornComputer {
  // ...
};

Class RiscPC is derived from both ComputerWithVRam and AcornComputer. What if both of the base classes had a function int get_ram(void); which one is called by the statement rpc.get_vram() if rpc is a RiscPC object? In this case, the compiler would be unable to decide; neither of the base classes has priority and you would have to qualify the statement with rpc.ComputerWithVRam::get_vram() or rpc.AcornComputer::get_vram().

In some cases, the compiler can make a sensible decision for you; the full rules are beyond the scope of this article, but they should not cause any unpleasant surprises.

It would also be reasonable, in this example, that ComputerWithVRam and AcornComputer would both be derived from a class Computer. In this case, a RiscPC would include two copies of Computer and you would have to qualify all statements involving Computer's methods. In practice, you would avoid wastefully duplicating data in that way unless it was necessary to have two or more similar base sub-objects (e.g. having an object on more than one linked list).

Virtual base classes

It is not possible, in the general case, to ensure that a derived class only has one copy of an indirect base class, but it is possible to make a number of derived classes all share one base sub-object. This is done by using a virtual base class. All derived classes that declare the base virtual share the same copy of that sub-object. If the base has a constructor, it is called once when its first derived object is constructed.

class Screen {
  // ...
public:
  void draw(void);
  // ...
};
class ScreenWithBorder : virtual public Screen {
  // ...
public:
  void draw(void);
  // ...
};
class ScreenWithStatusBar : virtual public Screen {
  // ...
public:
  void draw(void);
  // ...
};

All ScreenWithBorder and ScreenWithStatusBar objects will share the same Screen sub-object; this is a good example, because a computer (usually) only has one screen.

There is one point to beware. Suppose the definitions of draw() in the derived classes are:

void ScreenWithBorder::draw()
{
  Screen::draw();
  // Draw border
}
void ScreenWithStatusBar::draw()
{
  Screen::draw();
  // Draw status bar
}

and you also define:

class ScreenWithBorderAndBar :
  public ScreenWithBorder, public ScreenWithStatusBar
{
 // ...
public:
  void draw(void);
};
void ScreenWithBorderAndBar::draw()
{
  ScreenWithBorder::draw();
  ScreenWithStatusBar::draw();
}

The last function causes Screen::draw() to be called twice. This is, at best, inefficient, and it could cause corruption of the screen. To avoid this, you would have to give each derived class two drawing functions, one to just draw what is specific to each class and one to draw that plus what is specific to each base class. I'll leave that for you to work out.

Conclusion

Derived classes are implemented in C++ quite simply, but they give rise to subtle effects throughout the language, particularly in the ability of using a pointer to a derived class where a pointer to its base class is expected.

Unfortunately, I lack the time and experience to provide a tutorial on the wonderful uses (and abuses) to which you can put C++'s powerful OOP concepts, but I hope that you are finding this series useful for learning syntax and implementation details.


Contents - The Archives - Archive Articles