The Idea of Inheritance

Different things often have something in common. Buses and cars are all four-wheeled vehicles, a goat and a sheep are both plant-eating domesticated animals, and so on. In object-oriented programming things are modeled using classes; inheritance is a powerful way to express the common properties of classes.

Inheritance, as you will see, will also save you a lot of typing. A class can inherit all the properties of another class, plus some extra functionality. So it is useful for customizing existing classes.

Extending a struct

Say you want to define an Employee class. In Chapter 7 you created quite a lot of code that uses the Person class, and so it seems reasonable just to copy the fields of Person and add some extra fields to create the Employee class: the date of employment and the employee ID number. The following is an Employee class that is mostly a copy of Person:

struct Employee {
    string m_name;
    long   m_phone;
    string m_address;
    long   m_id;
    //
    Date   m_employed;
    Long   m_num;

    string name()  {  return m_name; }
    long   phone() {  return m_phone; }
    Date   employed() {  return m_employed; }
};

NOTE

Remember that there is no essential difference between struct and class.


Making up new classes like this can get ugly: Imagine if Person were a fully-fledged class with dozens of methods that would all have to be copied. Generally, copy-and-paste programming is not a good idea; it is wasteful and confusing. A common nightmare for maintainers is having sections of code that are almost identical, which happens when code is copied and then modified. Anytime you had new ideas about a Person class, you would again have to copy code. For example, I've obviously left out an email address in both my definition of Person and Employee; both classes would have to be changed.

Also, if there were a whole library of classes and functions that work with Person objects, would you also want to copy them? For instance, say there is a function print(Person *) that prints out a Person class, given a pointer. The following hack actually works:

Person walker;
print ( &walker );
Employee janitor;
print((Person *) &janitor);

You know that Person and Employee are practically identical, up to the fourth field; therefore, it is possible to pretend that an Employee class is a Person class. However, C++ does not automatically convert between unrelated classes, so a typecast (Person *) is required.

I've called this a hack because it's quick, it works, and it's a recipe for future disaster. You can get away with using the code shown in the preceding example if the structures are fairly simple. But again, any small change to the first fields causes problems. (For instance, some other programmer adds an email address to Person.) Also, you will see later in this chapter how something as innocent as adding a method can change the layout of a class in memory. As a general rule, you should not make these kinds of detailed assumptions about how the fields of classes are laid out because they might work on one machine and fail utterly on another.

Another approach is to actually put a Person object inside the Employee class, as shown in the following code. You then have access to a bona fide Person object (such as print(&janitor.m_person)) whenever you need it. The name() and phone() methods of Employee are defined using the same methods of Person. (If methods have the same name, they can be distinguished by their full names: Employee::name(), Person::name(), and so on.)

struct Employee {
    Person m_person;
    Date   m_employed;
    long   m_num;

    string name() {  return m_person.name(); }
    long phone()  {  return m_person.phone(); }
    Date employed() {  return m_employed; }
};

This is a good solution that is commonly used. This structure does expose its innards in an unseemly way, however, in expressions such as janitor.m_ person. The need to properly encapsulate the classes' assumptions leads to many Employee methods merely calling Person methods. This extra typing is inevitably error prone; imagine if Person had dozens of methods like name() and phone().

The official C++ way of solving the problem is to derive the class Employee from the class Person; Person is said to be the base class of Employee. The base class follows the class name, separated by a colon (:)

struct Employee: Person {
    Date   m_employed;
    long   m_num;
};

After you have defined an Employee object, you can access fields such as m_name and m_id as if they were members of Employee. We say that Employee inherits m_name from Person, as well as name(). The ability to use the code of the parent class is called implementation inheritance. Figure 8.1 shows how the fields of Employee are laid out in memory: Employee automatically contains all fields from Person, plus two extra fields. The following example uses the UnderC #d command to show all fields of the object cleaner, showing that an Employee object contains everything from Person:

Figure 8.1. The memory layout of the Employee structure.



;> Employee cleaner;
;> cleaner.m_name = "Fred Bloggs";
;> cleaner.name();
(string) 'Fred Bloggs'
;> #d cleaner
(Date) m_employed = Date { }
(long int) m_num = 0
(string) m_address = ''
(long int) m_id = 0
(string) m_name = 'Fred Bloggs'
(long int) m_phone = 0

Employee as a Subset of Person

As with member functions, there is something of a stage magician's trick about inheritance; inheritance is very clever, and everyone applauds, but why go to so much trouble? In object-oriented programming, you try to model a problem so that the system reflects the world, by using objects, actions, and relationships. The classes in a program represent people, things, and abstractions. There is a very clear relationship between Employee and Person; every Employee object is a kind of Person object. In the following example, two functions have been declared, taking a pointer to Person and a reference to Person, respectively. C++ will happily allow you to pass an Employee object to these functions:

;> void print(Person *p);
 ;> bool read(Person& p);
 ;> Employee fred;
 ;> read(fred);
(bool) true
 ;> print(&fred);
						

