1.05k likes | 1.19k Views
HKUST Summer Programming Course 2008. Inheritance ~ Writing Reusable Code. Overview. Introduction Derived Object Initialization Polymorphic Substitution Principle Member Access Control public, protected and private Inheritance Polymorphism and Binding Virtual Functions
E N D
HKUST SummerProgramming Course 2008 Inheritance ~ Writing Reusable Code
Overview • Introduction • Derived Object Initialization • Polymorphic Substitution Principle • Member Access Control • public, protected and private Inheritance • Polymorphism and Binding • Virtual Functions • Overriding and Overloading • Virtual Destructor • Abstract Base Class (ABC)
Inheritance Introduction
Introduction - Inheritance • Inheritance is a key feature of an object-oriented language. • It is the ability to define new classes using existing classes as a basis. • The new class (derived class) inherits all data members and member functions of the classes on which it is based (base class). (Exceptions: constructors, destructor, friend functions and the assignment operator are not inherited.)
Introduction - Inheritance • In addition, the new class can have additional data members and member functions specific to it. • The new class only has to implement the behavior that is different from existing classes. • SYNTAX class <derived-class-name> : <access> <base-class-name>{ // body of class }; where <access> must be either public, private or protected which determines the access status of base-class members inside the derived class.
Example – Person Class • First, let us look at an example. We begin with a class Person: class Person{ private: char name[30]; public: Person(); const char* get_name() const; void set_name(const char* n); void output_info() const; };
Example – Person Class Person::Person(){ name[0] = ‘\0’; } const char* Person::get_name() const{ return name; } void Person::set_name(const char* n){ strcpy(name, n); } void Person:output_info() const{ cout << “Name: “ << name << endl; }
Example – Employee Class • If we want to described person in employment, we can define a new class using Person as a basis: class Employee : public Person{ private: int salary; public: Employee() : salary(0) {} int earns() const; void change_salary(int new_salary); void output_info() const; };
Example – Employee Class int Employee::earns() const{ return salary; } void Employee::change_salary(int new_salary){ salary = new_salary; }
Inherited Data Members • We say that class Employee is a derived class from Person and that class Person is a direct base class of Employee. • A derived class inherits all the data members from the base class. Person p; Employee e; p e name name salary
Inherited Member Functions • Member functions can also be inherited. • An object of class Employee will have the member functions get_name, set_name, output_info, earns and change_salary. • We can also write statements p.set_name(“David”); p.output_info(); e.set_name(“Andrew”); e.change_salary(20000); int s = e.earns(); e.output_info();
Inherited Member Functions • But we could not write p.change_salary(1000); or p.earns(); Since these member functions do not exist for class Person.
Direct and Indirect Inheritance • A derived class can also have derived classes. • For instance, we declare a class Programmer that is a derived class of class Employee. class Programmer : public Employee{ private: // fam_lang indicates a programmer’s // most familiar programming language. char fam_lang[10]; public: Programmer(); const char* read_fam_lang() const; void change_fam_lang(const char* f); };
Direct and Indirect Inheritance Programmer::Programmer(){ fam_lang[0] = ‘\0’; } const char* Programmer::read_fam_lang() const{ return fam_lang; } void Programmer::change_fam_lang(const char* f){ strcpy(fam_lang,f); }
Direct and Indirect Inheritance • In last example, the class Person and Employee are base classes of Programmer. • The class Employee is a direct base class. • The class Person is an indirect base class. • We say, the class Programmer is directly derived from class Employee and indirectly derived from class Person. • An object of class Programmer inherits data members and member functions from all of its base classes.
Inheritance Derived Object Initialization
Derived Class Object Initialization • Objects that belong to a derived class can be initialized like other objects with the aid of constructors. • When a constructor for the derived class is called for this purpose • The data members that have been inherited from the base class must also be initialized. This is done by calling one of the constructors for the base class. • The data members specific to the derived class will be in turn initialized by the constructor.
Example - Object Initialization • Consider the following example. • Originally, the class Person only had one constructor, a default constructor, now we add another one. class Person{ private: char name[30]; public: Person(); Person(const char* n); // ... };
Example - Object Initialization Person::Person(){ name[0] = ‘\0’; } Person::Person(const char* n){ strcpy(name, n); } • The new constructor is called when we make the declaration: Person p(“David”);
Example - Object Initialization class Employee : public Person{ private: int salary; public: Employee() : salary(0) {} Employee(const char* n, int l) : Person(n), salary(1) {} }; • In the second constructor of Employee class, we have used an initialization list to initialize data members of the object. • The expression Person(n) is a call of the constructor for the base class Person to initialize those members in the base class.
Example - Object Initialization • Let us now see what happens if we create and initialize an object of class Employee using the following declaration. Employee e1(“Peter”, 20000); • The constructor Employee(const char* n, int l) : Person(n), salary(1){} is called. • Because the parameter n has the value “Peter”, we get the expression Person(“Peter”) in the initialization list. • As a result, the constructor Person(const char* n) is called and the data member name initialized. • Then the data member salary is initialized to 20000.
Example - Object Initialization • Note: The constructor for the base class is always called before • the data members specific to the derived class are initialized and • the statements to be executed inside the braces of derived class are executed. • In the constructor, Employee(const char* n, int l) : Person(n), salary(1) {} • Person(n) in written in front of salary(1) in the initialization list but the order doesn’t matter. • The base class constructor is always called first.
Example - Object Initialization • Let’s consider another case. We initialize an Employee object by making the declaration Employee e2; using the the default constructor for class Employee, i.e. Employee() : salary(0) {} • An initialization list is also used in this constructor but there is no expression for the base class Person. Since a constructor for a base class must always be called, a call of the base class default constructor is generated automatically. • In other words, the default constructor for class Employee will be implicitly converted to Employee() : Person(), salary(0) {}
Example: Order of Construction/Destruction #include <iostream> using namespace std; class A{ public: A() { cout << “A’s constructor” << endl; } ~A() { cout << “A’s destructor” << endl; } }; class B{ public: B() { cout << “B’s constructor” << endl; } ~B() { cout << “B’s destructor” << endl; } };
Example: Order of Construction/Destruction class C : public B{ private: A a; public: C() { cout << “C’s constructor” << endl;} ~C() { cout << “C’s destructor” << endl; } }; int main(){ C c; return 0; } Output: B’s constructor A’s constructor C’s constructor C’s destructor A’s destructor B’s destructor
Inheritance Polymorphic Substitution Principle
Polymorphic Substitution Principle • The single most important rule in OOP with C++ is: Inheritance means “is a”. • If class D (the derived class) inherits from class B (the base class) • Every object of type D is also an object of type B, but NOT vice-versa. • B is more general concept, D is more specific concept. • Where an object of type B is needed, an object of type D can be used instead. Base class “is-a” relationship Derived class
Polymorphic Substitution Principle • In C++, using our Person and Employee examples, where Employee is derived from Person, this means: • It is also known as “Liskov Substitution Principle”.
Example – Substitution in Arguments void dance(const Person& p); // Anyone can dance void work(const Employee& e); // Only Employees work void dance(Person* p); // Anyone can dance void work(Employee* e); // Only Employees work int main(){ Person p; Employee e; dance(p); /* OK */ dance(e); /* OK */ work(e); /* OK */ work(p); /* ERROR */ dance(&p); /* OK */ dance(&e); /* OK */ work(&e); /* OK */ work(&p); /* ERROR */ return 0; }
Extending Class Hierarchies • We can easily add new classes to our existing class hierarchy of Person, Employee, and Programmer. • New classes can immediately benefit from all functions that are available for their base classes. • e.g. void dance(const Person& p); will work immediately for a new class type Programmer which indirectly inherits Person, even though this type of object was unknown when dance(const Person& p) was designed and written. • In fact, it is not even necessary to recompile the existing code: It is enough to link the new class with the object code for Person and dance(const Person& p).
Slicing • A derived class often holds more information (i.e. data members) that its base class. • Thus, if we assign an instance of the derived class to a variable of type base class, there is not enough space to store the extra information, and it is sliced off. • This is rarely desirable • Note that this does not happen when you use pointers or references to objects.
Example - Slicing class Person{ class Employee : public Person{ private: private: char name[30]; int salary; public: public: // ... // ... }; }; int main(){ Employee e; Person p; Person* pp1 = &e; // okay Person* pp2 = new Employee; // okay p=e; // slicing occurs, salary is not copied return 0; }
Name Conflicts • We are allowed to use the same name of a member in a derived class as in a base class. class Employee : public Person{ private: int salary; char name[30]; public: Employee() : salary(0) {} int earns() const; void change_salary(int new_salary); void output_info() const; }; • A new data member “name” in class Employee hides the data member “name” of class Person. Employee name (Person) name salary
Name Conflicts • We can do the same thing for member functions. • When a derived class define a member function with the same name as a base class member function, it overrides the base class member function. • This is necessary if the behaviour of the base class member function is not good enough for derived classes.
Name Conflicts • For instance, a member function with name output_info is declared in class Employee. • This will hide the member function output_info which was declared in class Person. • If we write e.output_info(), where e is an object of type Employee, the output_info that was declared in class Employee will be called.
Name Conflicts • The definition of output_info for class Employee looks like this: void Employee::output_info() const{ Person::output_info(); cout << “Salary: ” << salary << endl; } • The second line is interesting. It shows how we can access a hidden member. • To access a hidden member with the name, say m that has been declared in a base class B, we write B::m. • Here we write Person::output_info(), which means that the output_info declared in base class Person will be called.
Example – Name Conflicts class B{ private: int x, y; public: B() : x(1), y(2){ cout << “Base class constructor” << endl; } void f(){ cout << “Base class: ” << x << “ , ” << y << endl; } };
Example – Name Conflicts class D : public B { private: float x, y; public: D() : x(10.0), y(20.0){ cout << “Derived class constructor” << endl; } void f(){ cout << “Derived class: ” << x << “ , ” << y << endl; B::f(); } };
Example – Name Conflicts void smart(B* z){ cout << “Inside smart(): ”; z->f(); } int main(){ B base; B* b = &base; D derive; D* d = &derive; base.f(); derive.f(); b = &derive; b->f(); smart(b); smart(d); return 0; } Output: Base class constructor Base class constructor Derived class constructor Base class: 1 , 2 Derived class 10 , 20 Base class: 1 , 2 Inside smart(): Base class: 1 , 2 Inside smart(): Base class: 1 , 2
Inheritance Member Access Control
Member Access Control • So far, we have seen how, in a class definition, we can use the access specifiers public and private to indicate whether the members of a class will be accessible outside the class. • There is a third possibility, a sort of compromise between public and private, the reserved word protected.
Member Access Control • A class definition can have the form class C{ public: // declaration of visible members protected: // declaration of protected members private: // declaration of private members }; • If no access specifier is written, by default all the members will be private.
Member Access Control • If a member is declared in a class C and is private (declared in the private section), it can only be used by the member functions in C and by the friends of class C. • If a member is declared in a class C and the member is protected (declared in the protected section), it can only be used by the member functions in C, friends of C and member functions and friends of classes derived from C. • If a member is public (declared in public section), it can be used everywhere, without restriction.
Example – Protected Data Members • A protected member is accessible within the inheritance hierarchy but not outside it. • We will use class Clock to give an example. class Clock{ protected: int h, m, s; public: Clock(); Clock(int hour, int min, int sec); void set(int hour, int min, int sec); int get_hour() const { return h; } int get_min() const { return m; } int get_sec() const { return s; } void write(bool write_sec = true) const; void tick(); };
Example – Protected Data Members • An alarm clock has the same properties as an ordinary clock but we have added the possibility of having an alarm at a specific time. class Alarm_Clock : public Clock{ protected: int ah, am, as; public: void set_alarm(int hour, int min, int sec); void tick(); };
Example – Protected Data Members • The member function set_alarm with which we indicate when the alarm is to ring. void set_alarm(int hour, int min, int sec){ ah = hour; am = min; as = sec; } • The definition of the new variant of the function tick is most interesting here. void tick(){ Clock::tick(); if(h == ah && m==am && s==as) cout << “\a\a\a”; }
Example – Protected Data Members • In member function tick(), • First we call the function tick, which was declared in the base class Clock. This moves the clock forward by one second and then check to see whether it is time for the alarm to ring. • But for this to happen, we must have access to the data members of base class Clock (i.e. h, m, s). • If h, m and s had been private members in the base class, this would not have been possible, but by declaring them as protected, we will have access to them in derived class Alarm_Clock.
protected vs. private • As a rule, we said that if a class is constructed and intended to make it the base class for various derived classes, then its data members should be declared as protected rather than as private. • So why not always use protected instead of private? • Because protected means that we have less encapsulation: Re-member that all derived classes can access protected data members of the base class. • Assume that later you decided to change the implementation of the base class having the protected data members. • If it is protected, we have to go through all derived classes and change them.
protected vs. private • In general, it is preferable to have private members instead of protected members. • Use protected only where it is really necessary. Private is the only category ensuring full encapsulation. • This is particularly true for data members, but it is less harmful to have protected member functions. • In our example, there is no reason at all to make h, m, s protected, since we can access them through the public member functions.
Example – tick() using Public Functions Only class Alarm_Clock : public Clock{ protected: int ah, am, as; public: // ... void tick(){ Clock::tick(); // use public member functions to access // data members h, m and s. if(read_hour() == ah && read_min() == am && read_sec() == as) // \a represents alert sound (bell) cout << “\a\a\a”; } };