500 likes | 516 Views
Learn about the basics of inheritance and polymorphism in C++ programming, including creating derived classes and refining base class behaviors. Explore real-life examples and implementation details.
E N D
Software Design and C++ Lecture 5 Inheritance and Polymorphism
Contents • Introduction • Base classes and derived classes • Initializing derived class objects • Protected class members • Towards polymorphism – pointers and references to objects • Polymorphism • Abstract classes • Polymorphism and OOP
Introduction • Inheritance is a fundamental requirement of oriented programming • It allows us to create new classes by refining existing classes • Essentially a derived class can inherit data members of a base class • The behaviour of the derived class can be refined by redefining base class member functions or adding new member function • A key aspect of this is polymorphism where a classes behaviour can be adapted at run-time
Base classes and derived classes • We can think of many examples in real life of how a (base) class can be refined to a set of (derived) classes • For example a Polygon class can be refined to be a Quadrilateral which can be further refined to be a Rectangle • We can think of these classes as following an IS-A relationship • A Quadrilateral IS-A Polygon • A Rectangle IS-A Quadrilateral
We can think of lots of other possible examples of base/derived classes
Example • An Account base class models basic information about a bank account • Account holder • Account number • Current balance • Basic functionality • Withdraw money • Deposit money
class Account { private: int account_number; char* account_holder; int balance; public: Account(int, char*,int); void withdraw(int amount) { if (balance>amount) balance-=amount; } void deposit(int amount) { balance+=amount;} };
We can consider refinements to our Account class • CurrentAccount • Can have an overdraft facility • No interest paid • DepositAccount • Pays interest on any balance • No overdraft facility
We will create our refined classes using public inheritance from the Account base class • Classes CurrentAccount and DepositAccount inherit the basic attributes (private members) of account • account_number • account_holder • balance • Also, new attributes are added • CurrentAccount::overdraft_facility • DepositAccount::interest_rate
class CurrentAccount public Account { private: int overdraft_facility; public: CurrentAccount(int, char*, int, int); void withdraw(int); // Takes account of overdraft // facility };
class DepositAccount public Account { private: float interest_rate; public: DepositAccount(int, char*, int, float); float calc_interest(); // Calculates interest based // on the current balance };
CurrentAccount DepositAccount account_number account_holder balance account_number account_holder balance deposit() withdraw() deposit() withdraw() overdraft_facility interest_rate withdraw() calc_interest()
In class CurrentAccount, the member function withdraw() is overridden • This will be a crucial aspect of object oriented programming as we shall see later • Polymorphism • Also, a new member function, calc_interest() is defined in DepositAccount • This is an example of a derived class extending a base class
Before we can finish implementing the derived classes, we need to consider private/public access • Key point 1 • In public inheritance, public member functions of the base class become public member functions of the derived class
Thus in our example, the following are public member functions and can be called from outside of the class • CurrentAccount::withdraw() • CurrentAccount::deposit() • DepositAccount::withdraw() • DepositAccount::deposit()
Key point 2 • Private members of the base class can not be accessed from the derived class • Obvious otherwise encapsulation could be easily broken by inheriting from the base class • Begs the question, how do we initialise derived class objects? • We can see how this is done by implementing the constructors of DepositAccount and CurrentAccount
Account::Account(int acc_no, char* acc_holder, int b) { account_number=acc_no; strcpy(acc_holder,account_holder); balance=b; } DepositAccount::DepositAccount(int acc_no, char* acc_holder, int b, float int_rate):Account(acc_no, acc_holder, b) {interest_rate=int_rate;} CurrentAccount::CurrentAccount(int acc_no, char* acc_holder, int b, int ov_facility):Account(acc_no, acc_holder, b) {overdraft_facility=ov_facility;}
Thus derived class constructors are implemented by first calling the base class constructor and then initializing specific derived class private data members • Uses the following syntax: MyDerivedClass:MyDerivedClass(int arg1, int arg2, int arg3):MyBaseClass(arg1, arg2) { // Initialize additional private data member using arg3 }
Protected class members • A protected class member is one that can be accessed by public member functions of the class as well as public member functions of any derived class • Its half way between private and public • Encapsulation is then broken for classes in the inheritance hierarchy and thus must be used where performance issues are critical
Consider the implementation of CurrentAccount::withdraw() and DepositAccount::calc_interest() void CurrentAccount::withdraw(int amount) { // Needs to access and update Account::balance } float DepositAccount::calc_interest() { // Needs to access and update Account::balance }
Making Account::balance protected removes the need for Account::get_balance() and Account::set_balance() access functions class Account { private: int account_number; char* account_holder; protected: int balance; public: Account(int, char*,int); void withdraw(int amount) { balance-=amount;} void deposit(int amount) { balance+=amount;} };
void CurrentAccount::withdraw(int amount) { if ((balance-amount)>-overdraft_facility) balance-=amount; } float DepositAccount::calc_interest() { float interest=balance*interest_rate; balance+=interest; return interest; }
We can summarise private/protected/public access in the following table
Towards polymorphism – pointers and references to objects • A key aspect of object oriented programming is polymorphism • The ability to reference objects which show different behaviour • Polymorphism is implemented through pointers and references to objects
We can set up a pointer (or a reference) to an object very easily Account acc(12345, “J. Smith”, 100); Account* p_acc=&acc; Account 12345 J Smith 100 p_acc deposit() withdraw()
Key point • Because of the IS-A relationship, we can point a base class pointer at derived class objects – but not vice versa • An Account object is a DepositAccount and a CurrentAccount object DepositAccount dep_acc(12345, “J. Smith”, 100,5.0); Account* p_acc = &dep_acc; //OK
DepositAccount 12345 J Smith 100 p_acc deposit() withdraw() 5.0 calc_interest()
But we can’t point a derived class pointer at a base class object • An Account object is not (necessarily) a DepositAccount object Account acc(12345, “J. Smith”, 100); DepositAccount* p_dep_acc = &acc; //Error!
Polymorphism • Polymorphism is the key concept in object oriented programming • First we will look at the mechanics of polymorphism using our simple example classes • We will then look at how polymorphism lends itself to OOP as well as the advantages over a procedural approach
We can see from the CurrentAccount class that CurrentAccount::withdraw() overrides Account::withdraw() CurrentAccount account_number account_holder balance deposit() withdraw() overridden overdraft_facility withdraw()
Consider the following code CurrentAccount curr_acc(12345, “J. Smith”, 100,500); Account* p_acc = &curr_acc; //OK p_acc->withdraw(250); // Which withdraw()
CurrentAccount p_acc account_number account_holder balance deposit() withdraw() Which one is called? overdraft_facility withdraw()
Because p_acc has been declared as Account*, Account::withdraw() is called • Incorrect behaviour as p_acc points to a CurrentAccount object CurrentAccount curr_acc(12345, “J. Smith”, 100, 500); Account* p_acc = &curr_acc; p_acc->withdraw(250); // Calls Account::withdraw() // Ignores overdraft facility
The solution is to make Account::withdraw() a virtual function so that dynamic binding takes place • The derived class member function CurrentAccount::withdraw() is actually called and correct behaviour is observed • Changes to the definition of the Account class are minimal
class Account { private: int account_number; char* account_holder; protected: int balance; public: Account(int, char*,int); virtual void withdraw(int amount) { balance-=amount;} void deposit(int amount) { balance+=amount;} };
Abstract classes • In our example classes, Account::withdraw() was declared as a virtual function • Were were able to provide a sensible implementation of this function • This implementation could be regarded as default behaviour if the function was not overridden in derived classes
CurrentAccount curr_acc(12345, “J. Smith”, 100, 500); DepositAccount dep_acc(56789, “J. Smith”, 100, 5.0); Account* p_acc_curr = &curr_acc; Account* p_acc_dep = &dep_acc; p_acc_curr->withdraw(250); // Account::withdraw() // overridden by // CurrentAccount::withdraw() p_acc_dep->withdraw(250); // Account::withdraw() // called
Abstract classes arise when there is no sensible implementation of the virtual functions in the base class • Base class virtual functions are always overridden by derived class implementations • In this case, we simply assign the base class virtual functions to zero • They become pure virtual functions • A class containing at least one pure virtual function is an abstract class
As an example, suppose we wanted to design a hierarchy of shape classes for a computer graphics application class Shape { public: virtual void draw()=0; virtual void move(int x, int y)=0; virtual float area()=0; };
Shape is an abstract concept and there are no sensible definitions of Shape::draw(), Shape::move() and Shape::area() • They are assigned to zero and hence are pure virtual functions • This makes Shape an abstract class • We cannot declare Shape objects • Derived objects of Shape can be created and implementations of the pure virtual functions provided
class Square : public Shape { private: int x_pos, y_pos; int side; public: Square(int x, int y, int length) { x_pos=x; y_pos=y; side=length;} void draw() {// Call graphics commands …} void move(int x, int y) { x_pos+=x; y_pos+=y;} float area() { return side*side}; };
class Circle : public Shape { private: int x_cent, y_cent; int radius; public: Circle(int x, int y, int r) { x_cent=x; y_cent=y; radius=r;} void draw() {// Call graphics commands …} void move(int x, int y) { x_cent+=x; y_cent+=y;} float area() { return 3.14159*radius*radius;} };
The pure virtual functions in Shape are always overridden in the derived class through polymorphism void main() { Shape s; // Error! Can’t create a Shape object Shape* sp=new Square(5,6,20); // OK sp->move(10,10); // Calls Square::move() sp->draw(); // Calls Square::draw() float a=sp->area(); // Calls Square::area() };
We can regard abstract classes as a ‘glue’ which binds related classes together and where we don’t have to worry about implementational details • They just have to present a common interface Shape Circle Square Triangle draw() move() area()
Polymorphism and OOP • We can easily conceive of an application where a heterogenous list (or array) of shape objects are manipulated • For example, in a CAD system, a complex drawing may comprise a list of basic shapes • Each object on the list may exhibit different behaviour (for example may draw itself differently) but the application user need not worry about this • Polymorphism looks after it
Example • A hetereogenous list of shapes (accessed an array of Shape pointers) can each be drawn • This could be a means of drawing a complex shape from simpler sub-shapes ….. Shape** s s[0] s[1] s[2] s[3] s[4] draw() draw() draw() draw() draw()
void main() { Shape** s = new Shape*[5]; s[0]=new Square(0,0,5); s[1]=new Circle(2,4,3); s[2]=new Circle(3,1,6); s[3]=new Square(2,5,3); s[4]=new Square(2,6,7); for (int j=0; j<5; j++) s[j]->draw(); // Draws each shape on the list };
So what’s the advantage of doing it this (OOP) way? • Suppose we want to extend our list of ‘sub-shapes’ • We simply derive any new shape from Shape and add the required functionality to the virtual functions draw(), move() and area() • The code for the for loop doesn’t change • Polymorphism automatically invokes the correct behaviour
class Triangle : public Shape { private: int x_vertices[3], int y_vertices[3]; public: Triangle(int[] xv, int[] yv) {…} void draw() {…} void move(int x, int y) {…} float area() {..} }; void main() { … for (int j=0; j<5; j++) s[j]->draw(); // Draws each shape on the list }
And finally….. • Polymorphism is the critical component in object oriented systems • C++ supports this with the virtual function • In Java, all inherited member functions are virtual • Its important to understand how this leads to extendible applications as new objects exhibiting new behaviours can be easily introduced