CS202 Programming

Systems

 

Lecture Notes #5

 

 

 

Object Oriented Programming

using

Single Inheritance

 

 

 

Lecture and Reference


 

So far we have used classes and objects to represent generalized abstractions. We learned how to enable these abstractions to be used in the same contexts as built-in types. We learned what design tradeoffs to make to keep our abstractions as efficient as possible. But, even though we were using objects, we were not using object-oriented programming. We were simply one step closer by understanding the syntax of classes and objects. Our abstractions were limited to stand alone classes.

In the object-oriented programming paradigm, we begin to consider using classes in conjunction with one another. We should no longer think about classes, or objects, in isolation from one another. Instead of simply creating user defined data types, we create a hierarchy of related and interdependent classes and objects, following the natural structure of the problem. This is because object-oriented programming extends the concept of data abstraction to apply across abstractions.

Object-oriented programming involves a natural way of thinking about solutions. We organize information in ways that fit an application as it exists in the real world. Unlike procedural abstraction, where we focus on what actions take place (i.e., verbs), in object-oriented programming we focus on the component parts (i.e., nouns) and the relationships between these parts. This means we must think about creating solutions in an entirely new manner. We first consider nouns, then verbs.  

Success of object-oriented programming is based on the development of standard and reusable classes. This means that we must construct software with a much broader perspective so that it is easily reusable. This means not only developing for today's problems but considering future trends and understanding what features have crept into our systems that are unique to the current application but which would limit its general acceptance or be extra "baggage" for future applications.

The concepts of object-oriented decomposition and iterative design and implementation are especially useful when designing complex software systems. When a complex software system is designed, it is decomposed into smaller and smaller parts or components. This means that instead of having to understand the entire system all at once, a given level of a system can be examined at any one time. Only a few parts at a time need to be fully understood rather than all of the parts at once. Decomposition promotes smaller systems since pieces can be reused from previous implementations. It also means that programs can be designed to evolve over time.

Of course, there is no right answer or perfect way to decompose our problems. What makes it work is whether or not the result produces code that works well in our current application and adapts to future applications in the same environment. In the long run, object-oriented programming should allow us to simplify our development tasks by reducing the amount of new code needed to solve problems. Our goal should be to maximize reusability without  sacrificing readability or efficiency.

 

 

Inheritance Hierarchies and Dynamic Binding

Object-oriented solutions are designed based on an inheritance hierarchy which defines the relationships between classes, where one class shares the structure and/or behavior of one or more classes. For example, a manager or contractor class may share the structure and behavior of an employee; each of these is a kind of employee. The employee represents a generalized abstraction and the manager and contractor represent specialized versions. To provide this type of design requires that we understand how to implement such relationships between objects. Therefore, our first step in understanding object-oriented programming is to learn the syntax and semantics behind inheritance hierarchies. We will also learn how to extend abstractions with new functionality even when the code for those abstractions is not available.

With inheritance hierarchies, classes are related because of common attributes. It is often the case that each class in a hierarchy has functions of the same name that perform similar operations, only specialized for that particular class. The decision about which functions are invoked for an object in this hierarchy can be made at either compile time or postponed until run time. Deciding at compile time which function is invoked is called static binding. Deferring this decision until run time is called dynamic binding or pure polymorphism. Dynamic binding can only be used when we have an inheritance hierarchy. Therefore, once we have learned how to create inheritance hierarchies, we will learn how to support pure polymorphism in Lecture 13.

In Lecture 13 will see that the advantage of dynamic binding is that we can write our client programs in a way that allows for the later addition of new classes with little or no modification. This process is dependent upon the design of our classes and occurs at run time. As such, we will learn how to design our classes to allow for the ultimate in code reuse.

 

 

Some Thoughts about Object-Oriented Analysis

The first step in object-oriented programming involves analyzing our problem and our problem's environment.  During analysis our goal is to model the environment by identifying classes, including the operations that can be performed on objects. The goal of this process is to describe all of the mechanisms and abstractions that make our problem behave in the proper manner. If we leave something out (i.e., leave out some abstraction or some operation), then we must use an iterative process of analyzing our problem so that our design can be as complete as possible. This approach is called incremental design.

For the most part, this process makes sense. But, what sort of things should we be looking for when finding classes and objects? We look for similarities in behavior or structure. But, don't stop there. There may be similarities that are based on other factors. In fact, the organization of our classes will vary depending on the problem being solved. Therefore, take a look at the application. The first step should be to list the types of things that should be considered when checking for likeness, such as classifying by the look, shape, feel, interactions, roles, organization, physical objects, events, etc. Make sure that the choices picked follow the natural structure of the problem's environment.          


Inheritance Hierarchy:       A hierarchical relationship between classes.

Programming in the object-oriented paradigm requires that we create hierarchically based solutions, where one class is based on another. In C++ this can be done by creating an inheritance hierarchy. With inheritance, we can create relationships between classes in an organized and structured fashion. Generic classes can be created and then expanded upon by adding attributes and by adding and modifying behavior. This allows us to define hierarchically based solutions that adhere to the natural structure of the problem. It also provides a means to extend and modify a class without mandating that clients of that class change existing code.

Inheritance hierarchies provide a means for developing extensible and flexible designs from which we can reuse all or portions of existing classes, without unwarranted side effects. This can be done even when the class' implementation is not available or when we want to maintain existing functionality and, at the same time, provide new features in an upward compatible way.  Using inheritance, we can use data members and member functions from existing classes as the basis from which to start, reducing the need to reinvent.

