Object-Oriented Programming 45-691

Download Report

Transcript Object-Oriented Programming 45-691

STAT 598W: Lecture 12
Purdue University
More on Inheritance and Related
Topics
Topics




Inheritance
Polymorphism
Virtual methods
Abstract classes
Inheritance

Inheritance is the property that
instances of a child class can access
data and behavior of the parent class(s).



Parent  superclass; child  subclass
A CEO is a manager is an employee
A dog is a mammal is an animal is a living
thing...
Benefits of Inheritance



Software reusability: inherited behavior
doesn't have to be rewritten, and thus will
save time and likely be more reliable.
Code sharing: separate users use the same
classes; two subclasses use facilities of a
common superclass.
Consistent interface: by inheriting methods,
the interface to subclasses will be largely
consistent. Objects that are almost the
same will have interfaces which are almost
the same.
Benefits of Inheritance


Software components: “off-the-shelf”
software units, e.g., Microsoft's Microsoft
Foundation Classes library, QuantLib, the
Interactive Brokers C++ API.
Rapid prototyping: concentrate only on
portions of a new system which are different
than previous ones. Get something running
sooner, for early evaluation.
Benefits of Inheritance
Polymorphism:




Software is typically designed top-down, written
bottom-up. Inheritance encourages abstract
superclasses, specialized for particular
circumstances.
So rather than having only very low-level code
reusable, code at the highest level of abstraction
can be reused.
Polymorphic objects can react differently
depending on the type of input they are given.
Costs of Inheritance



Execution speed: lots of function calls, very
general methods for dealing with arbitrary
subclasses. We are getting better at this…
Program size: a library of objects may have
just what you want, but it may be “spread
out.” Purpose-built code will likely be
smaller.
Other types of complexity: flow of control in
OOP programs can be just as hard to trace.
Implementation Details

Consider a class for employees:
class Employee {
public:
string name;
int age;
int department;
int salary;
Employee * next; // link to employee list
// ...
};
This example comes from Stroustrup
Managers Are Objects Too
class Manager {
Employee emp;
Employee* group;
int level;
// ...
};

// manager's employee record
// people managed
// high, higher, highest
So far, so good, but…
A Problem, and a Solution



A manager is an employee, so Employee data is
stored in the emp member of Manager. But how to
get a Manager into the linked list of employees?
A Manager* is different from an Employee*.
A better way: a Manager is an Employee, so make
it a subclass:
class Manager : public Employee { // public inheritance
Employee* group;
short level;
// ...
};
Subclass Advantages

Now we can create a list of employees,
some of whom are managers:
Employee * makeList() {
Manager m1, m2;
Employee e1, e2;
Employee * elist;
elist = &m1;
// put m1 on elist
m1.next = &e1;
// put e1 on elist
e1.next = &m2;
// put m2 on elist
m2.next = &e2;
// put e2 on elist
e2.next = 0;
// terminate elist
return elist;
}
How Does This Work?


This works because a manager is an
employee, so an Employee * can point
to a Manager.
This doesn't work the other way around,
unless there is explicit pointer type
conversion. (Which is really really
dangerous!)
Let's Add Some Methods
class Employee {
string name;
// ...
public:
Employee* next;
void print();
// ...
};
class Manager : public Employee {
// ...
public:
void print();
// ...
};
This Would Seem Natural
void Manager::print() {
cout << "name is " << name << '\n';
}

// error
But derived classes can't access private
portions of the base class. Why?


If they could, then private stuff wouldn't
really be private.
Anyone could construct a derived class and
have access.
The Good Solution


Derived classes cannot access private
members of the base class.
But they can access public members:
void Manager::print() {
Employee::print(); // print employee info, then
// print manager info
}

Note the use of the scope resolution operator,
needed since print() is being redefined in
Manager. (What happens if we forget to say Employee::?)
The Not-So-Good Solution



Make name a protected member of
the Employee class, so subclasses (like
Manager) have access
This is generally a bad idea, but
everyone does it.
Why bad? It breaks encapsulation;
anyone can write a subclass to get
access.
The Inevitable Student Class
#include <iostream>
#include <string>
using namespace std;
enum studentYear {freshman, sophomore, junior, senior, graduate};
class Student {
protected:
int studentID;
double gpa;
studentYear y;
string name;
public:
Student(int id, double g, studentYear x, string nm);
void print() const ;
};
GradStudents are Students
enum support { ta, ra, fellow, other};
class GradStudent: public Student {
protected:
support s;
Student members have to
string dept;
be protected for this to work
string thesis;
public:
GradStudent(int id, double g, year x, string nm,
support t, string d, string th);
void print() const;
};
New Concepts