Working the other way, from Person to Employee, does not happen automatically. It is true that every Employee object is a Person object, but not true that every Person object is an Employee object. In the following example, C++ requires a typecast (Employee *) to pass p to cust_id(). The answer returned by cust_id() is nonsense, for the simple reason that Person doesn't have an m_num field.

;> int cust_id(Employee *e)
;> {
							return e->m_num; }
;> cust_id(&cleaner);
(int) 0
;> Person p;
;> cust_id(&p);
CON 16:Could not match void cust_id(Person*);
0 parm
;> cust_id((Employee *)&p);
(int) 1701602080

You can build a chain of classes by using inheritance. For example, you can derive Manager from Employee. A Manager object has all the properties of an Employee object, plus a list of everyone managed and a salutation (Dr., Mr., Ms., and so forth).

struct Manager: Employee {
  list<Employee *> m_managed;
  string           m_salutation;
 };

Although by default Manager inherits name() from Person (as well as m_employed from Employee, and so on), it is possible for Manager to redefine name(). Assume that the company we're talking about is the old-fashioned kind where the directors are Mr. Smith, Dr. Lorenz, and so on. Their names must always be preceded by the salutation. In this code you must precisely specify Person::name(). If you left out the explicit Person scope, then the compiler would read name() as Manager::name(), which would result in an uncontrolled recursive call. Here is a definition of the Manager::name() method:

string Manager::name()
{
  return m_salutation + " " + Person::name();
}

In this case you can say m_name instead of Person::name(), but it is a good idea to let the base class manage its own data. If m_name were a private member of Person, you would not be able to use it directly anyway.

Inheritance makes it possible to use strong type checking for function arguments and yet flexibly pass such functions many different kinds of objects. Originally, when the code to print out the Person class was written, nobody was thinking of Employee objects. But because any Employee object in effect contains a Person object, you can reuse that printing code without forcing the type. Remember you had to use a typecast to pass a Person pointer as an Employee pointer: cust_id((Employee *) &p). (This is sometimes called type coercion, and it is similar to a child trying to get a square peg into a round hole. It might work, depending on the particular child and the particular hole.)

Inheritance creates a lineage of related classes, and the distance along that line is the number of generations between two classes. For example, Employee is closer to Person than Manager is to Person. This distance is used when resolving overloaded functions. Here there are two versions of print(); Manager * will match the second version because the argument type is closer.

void print(Person *p);   //(1)
void print(Employee *e); //(2)
Employee ee; Manager mm;
print(&mm);    // will match (2), not (1)

Either version of print() works on Manager *, but Employee * is closer to Manager * than Person * is. The second version of print() is clearly more specialized (and it may well be defined in terms of the first version).

Access Control and Derivation

Up to now I've used struct instead of class, because it is slightly simpler. The difference when using class is that all the data fields are private by default, and the only access to these is through get and set methods like employed():


class Employee: public Person {
    Date   m_employed;
    long   m_num;
  public:
    void employed(const Date& d) {  m_employed = d; }
    void number(long n)          {  m_num = n;      }
    Date employed()   {  return m_employed; }
    long number()     {  return m_num;      }
 };

typedef std::list<Employee *> EmployeeList;

class Manager: public Employee {
  EmployeeList m_managed;
  string       m_salutation;
 public:
  EmployeeList& managed()         {  return m_managed; }
  void salutation(string s)       {  m_salutation = s; }
  string name()
  {  return m_salutation + " " + Person::name(); }
 };

Anything private in Employee will not be available to Manager; methods of Manager cannot accesss m_num and must use number() instead. This makes it much easier to change code in Employee without having to worry about code in all of Employee's derived (or descendant) classes.

Making all the data private is often too restrictive; you don't want everyone outside to rely on the implementation details, but you can let derived classes have direct access with the protected access specifier. Here is a (nonsense) example:


class One {
private:
   int m_a;
protected:
   int m_b;
public:
   int m_c;
};

class Two: public One {
protected:
   int m_c;
public:
  int use_a() {  return m_a; }  // error: m_a is private to One
  int use_b() {  return m_b; }  // ok: m_b is protected
  int use_c() {  return m_c; }  // ok: _everyone_ can access m_c!
};
int use_One(One& o) {
  return o.m_b;        // error: m_b is protected
}

This is the C++ equivalent of the saying “blood is thicker than water”; it shows that relationship is privileged. Saying that One::m_b is protected means that any children of One have direct access to m_b. By saying that One::m_a is private, the writer of the class is saying that the implementation can change and that no derived class should rely on it. Note the public attribute in the declaration of Two; it shows that One is a public base class of Two. This is an extremely common situation. Two has free access to all public and protected members of One. (Sometimes you will see a private base class. This might seem like the programming equivalent of a black hole; but you can use classes with private base classes when you need full control of what appears as public.) From now on, this chapter explicitly indicates the access mode by using class instead of struct and by using public derivation.

Constructor Initialization Lists