How do we specify relationships between classes? Our first step is to become familiar with how to create an inheritance hierarchy and then use it to our benefit. The following sections introduce the terminology commonly used with inheritance hierarchies and discuss the objectives for creating  them. Later in this lecture we will learn how to create an inheritance hierarchy and apply what we already know about classes to it.

 

5.1 Terminology

Base Class:                  The "parent" class on which other classes are based or derived from. Also known as the direct base class.

Derived Class:             The "child" class that is based on or derived from another "parent" or base class.

Indirect Base Class:    The "ancestor" base class, but not a direct parent.

By defining a class that is based on another class, using inheritance, one class is a specialization of another. Such a class is said to be a derived class. The class it is derived from is a base class. The derived class inherits the base class' members. The benefit of this type of relationship is that it allows reuse of existing code from the base class and allows us to focus on the new or specialized behavior in the derived class. An existing client should not be aware that a new derived class has been created if the specialized relationship is properly defined and encapsulated.

Another way to think of inheritance is as a hierarchy, as shown in Figure 5-1. Every hierarchy has a root (e.g., base class) which has zero or more children. Each child (e.g., derived class) is either a leaf or branches into children of its own. Each class is inherently related to its parent, as well as to its ancestors. In C++, the root of each hierarchy or sub-hierarchy is called a base class. If the base class is the parent of the class in question, then it is a direct base class. Otherwise, if it is an ancestor, then it is an indirect base class. The children classes are derived from base classes and are commonly called derived classes. Derived classes inherit all of the attributes and behavior (i.e., members) of their base classes and can extend the functionality of those classes by providing more specialized attributes and behavior not inherited from the base class. Derived classes can also alter their inherited attributes or behavior by providing members of the same name as those in a base class, thus hiding the base class members.

 

 

Figure 5-1: An Inheritance Hierarchy

 

In general, any class that we create that inherits the properties of another class is a derived class. This means that we will rarely be limited to only one base class. Instead, all ancestors are base classes! And, all descendants are derived classes.   

With object-oriented programming our solutions are designed by decomposing the problem into objects that are grouped hierarchically.

 

5.2 Objectives

Because derived classes inherit the members of the base classes, one class' design can be based on existing members from another class. Think of this as using building blocks. Instead of starting from scratch with each class that we design, we are able to extend one class from one another, reusing an existing class and reducing the need to reinvent. With derived classes, we can actually extend the base class' functionality. New member functions can be added without modifying the base class itself.  This means that a derived class can add to the capabilities of the base class by adding additional data members and member functions. Additionally, a derived class can change the inherited base class client interface by specifying data members and member functions of the same name, hiding those inherited from the direct or indirect base classes.

So, what do these classes usually represent? Base classes are typically used to establish the common attributes and behavior for a particular problem or application. A derived class may then be used to refine and add to that of the base class. A base class contains the data and operations common to all classes derived from it. Classes derived from a base class represent specialized versions, with new or altered data and operations. The relationship between a derived class and its base class is often called an "is a" relationship. This is because a derived class "is a" base class. A derived class is everything the base class is and more, because it has been extended or specialized. Because of this, a derived class object can always be used when a base class object is needed.

 

 

5.3 Creating a Single Inheritance Hierarchy

With inheritance, we use the data members and member functions from an existing class as the basis from which to develop new classes. This minimizes reinventing and allows reuse of existing software.

When a class is derived from one base class, it is called single inheritance. Single inheritance is at the heart of object-oriented programming and allows us to use a particularly powerful form of polymorphism called dynamic binding. When a class is derived from more than one base class, it is called multiple inheritance. Multiple inheritance is covered in Lecture 12. Dynamic binding is covered in Lecture 13.

 

5.3.1 Single Inheritance

Single Inheritance:     A class hierarchy where each class has at most one direct base class.

Designs that result in each class having at most one parent form a single inheritance hierarchy. This is the most common and straightforward approach to using inheritance. Single inheritance is similar to a file system consisting of a root directory and sub-directories on a computer hard drive. We can have as many inheritance hierarchies as needed to solve our problem. That is similar to partitioning our hard drive to create multiple file systems.

Figure 5-2 illustrates a single inheritance hierarchy. The base class is account. All classes are derived from this class, either directly or indirectly. checking is also a base class of the student class, since student is derived from it. This makes account an indirect base class of student. Notice how single inheritance has a tree-like structure.

 

 

Figure 5-2: An account Class Single Inheritance Hierarchy

 

Both the checking and saving classes must specify in their definition that they are derived from the account class. This means that the account class is the parent of both checking and savings. Since both checking and savings have only that single parent, they form a single inheritance hierarchy. The same holds true for the student class. It must specify in its definition that it is derived from the checking class. The checking class is the parent of student.

 

5.3.2 The Syntax of Single Inheritance

We form an inheritance hierarchy by specifying the relationship between classes. We don't need to alter the base classes to specify which classes are derived from them. Instead, we specify in the derived class which class is to be its parent. It is this parent's members that are then inherited by the derived class.

To specify a derived class, we define the class as we learned but we also add the base class' name as part of the derived class' definition. The following illustrates the syntax for creating a single inheritance hierarchy using public derivation where derived is the name of the derived class and base is the name of the base class:

 

class derived : public base

{

  public:

    ...

};

 