Which base class members are accessible to
derived classes? The choices are public,
protected, private.
public means that protected and public members
of Student are available to GradStudent. This is
the normal (but not default) case.
Note that private members of the base class are
never available to subclasses. Why?
It is typical for a subclass to add new members,
both data and methods. Note print() is an
overridden method, a concept different from
overloading. The signature is the same, but the
owner is different.
Code Reuse With Inheritance
Student::Student(int id, double g, studentYear x, string nm)
:studentID(id),
gpa(g), y(x),
name(nm)
{
}
GradStudent::GradStudent(int id, double g, studentYear x,
string nm, support t, string d, string th)
:Student(id, g, x, nm),
s(t), dept(d),
thesis(th)
{
}
It’s common for the constructor of the derived class to call
the base class constructor. Now e.g., name can be private.
The print() Methods
void Student::print() const {
cout << name << ", " << studentID << ", "
<< (int)y << ", " << gpa;
}
void GradStudent::print() const {
Student::print();
cout << ", " << dept << ", " << (int)s
<< ", " << thesis;
}
Finally, a Driver Program
void main() {
Student s(365, 2.53, sophomore, "Larry Fine");
Student* ps = &s;
GradStudent gs(366, 2.03, graduate, "Curly Howard", ta,
"Theatre", "Nyuk Nyuk Nyuk");
GradStudent* pgs;
}
ps->print();
cout << endl;
ps = pgs = &gs; // implicit conversion of gradStudent* to student*
ps->print();
// student::print()
cout << endl;
pgs->print();
// gradStudent::print()
cout << endl;
Here is the Output
Larry Fine, 365, 1, 2.53
Curly Howard, 366, 4, 2.03
Curly Howard, 366, 4, 2.03, Theatre, 0, Nyuk Nyuk Nyuk
Static and Dynamic Typing




Binding time: the time when an attribute or meaning
of a a program construct is determined.
For instance, in strongly typed languages, variable
names are bound to variable types at compile time
(e.g., int a). This leaves no room for variables to
take on other types (c.f. variants in VBA).
Dynamically typed languages need some sort of run
time system, for example to determine variable types
and bind appropriate operators.
Essentially, the question is, are types associated with
variables (names) or with values?
Static and Dynamic Typing



If x and y are declared ints, then the
compiler knows what to do with the
expression x + y.
In OOP, there are additional problems. We
saw before that pointers to base class types
are valid pointers to subclass objects. This
violates the spirit of “strong typing”.
Similarly, an object of a subclass type is a
member of the superclass, so it is possible to
make an assignment.
Employees and Managers
void main() {
Employee e;
Manager m;
e.setSalary(30000);
e.print();
m.setSalary(40000);
m.setLevel(4);
m.print();
//m = e;
error: an e isn't an m
e = m;
// Legal: an m is an e
e.print();
// But, Employee::print() is called
}
The “Container Problem”



If we view m as an Employee, can we ever
recover that m is really a Manager?
We would like to write abstract “container
classes” like sets and lists, but in C++,
heterogeneous classes are harder than
homogeneous ones.
But if we use pointers to elements of
collections, things can be done.
Solving the Problem


Given a pointer of type base *, how do we
know if it points to an object of type base or
to some derived type?
Three possible solutions:



Ensure that only objects of a single type are
pointed to. This insists on homogeneous
containers.
Place a “type field” in the base class for functions
to inspect.
Use “virtual functions”.
Example of a Type Field
struct employee { // note this is a struct; everything public
enum emp_type { M, E};
emp_type type;
employee * next;
char * name;
// ...
}
struct manager : employee { // also a struct
employee * group;
short level;
// ...
}
Then, Define
void employee_print(employee * e) {
switch (e -> type) {
case E:
cout << e->name << '\t' << e->department << '\n';
break;
case M:
cout << e->name << '\t' << e->department << '\n';
manager * p = (manager*)e;
cout << "level " << p->level << '\n';
break;
}
This is a really bad idea. Don’t do it!
Printing the Employee List

A function to print employees might go
like:
void f(employee * elist) {
for (; elist; elist = elist->next)
print_employee(elist);
}

But what happens if a new subclass is
defined? Go through all the code?
Static and Dynamic Binding



If a message is passed to a receiver, which
method responds to the message?
On one hand, this is obvious: if the receiver
knows its type, then it will perform the
method associated with that type, or look
upwards.
On the other hand, there has to be a
mechanism to “find an object's type,” and this
may be expensive at run time.
Static and Dynamic Binding




Static binding if the declared object type
determines the method to use.
Dynamic binding if the actual type (at run
time) determines the method to use.
C++ “prefers” to use static binding, since it
imposes no additional overhead.
C++ can be forced to use dynamic binding if
necessary (the programmer has to ask for it
explicitly).
Virtual Functions


For dynamic binding, C++ gives us
virtual functions.
The virtual keyword says that a
function may be overridden in a
subclass, and that the type of the
object receiving a message should
determine which method (function) to
use.
Virtual Function Example
class Base {
public:
int i;
virtual void print_i() {
cout << i << " inside Base\n";
}
};
class Derived : public Base {
public:
virtual void print_i() {
cout << i << " inside Derived\n";
}
};
This is new
Virtual Function Example
void main() {
Base b;
Base* pb = &b;
Derived d;
b.i = 1
d.i = 2;
pb->print_i();
pb = &d;
pb->print_i();
}
This yields:
1 inside Base
2 inside Derived
Even though pb was declared
a pointer to type Base, it may
point to a Derived object.
When it does, Derived's member
function is chosen.
Abstract Classes


In the employee example, the base class
“makes sense”, that is, we can conceive of
objects of that type.
Sometimes this is not the case.
class Shape {
public:
virtual void rotate(int) {
error(“Shape:rotate");
}
virtual void draw() {
error(“Shape::draw");
}
};
Pure Virtual Functions



Making a shape of this unspecified kind is
rather pointless, since every operation on this
object results in an error.
We can get the compiler to help us keep
track by making Shape an abstract class.
This is done by defining one or more of its
member functions as pure virtual.
Pure Virtual Functions
class Shape {
// ...
public:
virtual void rotate(int) = 0;
virtual void draw() = 0;
// ...
};


Now no objects of type Shape may be
created.
An abstract class can only be used as a
base class for derived types.
Pure Virtual Functions
Let’s Use This Stuff!

Some classes to represent arithmetic
expressions:




Term (abstract)
Constant : public Term
BinaryOp : public Term (abstract)
Plus : public BinaryOp
Expression Trees
operands
Term
This is the “Composite”
design pattern
Expression
Variable
Constant
operator
name
value
+
+
Binary
3.3
Unary
1.1
2.2
Starting the Inheritance
Hierarchy
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
class Term {
public:
Term() {}
virtual ~Term() {}
virtual string symbolicEval() = 0;
virtual double numericalEval() = 0;
};
Our basic abstract class.
It has two pure virtual
methods, and a virtual
destructor.
symbolicEval() writes
an expression like
((1.1 + 2.2) + 3.3)
numericalEval() evaluates
it: 6.6
The Constant Class
class Constant : public Term {
double value;
public:
Constant() { value = 0; }
Constant(double v) { value = v; }
virtual ~Constant() {}
virtual string symbolicEval() {
ostringstream oss;
oss << value;
return oss.str();
}
virtual double numericalEval() {
return value;
}
};
This class is no longer
abstract, since the
pure virtuals are
overridden.
symbolicEval() uses
an ostringstream
object that allows
“<<-ing” into a
string.
The BinaryOp Class
class BinaryOp : public Term {
public:
virtual ~BinaryOp() {
if (lChild) delete lChild;
if (rChild) delete rChild;
}
protected:
Term * lChild, * rChild;
BinaryOp(Term * l, Term * r) {
lChild = l;
rChild = r;
}
};
This is the parent class for
the binary arithmetic
operators. It centralizes
common construction
and destruction activities.
Note that this class is still
abstract.
The Expression class in the
UML diagram is conceptually
nice, but not needed in
here.
Plus: A Typical Binary
Operator
class Plus : public BinaryOp {
public:
Plus(Term * l, Term * r) : BinaryOp(l, r) {}
virtual ~Plus() {}
virtual string symbolicEval() {
ostringstream oss;
oss << "(" << lChild->symbolicEval();
oss << " + ";
oss << rChild->symbolicEval() << ")";
return oss.str();
}
virtual double numericalEval() {
return (lChild->numericalEval() + rChild->numericalEval());
}
};
A Simple Driver
void main() {
Constant * c1 = new Constant(1.1);
Constant * c2 = new Constant(2.2);
Constant * c3 = new Constant(3.3);
Plus * p1 = new Plus(c1, c2);
Plus * p2 = new Plus(p1, c3);
cout << p2->symbolicEval() << " = ";
cout << p2->numericalEval() << endl;
delete p2;
}
Note that all Terms are created with new, and the tree is “held
together” with pointers. Pay particular attention to the way
delete works. Term’s destructor must be virtual for this to work!
More Advanced Stuff
Beware Implicit Conversions
class Base {
public:
virtual void foo(int) { cout << “Base::foo(int)” << endl;}
virtual void foo(double) {cout << “Base::foo(double)” << endl;}
//...
};
class Derived : public Base {
public:
virtual void foo(int) {cout << “Derived::foo(int)” << endl;}
//...
};
Beware Implicit Conversions
void main() {
Derived d;
Base b, *pb = &d;
}
b.foo(9);
b.foo(9.5);
d.foo(9);
d.foo(9.5);
pb->foo(9);
pb->foo(9.5);
// selects Base::foo(int)
// selects Base::foo(double)
// selects Derived::foo(int)
// selects Derived::foo(int) overriden
// selects Derived::foo(int)
// selects Base::foo(double) virtual func
Pure Virtual Functions


A pure virtual function which is not
defined in a derived class remains a
pure virtual function, so that the
derived class is also an abstract class.
This allows us to build implementations
in stages.
Pure Virtual Functions
class X {
public:
virtual void f() = 0;
virtual void g() = 0;
};
X b; // error: declaration of object of abstract class X
class Y : public X {
void f(); // overrides X::f
};
Y b; // error: declaration of object of abstract class Y
class Z : public Y {
void g(); // overrides X::g
};
Z c; // this is OK