Until this point, we have assumed that base classes have default constructors. Although there is no explicit constructor for Employee, the Date field might need to be constructed, and its base class Person certainly has a default constructor to make sure that the strings are properly initialized. In the section “Constructors and Destructors” in the last chapter you saw that such a constructor would be automatically supplied for Person. In this way, you can continue to pretend that complex objects such as strings really are simple C++ types.

In the same way, the compiler generates a default constructor for Employee using these constructors. It is helpful to think of the Person part of Employee as being a member object. If Employee had an explicit constructor, the compiler would quietly add code to that constructor to construct the base class, as well as any member objects. Therefore, the following would work well:

class Employee: public Person {
 ...
 public:
    Employee(string name, int id) {
      m_name = name;
      m_employ_num = id;
    }
 ...
 };

If the base class does not have a default constructor, then it must be called explicitly. Consider the case when Person::Person() takes a string argument intended to initialize m_name; the Employee constructor must call Person::Person(string name) explicitly in a constructor initialization list, which might also contain member initializations. This list follows a colon (:) after the constructor declaration and will contain any base class or member objects which need initialization. It is the only way you can pass arguments to the constructors of these objects. For example,

// have to do this with base classes
Employee(string name, int id) : Person(name)
{
  m_employ_num = id;
}
 // or ....
Employee(string name, int id)
: Person(name), m_employ_num(id) {  }

There are two other cases in which you need to use initialization lists. First, you use initialization lists if any member objects or base classes have no default constructors. If Employee does have a Person member, then Person needs to be constructed with an explicit name value. In this case, you can think of the base class as a member.

Second, you have to use initialization lists if any members are references. In both of these cases, you cannot have a declaration without an explicit initialization. For example:

struct Fred {
 Person m_person;
 int& m_accessed;

 Fred(string name, int& i)
   : m_person(name), m_accessed(i)
  {  }
  ...
};

Constants in Classes

An easy mistake to make is to try to initialize a member variable directly. This is, after all, how you usually initialize variables, and in fact Java allows this for class fields. The only place that standard C++ allows this kind of initialization is with constants, which must be declared static as well as const. But not every compiler manages this case properly, and you often see enumerations used for this purpose. In this example, you can see that generally initializations inside a struct are not allowed, except for constants. I have also shown an enum used to create constants. Both work fine (except on some older compilers like Microsoft C++ 6.0), and in both cases you access the constant with scope notation: F1::a, and so on. This is how constants like string::npos and ios::binary are implemented.

;> struct Test {
;1}  int a = 1;
CON 3:initialization not allowed here

;> struct F1 {
;1}  static const int a = 648;   // Microsoft C++ 6.0 complains
;1}  enum {  b = 2, c = 3 };     // always acceptable
;1}  }
							;
;> F1::a;
(const int) a = 648
;> F1::b;
(const int) b = 2

Using Classes as Exceptions

In Chapter 4, in the section “Defensive Programming,” when I discussed exception handling, I mentioned that throwing different types of exceptions in functions gives any caller of those functions great flexibility in handling those different types. A function may choose to ignore some types of exceptions and let them be caught elsewhere. For instance, in the reverse-Polish calculator case study in Chapter 4, “Programs and Libraries,” string exceptions are true errors and integer exceptions are used to signal that there is no more input; in the case of string exceptions, the message is printed out, but the loop is not terminated.

You can think of the catch block following a try block as a function that receives the thrown exception as its (optional) argument. A number of catch blocks are possible because the runtime system looks at these argument types in turn and decides which is the best match; this is a simple form of overloading. Classes make excellent types for exceptions because you can generate as many distinct, well-named types as you like. For example, the following is an example that defines an Exception type; it defines the single method what(), which returns some explanation of what error occurred (for example, ParmError is a kind of Exception error, and DivideByZero is a kind of ParmError error):


class Exception {
 string m_what;
public:
 Exception(string msg) : m_what(msg) { }
 string what() {  return m_what; }
};
class ParmError: public Exception {
public:
  ParmError(string msg) : Exception(msg) { }
};

class DivideByZero: public ParmError {
public:
  DivideByZero() : ParmError("divide by zero") { }
};

int divide(int i, int j)
{
  if (j == 0) throw DivideByZero();
  return i/j;
}

void call_div()
{
 int k;
 try {
   k = divide(10,0);
 }
 catch(DivideByZero&) {
   k = 0;
 }
 catch(ParmError& e) {
   cout << e.what() << endl;
 }
 catch(Exception&) {
   throw;  // re-raise the exception...
 }
}

This set of classes classifies run-time errors. All errors will be caught by Exception; all errors to do with bad parameters will be caught by ParmError; and only dividing by zero will cause DivideByZero. So the calling function can choose exactly how detailed its error handling should be. It could just catch Exception and display its what() value.

The important thing to realize about using exception hierarchies is that you need to have the catch blocks in the right order. It would be wrong to put catch(ParmError&) before catch(DivideByZero&) because DivideByZero is a kind of ParmError error and its catch block would catch the exception first. This is very different from how normal function overloading works, of course.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.45.5