Saying class derived : public base establishes a hierarchy and specifies that the derived class is derived from the base class. The keyword public specifies that all public members of the base class remain public in the derived class. This is called public derivation and is how we specify an "is a" relationship between two classes. In an "is a" relationship, we can always substitute a derived class object for a base class object because a derived class object is a base class object. As we will see in Lecture 12, there are other forms of derivation that do not represent an "is a" relationship.

 

5.3.3 Defining a Hierarchy

The following example defines a base class account and two derived classes: checking and savings.

 

//account.h (Ex1601)

 

//base class

class account {

  public:

    account();

    void statement();

  private:

    char name[32];  //account owner

    float balance;  //account balance

};

 

//checking class derived from account

class checking : public account {

  public:

    checking();

    float get_charges();

  private:

    float charges;  //charges for current month

};

 

//savings class derived from account

class savings : public account {

  public:

    savings();

    float get_interest();

  private:

    float interest; //interest for current month

};

 

Saying class checking : public account when defining the checking class indicates that checking is a derived class. The keyword public tells the compiler that all public members of the account class remain public in the checking class (i.e., public derivation is taking place). The name account tells the compiler that the checking class is derived from the account class. The account class is the direct base class for checking and savings.

Since checking and savings are derived from account, we have a hierarchical relationship. This means that objects of the checking and savings classes inherit all of the members of the account class. In addition, they contain the additional members defined by either the checking or savings class.

Since we have used public derivation, we have established an "is a" relationship. A checking class is an account class.  A savings class is an account class. Objects of the checking and savings classes contain all of the members of an account object and can be used where ever an account object can be used. This is illustrated in Figure 5-3.

 

 

 

savings object

 

checking object

 

checking::get_charges()

charges

 
 

 

 

 

 

 

 

 

 

 


Figure 5-3: Inheritance of Data Members and Member Functions

 

Both the checking class and the savings class inherit the statement, name, and balance members from account. Because the name and balance data members are private, they are not accessible within the checking or savings classes. However, both derived classes are able to use the public statement member function. All that has to be added is the specialized behavior for the checking and savings classes. In reality, there would be many functions that could be inherited and reused from such an account class.

 

5.3.4 Implementing a Hierarchy

The following example implements our account, checking, and savings classes. No new syntax is required. We simply implement the member functions declared in each class in the same way that we did previously. But, we only need to implement the account member functions once. They can then be used by both the checking and savings classes and clients of these classes as though there were implemented as part of those classes. The checking and savings classes only contain the data members and member functions necessary for their specialized behavior.

 

//account.cpp (Ex1601)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

//account class member functions

account::account() :

  balance(0) {

  strncpy(name, "none", 32);

  name[31] = '\0'; //force terminating nul

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

 

//checking class member functions

checking::checking() :

  charges(5) {

}

float checking::get_charges() {

  return (charges);

}

 

//savings class member functions

savings::savings() :

  interest(0) {

}

float savings::get_interest() {

  return (interest);

}

 

//main.cpp (Ex1601)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  account a;

  checking c;

  savings s;

 

  a.statement();

  cout <<endl;

 

  cout <<"Checking ";

  c.statement();

  cout <<"  charges = " <<c.get_charges() <<endl;

  cout << endl;

 

  cout <<"Savings ";

  s.statement();

  cout <<"  interest = " <<s.get_interest() <<endl;

  return (0);

}

 

This results in the following output:

 

Account Statement

  name = none

  balance = 0

 

Checking Account Statement

  name = none

  balance = 0

  charges = 5

 

Savings Account Statement

  name = none

  balance = 0

  interest = 0

 

In this implementation, we have included the checking and savings class definitions in the same header file as account. In a larger system, it is common to put each derived class into its own header file. The same thing applies to the implementation files; each derived class would have its own implementation file as well.

 

 

5.4 Member Access

Even though inheritance hierarchies allow derived classes to inherit members from their base classes, it does not mean that those members will be accessible within a derived class. This is because members within a hierarchy have their own visibility.

The following sections discuss visibility. We then look at what members are directly accessible (i.e., visible) to derived classes.

 

5.4.1 Public and Private Members

Public Members:        Visible to client applications and derived classes.

Private Members:       Not visible to either client applications or derived classes.

As we have seen, public members of a base class are visible and fully accessible by classes derived from them. And, data and member functions in the private section are only available to the class in which they are defined. They are not accessible to any other class (with the exception of friends). Derived classes do not have access to a base class' private data and member functions, even though they are inherited. Even though memory is allocated for such data members, they may only be accessed from members within the base class itself (or friends). This is important because giving any other class (besides a friend) access to private information would compromise our ability to ensure data hiding and encapsulation. Such a compromise would decrease the value of programming with objects.

Previously, we recommended that data members be specified in the private section. By following this guideline when designing hierarchies, all derived classes would explicitly need to use the base class' public member functions to access inherited data. This isn't practical when building classes that are intended to work in harmony with one another. And, it reduces our ability to extend the functionality of a given class in the future. Direct access to those members is sometimes necessary. The next section discusses how to allow derived classes to have access to base class members while restricting access by client applications.  This is accomplished by declaring members as protected members.

 

5.4.2 Protected Members

Protected Members:      Visible to derived classes, but not visible to client applications.

Information that is hidden from the client can be made available for access by a derived class by specifying the members as protected instead of private. Data and operations that are protected are visible only to the class itself and derived classes. Whenever we design classes from which other classes may be derived, we should consider placing members in the protected rather than the private section. This keeps the data hidden from client applications, but allows for future reuse and extendibility by derived classes. Derived classes not only have access to the base class' public members but they also have access to its protected members. Clients of a class only have access to public members; they are prohibited from accessing the private or protected members.

