Previous | Table of Contents | Next |
7.5.2.2. Virtual Destructors
Because virtual functions are useful only when called through pointers or references, they are most interesting when used in conjunction with dynamically allocated objects. When an object is dynamically allocated, it is usually dynamically freed as well, and just as it may be useful to ignore the specific type of an object when using it, it may also be useful to ignore the specific type when freeing its memory.
For example, consider a dynamically allocated Vehicle:
Vehicle* vp = new Helicopter(args);
We declared vp as a pointer to Vehicle because we did not want to have to remember that we were dealing with a Helicopter. However, when we no longer need this particular Helicopter, there is trouble:
delete vp; // Wrong!
The trouble is that we are trying to deallocate a Vehicle, but what we really have is a Helicopter.
In a case like this, we have to tell the compiler that when we delete something that is nominally a Vehicle, it might actually be an object of any type derived from Vehicle. The way to do this is to give class Vehicle a virtual destructor:
class Vehicle { public: virtual ~Vehicle() { } // ... };
Virtual destructors are usually empty, because they are typically nothing more than a signal to the compiler to do the right thing in the context of a delete.
7.5.2.3. Pure Virtual Functions
It is often useful to define a base class whose sole purpose is to be a base class; there will never be any objects of that class. So, for example, it is likely that we will never want to have an object that represents a vehicle without saying something about what kind of vehicle we have. In such a case, we have to define the virtual function in the base class, because it is part of the interface to the base class, but it is hard to know how to do so, because the virtual function is meaningful only for derived classes.
C++ offers us a way around this dilemma by letting us define a pure virtual function We do so by writing, for example:
class Vehicle { public: void start() = 0; // ... };
Here we have said that every Vehicle can be started somehow, but we dont know how until we know what kind of Vehicle we have.
A class that has one or more pure virtual functions is called an abstract base class. To ensure that these undefined functions are never called, the compiler prevents an object of an abstract base class from ever being created, except as part of an object of a derived class. Such derived classes must override, and define, every pure virtual function in the base class, or else they are considered abstract base classes themselves and cannot be used to create objects either.
Later, we will see an example of an abstract base class.
7.5.2.4. Dynamic Casts
Suppose we have a pointer to a Vehicle and we want to know whether the pointer actually points to a Helicopter (or some type derived from Helicopter). Why might we care? Usually the reason is that we wish to take some kind of action in the case of a Helicopter that is not appropriate for other kinds of Vehicle.
The most straightforward way to base an action on the specific type of an object is to define a virtual function in the base class, and then override it in the appropriate derived class with one that performs the appropriate action. However, this strategy is not always possible, because we do not always control the definition of our base classes.
To allow for type inquiry even for classes we did not define, C++ has a dynamic cast facility. Suppose, for example, that vp points to some kind of Vehicle. Then we might write:
Helicopter* hp = dynamic_cast<Helicopter*> (vp);
If vp actually did point to a Helicopter object or an object of a class derived from Helicopter, then hp will now point to the same object. Otherwise hp will be zero. A similar facility exists for references, but because a reference must always refer to an object, a dynamic cast on a reference will throw an exception if it fails.
There are other special-purpose casts available as well, but they are beyond the scope of this chapter.
7.5.2.5. Protection and Inheritance
When we have been defining our derived classes, we have always said things along the lines of
class D: public B { /* ... */ };
Here, the public means that it should be publicly known that D is derived from B. It is that fact that allows us to convert a D pointer into a B pointer.
We could also have said
class D: private B { /* ... */ };
or, equivalently,
class D: B { /* ... */ };
in which case B would be called a private base class of D. Private base classes are uncommon; conceptually they are classes where the inheritance is part of the implementation but not part of the interface.
In keeping with the open nature of classes derived with struct, public inheritance is the default there; so
struct D: B { /* ... */ };
is equivalent to
class D: public B { public: /* ... */ };
The presence of inheritance opens the possibility for one other kind of protection. In the absence of inheritance, an abstract data type has two aspects to its construction: those seen by its users and those seen as part of its implementation. With inheritance, however, comes a third possibility, namely members that derived classes see but general users do not.
To cater to inheritance this way, C++ has a third protection keyword, protected. If we write a class such as
class B { public: // ... protected: // ... private: // ... };
then anyone can access the members defined after the public label, only members and friends of B and members of classes derived from B can access members defined after the protected label, and only members and friends of B can access members defined after the private label.
Previous | Table of Contents | Next |