Inheritance
Mechanism for enhancing existing classes, reusing code
E.g. Manager inherits the name, salary and print() method from Employee. This makes it easier to change code or add functionality in the future.
class Employee {
public:
string name;
int salary;
void print() { cout << name << " : has salary " << salary << endl; }
};
class Manager : public Employee {
};
Multiple Inheritance (MI)
Multiple inheritance is allowed in C++. Because of the complexity MI involves, sometimes good programming practices discourage against using MI. Here are some precautions:
1) No class can be its own direct or indirect parent.
- This prevents the inheritance diagram from ever having cycles. A directed graph with no cycles is termed a directed acyclic graph, or DAG. Class diagrams for classes with multiple inheritance are one example of a DAG.
2) One must not abandon the is-a relationship that exists between base and derived classes.
- A common mistake is seeing that a car has an engine and a body and then having the car inherit both of these classes. This is inappropriate.
- An example of appropriate multiple inheritance is the case of a teaching assistant (TA) being both a student and staff. The teaching assistant class may inherit both base classes of student and staff because a TA is both a student and staff.
3) Consider alternative designs to capture the relationship:
- Inherit multiple interfaces is sometimes a better choice as it avoids ambiguity
- Having nested classes could also be a good choice as the nested class can maintain an instance of the outer class.
Encapsulation
The act of hiding implementation details is called encapsulation.
E.g. Here the getCost method returns the cost of the product. The details are hidden and makes the API simple to use and easy to change. For example in the future the tax rate may change for the different regions we don't need to change the public section of the class, only the getTax() method.
class Sale {
private:
double price;
double getTax(int region) {...};
public:
double getCost() { return price + (price * getTax(0); }
};
Polymorphism
Objects can with the same base class can behave as their derived classes in run time when it's appropriate.
E.g.
class Employee {
public:
string name;
int salary;
Employee(string n, int s) : name(n), salary(s) {}
int getPay() { return salary; }
};
class Manager : public Employee {
private:
int calculateBonus() { return 500; }
public:
Manager(string n, int s) : Employee(n, s) {}
int getPay() { return salary + calculateBonus(); }
};
Here we have different implementations of getPay() for employee and manager. What if we have a list of employees and managers mixed together and want to get their pay with a single for loop or function call? Polymorphism allows us to do that. But first we need to add "virtual" to the getPay() method so that the compiler knows it could potentially be overrided by derived classes. This is an example of dynamic binding. The method getPay() is dynamically bound whereas the regular methods are statically bound. The difference is statically bound methods are selected at compile time, and dynamically bound method is selected at run time.
class Employee {
public:
string name;
int salary;
virtual int getPay() { return salary; }
};
A reference from Big C++ on virtual:
- The virtual keyword must be used in the base class. All functions with the same name and parameter types in derived classes are then automatically virtual. However, it is considered good taste to supply the virtual keyword for the derived-class functions as well. Whenever a virtual function is called, the compiler determines the type of the implicit parameter in the particular call at run time. The appropriate function for that object is then called.
E.g. This will call the correct getPay() method on the manager and employees.
vector<Employee*> allEmployees = vector<Employee*>(3);
allEmployees[0] = new Employee("John", 3000);
allEmployees[1] = new Employee("Joe", 4000);
allEmployees[2] = new Manager("Jay", 4500);
for (int i = 0; i < allEmployees.size(); i++) {
cout << allEmployees[i]->getPay() << endl;
}
This will print:
3000
4000
5000
The manager got his/her bonus of 500 added because polymorphism called the correct method at run time.
Another note - we used a vector of Employee pointers in this example. Is it possible with vector of Employees? The answer is no.
The reason is the derived classes are typically larger than the base classes and a vector of objects cannot deal with variation in sizes and therefore the derived instances will get sliced away. So we must use a pointer because pointers to various objects all have the same size - the size of a memory address.
What if we used a vector of Managers to store all the employees? Since the derived classes are typically larger, we surely cannot fail by using the larger of the two objects? The answer to this is no. We cannot use a vector of type "Manager" to store objects of type "Employee" because of the inheritance hierarchy. A Manager is an Employee, but an Employee is not necessarily a Manager. The code will not compile.
Lastly it is possible to make a vector of Managers and create all employees as managers with bonus of $0. However this defeats the purpose of separating the concept of Employees and Managers. Why even have two classes at this point?