Practical Rule:  If we want base class members to be made available to derived classes, but hidden from client applications, then declare those members in the protected section!

 

5.4.3 Member Access Summary

Members may be defined in the public, protected, or private sections. Public access makes members available for use by client applications and derived classes. Public members represent the interface presented to client applications. Protected access makes members available for use only by derived classes and friends of the class. Client applications cannot access protected members unless they are declared to be a friend. Public and protected members represent the interface presented to derived classes. Private access restricts access to the class and makes members available for use only within the class and by friends of the class. Client applications and derived classes cannot access private members unless they are declared to be a friend.

 

 

5.5 Constructors Within a Single Inheritance Hierarchy

We learned to define constructors to initialize data members defined within a class. This was rather straightforward. Now, with inheritance hierarchies, it becomes more involved. We need to know how constructors can be used within a hierarchy to ensure proper initialization.

The following sections introduce how a base class constructor is invoked through a hierarchy. We will examine how this is done implicitly, how it is done explicitly when arguments are required by the constructor, and in which order the base class and derived class constructors are invoked.

 

5.5.1 Base Classes With Default Constructors

A base class constructor is always invoked before a derived class constructor in an inheritance hierarchy. This means that a derived class' constructor can assume that the base class members have been initialized by the time it is executed. The body of a derived class constructor is executed last after the base class and all indirect base class constructors within the hierarchy have executed.

In our previous example, the base class contained a default constructor. This constructor initialized the data members whenever an object of this class was created. This means that when the client specifies the following:

 

account a;

a.statement();

 

This results in the following output:

 

Account Statement

  name = none

  balance = 0

 

But, when we have a derived class, we are not explicitly using the base class' constructor. In fact, if we look closely at our previous example, it appears that the derived class is somehow setting the default values of the data members of the base class. To explain this, we need to have a clear understanding of the relationship between an instance of a derived class and the base class itself. Look back at Figure 5-3. Each derived class contains (inherits) the data members of its base class.  It is not the derived class that sets the default values of the base class; rather, it is the default base class constructor that is implicitly invoked by the derived class constructor that initializes the base class members.

When the base class has a default constructor, it is automatically invoked when an object of the derived class is defined. This happens whether or not the derived class constructor is a default constructor or requires arguments. The following example illustrates this:

 

//account.h (Ex1602)

class account {

  public:

    account();

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking();

  private:

    float charges;

};

 

class savings : public account {

  public:

    savings();

  private:

    float interest;

};

 

//account.cpp (Ex1602)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account() :

  balance(0) {

  strncpy(name, "none", 32);

  name[31] = '\0';

  cout <<"account constructor called" <<endl;

}

 

checking::checking() :

  charges(5) {

  cout <<"checking constructor called" <<endl;

}

 

savings::savings() :

  interest(0) {

  cout <<"savings constructor called" <<endl;

}

 

//main.cpp (Ex1602)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  cout <<"Create checking object:" <<endl;

  checking c;

 

  cout <<"\nCreate savings object:" <<endl;

  savings s;

  return (0);

}

 

This results in the following output:

 

Create checking object:

account constructor called

checking constructor called

 

Create savings object:

account constructor called

savings constructor called

 

Supplying a default constructor in our base classes allows for the most straightforward class design. And, supplying a default constructor in a derived class makes it easier to use if classes are subsequently derived from it.

 

5.5.2 Base Class Constructors with Arguments

If a base class constructor expects an argument list, the derived class must explicitly specify the base class constructor's arguments. If it doesn't, then the base class is expected to have a default constructor, which is implicitly called. The way a derived class explicitly specifies the base class constructor's arguments is by listing the base class constructor in the derived class' initialization list along with the actual arguments expected by the base class constructor. For example, if a base class constructor expects two integers as arguments, the derived class constructor would resemble the following:

 

class derived : public base {

  public:

    derived(int i, int j) : base(i, j) {

      ...

    }

  ...

};

 

Of course, the derived class' constructor is not required to have the same arguments as the base class. But, because a derived class extends the behavior of the base class, it is usually the case that any arguments required by the base class constructor are also arguments expected by the derived class constructor. The arguments to the base class constructor can only consist of values supplied as arguments to the derived class constructor, constants, literals, global variables, or expressions made up from such values. We cannot use derived class data members as arguments to a base class constructor nor can we invoke a member function of the derived class and use its return value as one of the actual arguments because the derived class has not yet been initialized.

In the example below, we modify the account, checking, and saving class constructors to take arguments. We then modify the derived class constructors to specify the appropriate arguments for the base class constructor.

 

//account.h (Ex1603)

class account {

  public:

    account(const char* ="none", float=0);

    void statement();

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

    float get_charges();

  private:

    float charges;

};

 

class savings : public account {

  public:

    savings(const char* ="none", float=0);

    float get_interest();

  private:

    float interest;

};

 

//account.cpp (Ex1603)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b), //invoke base class constructor

  charges(c) {

}

float checking::get_charges() {

  return (charges);

}

 

savings::savings(const char* n, float b) :

  account(n, b), //invoke base class constructor

  interest(0) {

}

float savings::get_interest() {

  return (interest);

}

 

