Specification | Implementation |
---|---|
class Account { public: Account(); float account_balance(); float withdraw( float ); void deposit( float ); private: float the_balance; }; |
Account::Account() { the_balance = 0.0; } float Account::account_balance() { return the_balance; } float Account::withdraw(float money) { if ( money <= the_balance ) { the_balance = the_balance-money; return money; } else { return 0; } } void Account::deposit( float money ) { the_balance = the_balance + money; } |
This chapter introduces a way of re-using existing classes. This is done via a mechanism which allows a new class to be created out of existing classes. This new class is said to be derived from the existing base classes.
Having already created a class to deal with a bank account Account, it is appropriate to think about how an interest-bearing account may be defined. This new account will pay interest on the current balance held in the account at the end of each day.
Rather than create a totally new class, the concept of inheritance
can be used. A class is declared which inherits all the data and
methods of Account, and adds new data items and methods
to facilitate the operation of an interest-bearing account.
An interest-bearing account is the same as an ordinary account
but includes ways of specifying the daily interest rate and the
actions of accumulating the daily interest and adding this to
the outstanding balance at the end of the accounting period.
In the example below, the default annual interest rate on the account is 10%, which on a daily basis represents 0.026116%.
The specification for this class is:
class Interest_Account : public Account { public: Interest_Account(); static void prelude( const float );// Set up interest rate void calc_interest(void); // calculate the interest void add_interest(void); // Add the interest to account protected: void accumulate_interest(const float); // total private: float the_accumulated_interest; // Interest gained static float the_interest_rate; // interest rate }; |
The role of a protected member of a class will be discussed fully later in this chapter in section 9.4. Essentially protected restricts access of the member function to only other member functions of the class.
The class Interest_Account has the following methods:
In the class Interest_Account | Inherited from Account |
---|---|
add_interest calc_interest Interest_Account accumulate_interest Account |
account_balance withdraw deposit prelude |
and data items:
In the class Interest_Account | Inherited from Account |
---|---|
the_accumulated_interest the_interest_rate |
the_balance |
As can be seen from the above new class Interest_Account this mechanism saves considerable time and effort, as code from a previously defined class can be re-used to make a new class.
However, this re-use is conditional on existing classes performing suitable operations.
Figure 9.1 illustrates the syntax for deriving a new class from a base class.
Figure 9.1 Interest_Account derived from Account.
Note: The scope modification to the members of the base class will be discussed in detail in section 9.4.1. In this case it makes all the non-private members of Account visible to an implementor and user of the class Interest_Account.
The implementation part of this new class is as follows:
const float DAILY_RATE = 0.00026116; // 10% float Interest_Account::the_interest_rate; // Declare storage Interest_Account::Interest_Account() { the_accumulated_interest = 0.0; } void Interest_Account::prelude( const float ir ) { the_interest_rate = ir; } |
The member function prelude is responsible for initializing the static member of the class. Remember, if a data member of a class is declared as static, then only one copy of this data item exists which is shared by all the instances of the class.
The member functions add_interest, accumulate_interest and calc_interest are responsible for processing the interest on the account.
At the end of each working day calc_interest will be called and will accumulate the interest in the member variable the_accumulated_interest. This will be deposited into the customer's account at the end of the accounting period using the function add_interest.
void Interest_Account::calc_interest(void) { accumulate_interest( account_balance() * the_interest_rate ); } |
void Interest_Account::accumulate_interest(const float ai ) { the_accumulated_interest += ai; } |
void Interest_Account::add_interest(void) { deposit( the_accumulated_interest ); the_accumulated_interest = 0.0; } |
Each instance of Interest_Account will contain the member variable the_balance and the_accumulated_interest.
The constructor for an object of type Interest_Account will use the constructor for Account to set up the initial balance in the account. This does not have to be explicitly called if the constructor for the base class has no parameters.
Interest_Account::Interest_Account() { the_accumulated_interest = 0.0; } |
Note: If the constructor for Account had a parameter, then the constructor for Interest_Account would have been specified in a slightly different way to allow a parameter to be passed to the constructor of Account.
The constructor in the base class is called first, followed by the constructor in the derived class. When the object's storage is released the destructor in the derived class will be called first, followed by the destructor in the base class.
The following code uses the class Interest_Account:
void process(void); void main() { const float DAILY_RATE = 0.00026116; // 10% Annual rate Interest_Account::prelude(DAILY_RATE); cout << setiosflags( ios::fixed ); cout << setiosflags( ios::showpoint ); cout << setprecision(2); process(); } |
The function process then tests the class Interest_Account:
void process() { Interest_Account mike; float obtained; mike.deposit(1000); cout << "mike Balance = " << mike.account_balance() << "\n"; obtained = mike.withdraw(200); cout << "mike has withdrawn " << obtained << "\n"; cout << "mike Balance = " << mike.account_balance() << "\n"; mike.deposit(50); cout << "mike Balance = " << mike.account_balance() << "\n"; mike.calc_interest(); mike.add_interest(); cout << "mike Balance = " << mike.account_balance() << "\n"; } |
and produces the following output:
mike Balance = 1000.00 mike has withdrawn 200.00 mike Balance = 800.00 mike Balance = 850.00 mike Balance = 850.22 |
Note: As the bank account is held as a floating point number, the actual amount held may well not be exact to two decimal places.
The class Interest_Account has the relationship 'is a' to the class Account. Hence an object of type Interest_Account may be used as if it were an object of type Account. Here the inheritance mechanism has been used to produce a specialization of Account.
A class which has been derived from a base class may itself be used as a base class to derive other classes. For example, the bank may wish to introduce a savings account with a tiered rate of interest. With this type of account the more money that is saved, the higher the interest paid on the account. The base interest rate offered, however, is the same as an Interest_Account.
Amount in account | Annual interest rate | Daily interest rate (365 days in year) |
---|---|---|
less than £10000 | 10% | 0.026116% |
£10000 - £24999.99 | 11% | 0.028596% |
£25000+ | 12% | 0.031054% |
The specification for the class would be:
class Special_Interest_Account : public Interest_Account { public: Special_Interest_Account(); void static prelude( const float, const float, const float ); void calc_interest(void); private: static float the_interest_rate1; static float the_interest_rate2; static float the_interest_rate3; }; |
while the implementation of the member functions would be:
const float DAILY_RATE_R1 = 0.00026116; // 10% const float DAILY_RATE_R2 = 0.00028596; // 11% const float DAILY_RATE_R3 = 0.00031054; // 12% float Special_Interest_Account::the_interest_rate1;// The storage float Special_Interest_Account::the_interest_rate2;// The storage float Special_Interest_Account::the_interest_rate3;// The storage Special_Interest_Account::Special_Interest_Account() {} void Special_Interest_Account::prelude (const float ir1, const float ir2, const float ir3) { the_interest_rate1 = ir1; the_interest_rate2 = ir2; the_interest_rate3 = ir3; } void Special_Interest_Account::calc_interest(void) { float money = account_balance(); if ( money < 10000 ) accumulate_interest( account_balance()*the_interest_rate1 ); else if ( money < 25000 ) accumulate_interest( account_balance()*the_interest_rate2 ); else accumulate_interest( account_balance()*the_interest_rate3 ); } |
The member function calc_interest overloads the function calc_interest in the class Interest_Account to provide the extra functionality. The effect of this is that an instance of the class Special_Interest_Account cannot be used where an object of type Interest_Account is required. Though the inheritance mechanism is used to produce a specialization of Interest_Account it does not have the relationship 'is a' to Interest_Account.
If the member function calc_interest had to call the member function calc_interest in the class Interest_Account then the scope resolution operator :: would be used as follows:
Interest_Account::calc_interest() |
to distinguish between the two functions calc_interest.
The following code uses the class Special_Interest_Account:
void process( void ); void main() { const float DAILY_RATE_R1 = 0.00026116; // 10% Annual rate const float DAILY_RATE_R2 = 0.00028596; // 11% Annual rate const float DAILY_RATE_R3 = 0.00031054; // 12% Annual rate Special_Interest_Account::prelude( DAILY_RATE_R1, DAILY_RATE_R2, DAILY_RATE_R3 ); process(); } void process() { Special_Interest_Account mike; float obtained; cout << setiosflags( ios::showpoint ); cout << setiosflags( ios::fixed ); cout << setprecision(2); cout << "mike Balance = " << mike.account_balance() << "\n"; mike.deposit(20000); cout << "mike Balance = " << mike.account_balance() << "\n"; obtained = mike.withdraw(2000); cout << "mike has withdrawn " << obtained << "\n"; cout << "mike Balance = " << mike.account_balance() << "\n"; mike.deposit(50); cout << "mike Balance = " << mike.account_balance() << "\n"; mike.calc_interest(); mike.add_interest(); cout << "mike Balance = " << mike.account_balance() << "\n"; } |
and produces the following output:
mike Balance = 0.00 mike Balance = 20000.00 mike has withdrawn 2000.00 mike Balance = 18000.00 mike Balance = 18050.00 mike Balance = 18055.16 |
Inheritance can cut down considerably the amount of code that has to be written for an application but, to use inheritance effectively, careful planning is required!
The visibility of a member of a class to the outside world, and to other classes which may inherit the class, depends on how it has been declared in the class. In essence there is a hierarchy of visibility.
class Account { public: protected: private: } |
The following table summarizes the visibility of class members. For each case, possible items that may be described in this way are suggested.
Member of class declared as | Visibility of class member | Examples of items which might be declared in this way |
---|---|---|
Public | To all items within scope of this class. | Functions visible to a user of the class. |
Protected | To another class member and any class member of a class which inherits this class publicly. | Functions which are used to build the functions visible to a user of the class and would be useful to a derived class. |
Private | Only to another class member. | Variables, and any functions which should not be used even by a derived class. |
Note: Items are by default considered private until a visibility label is met.
This visibility of base class members can be changed, by use of a scope modification when the derived class is declared. Shown below is the effect of scope modification on the visibility of base class members in a derived class.
If a new class Derived had been described as:
class Derived : public Base { |
then the access to the members of Base in the class Derived would be as follows:
If a new class Derived_2 had been described as:
class Derived_2 : private Base { |
then the access to the members of Base in the class Derived_2 would be as follows:
Note: It is good programming practice to restrict the scope or visibility of items as far as possible.
As seen previously a constructor for a class will be called, if one has been specified when an instance of that class is created.
A destructor may also be specified which will be called just before the instance of the class variable goes out of scope and any storage is released back to the system. An instance of a class will usually go out of scope when the block in which it has been declared is exited.
For example, the specification of a room might be as follows:
class Room { public: Room(const int); // Constructor ~Room(); // Destructor // Other member functions private: // member variables }; |
It might be used in a program as follows:
watts_building() { Room w422(50); // declare an instance of Room } |
The constructor for the class Room is called when the room w422 is declared; the constructor in this case takes a mandatory parameter that describes the number of square metres of office space.
The destructor ~Room is called when the block containing an instance of the class variable is exited.
This demonstration class describes a room in a building. The class
contains the size of the room in square metres. A member function
is provided to list this information.
The constructor initializes an instance of room with its size.
The destructor ~Room simply records the fact that the
instance of room has gone out of scope and has hence been destroyed.
The specification of the class is:
class Room { public: Room(const int); ~Room(); void size(void); // display size of room private: int the_size_sq_metres; // Size in square metres }; |
The implementation would be:
Room::Room(const int n) { the_size_sq_metres = n; cout << "Constructor Room"; cout << " size in square metres = "; cout << n << "\n"; } Room::~Room() { cout << "Destructor Room" << "\n"; } void Room::size(void) { cout << "Method Room::size()"; cout << " size in square metres = "; cout << the_size_sq_metres << "\n"; } |
The above class description could be used in a program which contains the following function.
void proc_room() { Room w422(50); w422.size(); } |
The output from the program when this function is called would be:
Constructor Room size in square metres = 50 Method Room::size() size in square metres = 50 Destructor Room |
This demonstration class is derived from the class Room and describes an office in a building. The class Office is initialized with the size of the office and the number of staff members who can be accommodated.
The specification for the class is:
class Office : public Room { public: Office( const int , const int ); ~Office(); void staff(void); // Display number of staff in room private: int the_no_staff; // The number of staff }; |
The implementation for the class would be:
Office::Office( const int size, const int no_staff) : Room(size) { the_no_staff = no_staff; cout << "Constructor Office"; cout << " the number of staff = " << the_no_staff << "\n"; } Office::~Office() { cout << "Destructor Office" << "\n"; } void Office::staff(void) { cout << "Method Office::staff()"; cout << " number of staff = " << the_no_staff << "\n"; } |
The constructor for Office cannot in this case implicitly call the constructor for Room, as the size of the room has to be specified as a parameter to the constructor. This is called explicitly as illustrated in Figure 9.2.
Figure 9.2 Call of a constructor explicitly in the base class.
The above class description could be used in a program which contains the following function.
void proc_office() { Office w418(24,2); w418.staff(); w418.size(); } |
The output from the program when this function was called would be as follows:
Constructor Room size in square metres = 24 Constructor Office the number of staff = 2 Method Office::staff() number of staff = 2 Method Room::size() size in square metres = 24 Destructor Office Destructor Room |
Note: The order in which the constructors and destructors are called for a class which has derived components from a base class.
So far a new class has been created for a single existing class, which itself may have been created from another class. In some situations, what is required is to create a new class based on two or more existing classes. This concept is often referred to as multiple inheritance.
As an example, consider the class Interest_Account which provides a class for representing an interest-bearing account. The name of the account holder is not specified as part of the account. A new class Named_Account can be created from the existing classes Interest_Account and Name_Address which will allow the following operations on an instance variable.
The specification for this class is:
class Named_Account:public Interest_Account, public Name_Address { public: Named_Account(const char name[] = "", const float = 0.0); void mini_statement() const; }; |
The member functions are:
From the class Interest_account |
From the class Name_Address |
In the class Named_Account |
---|---|---|
Account account_balance accumulate_interest add_interest calc_interest deposit Interest_Account prelude withdraw |
Name_Address print_address print_nameset |
Named_Account mini_statement |
The member variables are:
From the class Interest_account |
From the class Name_Address |
In the class Named_Account |
---|---|---|
the_interest_rate the_accumulated_interest the_balance |
the_string | - |
The implementation of the class is:
Named_Account::Named_Account(const char name[],const float amount) : Name_Address(name) , Interest_Account() { deposit( amount ); // set account to initial amount } |
void Named_Account::mini_statement() const { cout << "Mini Statement for "; print_name(); cout << " Balance of account is £" << account_balance(); cout << "\n"; } |
The only new code is the member function mini_statement which will print the name of the account holder followed by the current balance of their account. This uses the member function print_name of the class Name_Address.
The relationship between the classes is illustrated in Figure 9.3
Figure 9.3 Class hierarchy for Named_Account.
If classes are constructed carefully, then the possibility for re-use is considerably enhanced. However, there will be cases when the class does not do exactly what is required, in which case a re-casting of the base class may be the only answer.
For an object of type Named_Account the order of calling the base class's constructors is shown in Figure 9.4.
Figure 9.4 Order of calling constructors.
The body of Named_Account constructor deposits amount into the account, by calling the member function deposit of the class Account.
Note: The constructors are called from left to right.
If the constructors are not explicitly stated, then the order used for calling the constructors for the base classes, is the order in which the compiler processes the base class definitions.
The class Named_Account can now be used in a program as follows:
void main() { const double DAILY_RATE = 0.00026116; // 10% Annual rate Named_Account::prelude(DAILY_RATE); cout << setiosflags( ios::showpoint ); cout << setiosflags( ios::fixed ); cout << setprecision(2); process(); // prototype omitted } |
void process() { Named_Account a_n_other( "A N Other/Brighton" ); a_n_other.mini_statement(); a_n_other.deposit(500); cout << "Deposit £500" << "\n"; a_n_other.mini_statement(); a_n_other.calc_interest(); a_n_other.add_interest(); cout << "Add interest at end of day" << "\n"; a_n_other.mini_statement(); } |
Note: The use of the prelude to initialize the variable containing the interest rate payable on the account.
When run, the program would produce the following results:
Mini Statement for A N Other Balance of account is £0.00 Deposit £500 Mini Statement for A N Other Balance of account is £500.00 Add interest at end of day Mini Statement for A N Other Balance of account is £500.13 |
In the examples shown so far, the binding between the call of a member function in an object and the code that is eventually executed, is evaluated at compile time. This is commonly referred to as static binding. The implication of this is that the programmer knows in advance what object the member function is called on.
For example, it would not be possible to deal with an array of bank accounts which are composed of different types of account.
Later in Chapter 11, a mechanism for dealing with an array of bank accounts of different type will be shown.
The table below summarizes which functions are inherited and which are not.
Type of function | Inherited | Must be a | Created by default |
---|---|---|---|
The constructor | No | Member function | Yes |
The destructor | No | Member function | Yes |
Member function(s) | Yes | - | No |
Friend function(s) | No | - | No |
Note: Friend functions are described in Chapter 10.
Using multiple inheritance can sometimes produce a class made from multiple copies of the same base class. To prevent this from happening, the keyword virtual, can be prefixed to the name of the base class.
In the examples below, two base classes C1 and C2 have the specification and implementation.
Class C1: | Class C2: |
---|---|
class C1 { public: C1(); // C1's interface }; C1::C1() { cout << "C1 "; } |
class C2 { public: C2(); // C2's interface }; C2::C2() { cout << "C2 "; } |
These base classes are used in the construction of two derived classes D1 and D2.
Class D1: | Class D2: |
---|---|
class D1: virtual public C1, virtual public C2 { public: D1(); // D1's interface }; D1::D1() { cout << "D1 "; } |
class D2: virtual public C1 { public: D2(); // D2's interface }; D2::D2() { cout << "D2 "; } |
Note: The keyword virtual is used as a prefix when the class C1 or the class C2 are used in the derivation of the new classes.
At a later stage the classes D1, D2 and C2 are used to derive a new class E1 which will only contain a single copy of the classes C1 and C2.
class E1 : public D1, public D2, virtual public C2 { public: E1(); // Public interface of E1 }; E1::E1() { cout << "E1 "; } |
The class hierarchy for the above classes is:
If the class E1 were used in the program:
void main() { E1 object; } |
the output would be:
C1 C2 D1 D2 E1 |
Note: The order of calling the constructors is a simple left
to right evaluation as specified in the derived class.
E1 -> D1 D2 C2# then E1 constructor
D1 -> C1 C2 then D1 constructor
D2 -> C1# then D2 constructor
# Class not included
Had the virtual keyword not been used, then multiple copies of the classes C1 and C2 would have been included. The results from running the program modified to exclude the keyword virtual would be:
C1 D1 C1 C2 D2 C2 E1 |
Note
The omission of virtual before the class C2 in the
derivation of E1 would have resulted in an extra copy
of the class C2 being included.