//main.cpp (Ex1603)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  checking c("Sue Smith", 1000.0);

  savings s("Jim Jones", 500.0);

 

  cout <<"Checking ";

  c.statement();

  cout <<"  charges = " <<c.get_charges() <<endl;

  cout << endl;

 

  cout <<"Savings ";

  s.statement();

  cout <<"  interest = " <<s.get_interest() <<endl;

  return (0);

}

 

This results in the following output:

 

Checking Account Statement

  name = Sue Smith

  balance = 1000

  charges = 5

 

Savings Account Statement

  name = Jim Jones

  balance = 500

  interest = 0

 

Notice the use of initialization lists in the derived class. The initialization list causes the base class constructor to be invoked with the correct arguments! The order of the arguments for the base class is very important. They must be listed in the same order as the base class constructor expects. If we had not included the base class constructor in the initialization list of the derived class, then the default base class constructor would be invoked. If no default base class constructor exists, then a compile error results.

Practical Rule: As we design our classes, if there is a chance that future classes will be derived from this class, we should consider defining default constructors. That way, a derived class can be defined that is not required to explicitly use the base class constructor.

 

5.5.3 The Timing of Constructor Invokation

In the previous example our initialization list consisted of base class constructors and initializers for derived class members. Given this, we might wonder when the constructors get invoked? Constructors are invoked in the order of the base class first, then derived class initializers, followed by the body of the derived class constructor. Figure 5-4 illustrates this for the savings class in the previous example.

 

 

Figure 5-4: Order of Constructor and Initializer Invokations

 

The placement of the base class constructor in the derived class' initialization list does not matter; it is always the first thing invoked even if it is listed after derived class data member initializers. And, remember from that the order of data members in an initialization list also does not matter; they are always initialized in the order in which they are defined within the class.

In summary, C++ respects its elders, since they come first! Another way to think of this is that we can't have a child before we have a parent!

 

 

5.6 Destructors Within a Single Inheritance Hierarchy

Destructors are invoked at the end of the lifetime of an object. Destructors are invoked in the opposite order from which their constructors are invoked. This means that the derived class destructor is invoked before its base class destructor. This is illustrated in the following example:

 

//account.h (Ex1604)

class account {

  public:

    account();

    ~account();

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking();

    ~checking();

  private:

    float charges;

};

 

class savings : public account {

  public:

    savings();

    ~savings();

  private:

    float interest;

};

 

//account.cpp (Ex1604)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account() :

  balance(0) {

  strncpy(name, "none", 32);

  name[31] = '\0';

  cout <<"account constructor called" <<endl;

}

account::~account() {

  cout <<"account destructor called" <<endl;

}

 

checking::checking() :

  charges(5) {

  cout <<"checking constructor called" <<endl;

}

checking::~checking() {

  cout <<"checking destructor called" <<endl;

}

 

savings::savings() :

  interest(0) {

  cout <<"savings constructor called" <<endl;

}

savings::~savings() {

  cout <<"savings destructor called" <<endl;

}

 

//main.cpp (Ex1604)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  cout <<"Create checking object:" <<endl;

  checking* pc = new checking;

 

  cout <<"\nCreate savings object:" <<endl;

  savings* ps = new savings;

 

  cout <<"\nDelete savings object:" <<endl;

  delete ps;

 

  cout <<"\nDelete checking object:" <<endl;

  delete pc;

  return (0);

}

 

This results in the following output:

 

Create checking object:

account constructor called

checking constructor called

 

Create savings object:

account constructor called

savings constructor called

 

Delete savings object:

savings destructor called

account destructor called

 

Delete checking object:

checking destructor called

account destructor called

 

 If there are indirect base classes, this sequence continues until the furthest base class destructor is invoked. A derived class destructor is guaranteed that its base class members are still available for use. Figure 5-5 illustrates the destruction process for the savings class in the previous example.

 

 

Figure 5-5: Order of Destructor Invokations

 

 

5.7 Extending the Behavior of a Class

We have seen how to create an inheritance hierarchy and how members of a base class are inherited. We have also seen how to implement constructors to properly initialize our inheritance hierarchies. Now it is time to focus on how we can extend or specialize the behavior of our derived classes by providing additional member functions and reusing the inherited behavior through these specialized functions.

In this section we look at member function hiding and member function overloading in conjunction with regular member functions, overloaded operators, and conversion functions.

 

5.7.1 Member Functions and Hiding

When we talk about inheritance, one of the first things that comes to mind is "what happens when members in our hierarchy have the same name"? Does overloading occur? The answer is no. Overloading means that we have unique signatures for the same named function within the same scope. This doesn't apply within a hierarchy because each class within an inheritance hierarchy has its own separate class scope. Therefore, overloading doesn't take place between classes. Instead, inheritance allows members in a base class to be hidden by members of the same name in a derived class. By hiding base class members the behavior of those functions can be redefined by the derived class without changing the base class or affecting existing client applications.

Members are hidden anytime we specify a data member or a member function in a derived class that has the same name as a member in a base class. A member function in a derived classes hides a member function in a base class even if the signatures are different. When that member is accessed, either from a client of the derived class or within the derived class itself, it is the derived member that is used.

In the following example, we hide the statement member function in the base class by providing an implementation of a statement member function specialized for each derived class. In order to implement statement in the derived classes, we need a means to access the name and balance members of the base class. This is accomplished by defining two access functions that return the values of those data members. In the next section, we will see how to implement statement in the derived classes without adding access functions to the base class. We can restrict base class client applications from using these access functions and make them available only to derived classes by declaring them to be protected members. In the following example, we see all three types of class access: public, protected, and private.

 

//account.h (Ex1605)

class account {

  public:

    account(const char* ="none", float=0);

    void statement();

  protected:

    const char* get_name();

    float get_balance();

  private:

    char name[32];

    float balance;

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

    void statement();

  private:

    float charges;

};

 

class savings : public account {

  public:

    savings(const char* ="none", float=0);

    void statement();

  private:

    float interest;

};

 

//account.cpp (Ex1605)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

const char* account::get_name() {

  return (name);

}

float account::get_balance() {

  return balance;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

void checking::statement() {

  cout <<"Checking Account Statement" <<endl;

  cout <<"  name = " <<get_name() <<endl;

  cout <<"  balance = " <<get_balance() <<endl;

  cout <<"  charges = " <<charges <<endl;

}

 

savings::savings(const char* n, float b) :

  account(n, b),

  interest(0) {

}

void savings::statement() {

  cout <<"Savings Account Statement" <<endl;

  cout <<"  name = " <<get_name() <<endl;

  cout <<"  balance = " <<get_balance() <<endl;

  cout <<"  interest = " <<interest <<endl;

}

 

//main.cpp (Ex1605)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  checking c("Sue Smith", 1000.0);

  savings s("Jim Jones", 500.0);

 

  c.statement();

  cout << endl;

 

  s.statement();

  return (0);

}

 

This results in the following output:

 

Checking Account Statement

  name = Sue Smith

  balance = 1000

  charges = 5

 

Savings Account Statement

  name = Jim Jones

  balance = 500

  interest = 0

 

When statement is called using an object of type checking, it is the derived class member function that is invoked and not the base class member function. The base class member function is hidden. This applies regardless of the argument list or return type. If the derived class member function required arguments, then clients must specify those arguments when calling the function. The name that is in scope is the name declared in the derived class. It is this member function that is used. Of course, a function may be overloaded within the derived class itself, allowing clients to select from a variety of overloaded functions within that scope. If the argument list used by the client does not match any of the functions defined within the derived class, a compile error will occur even if a base class has a matching function. Remember, this is because hiding is not function overloading!

Notice how hiding has allowed us to move details out of the application program and into the classes where they belong. All the application has to do is invoke the statement member function for the object it is interested in.  The object takes care of the rest!

Hiding applies the same for data members as it does for member functions. Any base class data members that are public or protected are accessible by the derived class. If the derived class defines data members of the same name (even though the types may be different), any base class data members of that name are hidden. It is the derived class data member that is accessed and not the hidden base class member regardless of the data type.

We could have made the name and balance data members protected instead of providing the access functions get_name and get_balance. But, if we did that then any future changes to the representation of those members in the base class might impact derived classes. By keeping the representation of the data members private and providing access functions to that data, we are free to later change the representation of the account class data members without affecting classes derived from account.

 

 

5.7.2 Access to Hidden Member Functions

Even when members are hidden, they can still be used. If they are public or protected, they can be accessed from within a derived class member function by using the class name and the scope resolution operator. The base class' statement function can be accessed by saying account::statement(). If the hidden members are public, they can be accessed from within a client application by saying object.account::statement().

This gives us a means to reuse base class functionality in the implementation of our derived classes. A derived class member function can call a hidden base class member function by using the scope resolution operator (::). By calling the base class member function from within the derived class member function, we can reuse the base class behavior and focus on implementing the specialized behavior in our derived class member function. This also eliminates the need to provide access functions in the base class like we did in the previous example.

The following example implements the statement functions in the checking and savings classes by reusing the base class implementation of statement. Notice that this has eliminated the need for the functions to access the name and balance members in the account class.

 

//account.h (Ex1606)

class account {

  public:

    account(const char* ="none", float=0);

    void statement();

  private:

    char name[32];  //account owner

    float balance;  //account balance

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

    void statement();

  private:

    float charges;  //charges for current month

};

 

class savings : public account {

  public:

    savings(const char* ="none", float=0);

    void statement();

  private:

    float interest; //interest for current month

};

 

//account.cpp (Ex1606)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  strncpy(name, n, 32);

  name[31] = '\0';

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

void checking::statement() {

  cout <<"Checking ";

  account::statement(); //reuse base class function

  cout <<"  charges = " <<charges <<endl;

}

 

savings::savings(const char* n, float b) :

  account(n, b),

  interest(0) {

}

void savings::statement() {

  cout <<"Savings ";

  account::statement(); //reuse base class function

  cout <<"  interest = " <<interest <<endl;

}

 

//main.cpp (Ex1606)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  checking c("Sue Smith", 1000.0);

  savings s("Jim Jones", 500.0);

 

  c.statement();

  cout << endl;

 

  s.statement();

  return (0);

}

 

This results in the following output:

 

Checking Account Statement

  name = Sue Smith

  balance = 1000

  charges = 5

 

Savings Account Statement

  name = Jim Jones

  balance = 500

  interest = 0

 

This example implemented the statement functions in the checking and savings classes by reusing the base class implementation of statement. The derived class member functions only had to add their specialized behavior. This implementation also eliminated the need for the account access functions get_name and get_balance.

 

5.7.3 Overloaded Functions and Overloaded Operators

Overloaded functions and overloaded operators are inherited in the same manner as any other member function defined within the base class. If a derived class has the same named function or operator as in the base class, then the base class overloaded function or overloaded operator is hidden even if the signatures differ.

There are several functions and overloaded operators that are not inherited by a derived classes. This includes the constructor, destructor, copy constructor, assignment operator, address-of operator, and comma operator.  Since these functions and operators are implicitly provided for each class by the compiler, these base class functions and overloaded operators are therefore hidden and are not directly accessible within the derived class or through an object of the derived class. Of course, they can always explicitly be accessed by naming the base class and using the scope resolution operator.

Because of this, special attention is required when implementing the copy constructor and assignment operator for a derived class. Implementing the copy constructor and assignment operator is discussed in Section 5.8.

 

 

 

5.7.4 Using Declarations

When a derived class member function or overloaded operator hides one or more direct or indirect base class members that have different signatures, those hidden members can be made accessible to clients of the derived class. The is done with a using declaration. A using declaration can bring any hidden public or protected base class member into the scope of the derived class. These members act as overloaded members of the derived class. However, such members remain hidden in situations where the argument list is the same as the same named member in the derived class.

In the following example, the member function fun is overloaded in the base class twice, once with an int argument and again with a double argument. Both functions are hidden in the derived class by the same named member that has a char* argument. The using declaration in the derived class makes the function taking the double accessible to clients of the derived class. But, the function taking the int is still hidden in the derived class by declaring a derived class member function that has the same signature.

 

//main.cpp (Ex1607)

#include <iostream>

using namespace std;

 

class base {

  public:

    void fun(int) {

      cout <<"base::fun(int)" <<endl;

    }

    void fun(double) {

      cout <<"base::fun(double)" <<endl;

    }

};

 

class derived : public base {

  public:

    using base::fun;  //fun(int) & fun(double) now in scope

    void fun(int) {   //hides fun(int) brought into scope

      cout <<"derived::fun(int)" <<endl;

    }

    void fun(char*) { //defines new fun(char*)

      cout <<"derived::fun(char*)" <<endl;

    }

};

 

int main () {

  derived d;

  d.fun(42);          //from derived

  d.fun(42.0);        //from base

  d.fun("string");    //from derived

  return (0);

}

 

This results in the following output:

 

derived::fun(int)

base::fun(double)

derived::fun(char*)

 

 

5.7.5 Friends

Friends declared within a base class are not inherited by a derived class. A parent's friend is not a child's friend unless the child explicitly makes the parent's friend their friend as well. If we want a derived class to also be a friend of the same function or class as its base class, then it needs to explicitly be declared within the derived class.

But, what about friends of our derived classes? Can a friend of a derived class access members of a base class? Yes, but access is restricted in the same way that it is for the derived class. That is, only the public and protected members of the base class are accessible. Neither a friend of a derived class nor the derived class itself has access to the base class' private members.

If we want a derived class to be a friend of the base class, we can declare it as such within the base class. Doing so allows a specific derived class to have access to private members of the base class. This should be avoided since it violates encapsulation of the base class. It is better to give protected access to the members that the derived classes needs access to.

 

 

5.8 Copy Constructor and Assignment Operator

Neither the copy constructor nor the assignment operator are inherited. This is because the compiler provides an implicit copy constructor and an implicit assignment operator for each class we create that hides the base class copy constructor and assignment operator.

We learned that the compiler supplied copy constructor and assignment operator are not sufficient when we have dynamically allocated resources, such as memory, in our classes. In such cases, we must implement our own. In order to properly implement a copy constructor and an assignment operator in a derived class, we must first understand how the implicitly defined copy constructor and assignment operator work. Then we will be prepared to understand the issues involved in implementing our own within a hierarchy.

 

5.8.1 Implicitly Defined

The implicitly defined copy constructor and assignment operator perform a memberwise copy operation for the members of their derived class. The copy constructor creates a new object from an existing object by performing a memberwise copy of each data member. The assignment operator copies an existing object into another object by performing a memberwise copy and overwriting the data members in the object being assigned

For a derived class, the implicitly supplied copy constructor and assignment operator both implicitly call the base class copy constructor and assignment operators before performing their memberwise copy operations. This ensures that the base class portion of the derived class is properly created or initialized before the derived class portion. The base class copy constructor and assignment operator can be either implicitly defined or explicitly implemented as part of the base class.  As long as they are accessible (i.e., public or protected), an implicitly defined copy constructor and assignment operator in the derived class will ensure that they are called.

 

5.8.2 Explicitly Defined

When we explicitly define either a copy constructor or an assignment operator in a derived class, we are responsible for ensuring that the base class copy constructor and assignment operator are called. This is because when we implement our own copy constructor or assignment operator, the compiler no longer provides an implicit one and cannot guarantee that the base class copy constructor or assignment operator are called.

This is a simple process for the copy constructor. We specify the base class' copy constructor in the initialization list of the derived class' copy constructor. For the assignment operator, this is not as simple. We must invoke the base class' assignment operator from within the body of our derived class' assignment operator. To do so requires that we cast the current object (accessible by *this) into a base class object as follows:

 

static_cast<base_class &>(*this) = operand;

 

To demonstrate this, we have modified the previous example to allocate memory dynamically. Looking back, the name member in our account class was a statically allocated array. This limits the length of the name that can be associated with an account and is wasteful of memory when we have short names. A more efficient solution is to dynamically allocate the memory to hold the name when an account object is created. Once we do that, we must then implement a copy constructor and an assignment operator for the account class.

To fully illustrate the implementation of a copy constructor and an assignment operator, we will extend our account class hierarchy by deriving another class from the checking class. We will call this class student. The student class represents a checking account for students. The only difference from a regular checking account is that no charges are assessed against the checking account and the name of the school must appear on the statements. This is illustrated in Figure 5-6.

 

 

Figure 5-6: Inherited Members for student Class

 

In our implementation of the student class, we dynamically allocate the memory to hold the name of the school. This requires that we also implement a copy constructor and an assignment operator for the student class to manage the dynamically allocated memory.

Because we are implementing our own copy constructor and assignment operator for the student class, we must explicitly call the checking class copy constructor and assignment operator. Even though the checking class copy constructor and assignment operator are implicitly defined, we are allowed to explicitly call them. And, even though the account class copy constructor and assignment operator are explicitly defined, the implicitly defined checking class copy constructor and assignment operator are guaranteed to call them.

We call the checking class copy constructor from the student class copy constructor initialization list as follows:

 

student::student(const student &s) :

checking(s) {

  ...

}

 

This works because a student object is a checking object. We can use a student object anywhere we need a checking object. The only member function of checking that matches this signature is the implicitly defined copy constructor. That is what is called passing the checking part of the student object as its argument.

We call the checking class assignment operator from the body of the student class assignment operator as follows:

 

student &student::operator=(const student &s) {

  ...

  static_cast<checking &>(*this) = s;

  ...

  }

 

This works because we are using the static_cast operator to force the type of the student object to be a reference to a checking object. This causes the overloaded checking assignment operator to be called for the object we are assigning to. We then assign the student object to this reference. Because a student object is a checking object, the overloaded checking assignment operator is called with the checking part of the student object.

If we didn't use a cast to change the type of the object we are assigning to, we would then have a recursive call to the overloaded student assignment operator!

 

 

5.8.3 Example of Derived Class Copy and Assignment

The following example is a complete implementation of the hierarchy illustrated in Figure 5-6. We have put output statements in the copy constructors and assignment operators to make it clear when they are being called.

 

//account.h (Ex1609)

class account {

  public:

    account(const char* ="none", float=0);

    account(const account &);

    ~account();

    account &operator=(const account &);

    void statement();

  private:

    char* name;     //account owner

    float balance;  //account balance

};

 

class checking : public account {

  public:

    checking(const char* ="none", float=0, float=5);

    void statement();

  private:

    float charges;  //charges for current month

};

 

class student : public checking {

  public:

    student(const char* ="none", float=0, const char* ="");

    student(const student &);

    ~student();

    student &operator=(const student &);

    void statement();

  private:

    char* school;   //student's school

};

 

//account.cpp (Ex1609)

#include <iostream>

#include <cstring>

using namespace std;

#include "account.h"

 

account::account(const char* n, float b) :

  balance(b) {

  name = new char[strlen(n) + 1];

  strcpy(name, n);

}

account::account(const account &a) {

  balance = a.balance;

  name = new char[strlen(a.name) + 1];

  strcpy(name, a.name);

  cout <<"account copy constructor called" <<endl;

}

account::~account() {

  delete[] name;

}

account &account::operator=(const account &a) {

  if (this != &a) {

    balance = a.balance;

    delete[] name;

    name = new char[strlen(a.name) + 1];

    strcpy(name, a.name);

  }

  cout <<"account assignment op called" <<endl;

  return(*this);

}

void account::statement() {

  cout <<"Account Statement" <<endl;

  cout <<"  name = " <<name <<endl;

  cout <<"  balance = " <<balance <<endl;

}

 

checking::checking(const char* n, float b, float c) :

  account(n, b),

  charges(c) {

}

void checking::statement() {

  cout <<"Checking ";

  account::statement();

  cout <<"  charges = " <<charges <<endl;

}

 

student::student(const char* n, float b, const char* s) :

  checking(n, b, 0) {

  school = new char[strlen(s) + 1];

  strcpy(school, s);

}

student::student(const student &s) :

  checking(s) {                           //call copy ctor

  school = new char[strlen(s.school) + 1];

  strcpy(school, s.school);

  cout <<"student copy constructor called" <<endl;

}

student::~student() {

  delete[] school;

}

student &student::operator=(const student &s) {

  if (this != &s) {

    static_cast<checking &>(*this) = s; //call assign op

    delete[] school;

    school = new char[strlen(s.school) + 1];

    strcpy(school, s.school);

  }

  cout <<"student assignment op called" <<endl;

  return(*this);

}

void student::statement() {

  cout <<"Student ";

  checking::statement();

  cout <<"  school = " <<school <<endl;

}

 

//main.cpp (Ex1609)

#include <iostream>

using namespace std;

#include "account.h"

 

int main() {

  student s("Kyle Reed", 5000, "UT");

  s.statement();

  cout <<endl;

 

  cout <<"student clone(s);" <<endl;

  student clone(s);

  cout << endl;

 

  clone.statement();

  cout << endl;

 

  cout <<"clone = s;" <<endl;

  clone = s;

  cout << endl;

 

  clone.statement();

  return (0);

}

 

This results in the following output:

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

student clone(s);

account copy constructor called

student copy constructor called

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

clone = s;

account assignment op called

student assignment op called

 

Student Checking Account Statement

  name = Kyle Reed

  balance = 5000

  charges = 0

  school = UT

 

If we had not called the checking class' copy constructor and assignment operator from the student class copy constructor and assignment operator, we would correctly copy the student part, but not the checking and account parts. Pay close attention when implementing derived class copy constructors and assignment operators. Bugs introduced here are very difficult to find later.