428 lines
12 KiB
Markdown
428 lines
12 KiB
Markdown
# CSE332S Object-Oriented Programming in C++ (Lecture 12)
|
||
|
||
## Object-Oriented Programming (OOP) in C++
|
||
|
||
Today:
|
||
|
||
1. Type vs. Class
|
||
2. Subtypes and Substitution
|
||
3. Polymorphism
|
||
a. Parametric polymorphism (generic programming)
|
||
b. Subtyping polymorphism (OOP)
|
||
4. Inheritance and Polymorphism in C++
|
||
a. construction/destruction order
|
||
b. Static vs. dynamic type
|
||
c. Dynamic binding via virtual functions
|
||
d. Declaring interfaces via pure virtual functions
|
||
|
||
## Type vs. Class, substitution
|
||
|
||
### Type (interface) vs. Class
|
||
|
||
Each function/operator declared by an object has a signature: name, parameter list, and return value
|
||
|
||
The set of all public signatures defined by an object makes up the interface to the object, or its type
|
||
|
||
- An object’s type is known (what can we request of an object?)
|
||
- Its implementation is not - different objects may implement an interface very differently
|
||
- An object may have many types (think interfaces in Java)
|
||
|
||
An object’s class defines its implementation:
|
||
|
||
- Specifies its state (internal data and its representation)
|
||
- Implements the functions/operators it declares
|
||
|
||
### Subtyping: Liskov Substitution Principle
|
||
|
||
An interface may contain other interfaces!
|
||
|
||
A type is a **subtype** if it contains the full interface of another type (its **supertype**) as a subset of its own interface. (subtype has more methods than supertype)
|
||
|
||
**Substitutability**: if S is a subtype of T, then objects of type T may be replaced with objects of type S
|
||
|
||
Substitutability leads to **polymorphism**: a single interface may have many different implementations
|
||
|
||
## Polymorphism
|
||
|
||
Parametric (interface) polymorphism (substitution applied to generic programming)
|
||
|
||
- Design algorithms or classes using **parameterized types** rather than specific concrete data types.
|
||
- Any class that defines the full interface required of the parameterized type (is a **subtype** of the parameterized type) can be substituted in place of the type parameter **at compile-time**.
|
||
- Allows substitution of **unrelated types**.
|
||
|
||
### Polymorphism in OOP
|
||
|
||
Subtyping (inheritance) polymorphism: (substitution applied to OOP)
|
||
|
||
- A derived class can inherit an interface from its parent (base) class
|
||
- Creates a subtype/supertype relationship. (subclass/superclass)
|
||
- All subclasses of a superclass inherit the superclass’s interface and its implementation of that interface.
|
||
- Function overriding - subclasses may override the superclass’s implementation of an interface
|
||
- Allows the implementation of an interface to be substituted at run-time via dynamic binding
|
||
|
||
## Inheritance in C++ - syntax
|
||
|
||
### Forms of Inheritance in C++
|
||
|
||
A derived class can inherit from a base class in one of 3 ways:
|
||
|
||
- Public Inheritance ("is a", creates a subtype)
|
||
- Public part of base class remains public
|
||
- Protected part of base class remains protected
|
||
- Protected Inheritance ("contains a", **derived class is not a subtype**)
|
||
- Public part of base class becomes protected
|
||
- Protected part of base class remains protected
|
||
- Private Inheritance ("contains a", **derived class is not a subtype**)
|
||
- Public part of base class becomes private
|
||
- Protected part of base class becomes private
|
||
|
||
So public inheritance is the only way to create a **subtype**.
|
||
|
||
```cpp
|
||
class A {
|
||
public:
|
||
int i;
|
||
protected:
|
||
int j;
|
||
private:
|
||
int k;
|
||
};
|
||
class B : public A {
|
||
// ...
|
||
};
|
||
class C : protected A {
|
||
// ...
|
||
};
|
||
class D : private A {
|
||
// ...
|
||
};
|
||
```
|
||
|
||
Class B uses public inheritance from A
|
||
|
||
- `i` remains public to all users of class B
|
||
- `j` remains protected. It can be used by methods in class B or its derived classes
|
||
|
||
Class C uses protected inheritance from A
|
||
|
||
- `i` becomes protected in C, so the only users of class C that can access `i` are the methods of class C
|
||
- `j` remains protected. It can be used by methods in class C or its derived classes
|
||
|
||
Class D uses private inheritance from A
|
||
|
||
- `i` and `j` become private in D, so only methods of class D can access them.
|
||
|
||
## Construction and Destruction Order of derived class objects
|
||
|
||
### Class and Member Construction Order
|
||
|
||
```cpp
|
||
class A {
|
||
public:
|
||
A(int i) : m_i(i) {
|
||
cout << "A" << endl;}
|
||
~A() {cout<<"~A"<<endl;}
|
||
private:
|
||
int m_i;
|
||
};
|
||
class B : public A {
|
||
public:
|
||
B(int i, int j)
|
||
: A(i), m_j(j) {
|
||
cout << "B" << endl;}
|
||
~B() {cout << "~B" << endl;}
|
||
private:
|
||
int m_j;
|
||
};
|
||
int main (int, char *[]) {
|
||
B b(2,3);
|
||
return 0;
|
||
};
|
||
```
|
||
|
||
In the main function, the B constructor is called on object b
|
||
|
||
- Passes in integer values 2 and 3
|
||
|
||
B constructor calls A constructor
|
||
|
||
- passes value 2 to A constructor via base/member initialization list
|
||
|
||
A constructor initializes `m_i` with the passed value 2
|
||
|
||
- Body of A constructor runs
|
||
- Outputs "A"
|
||
|
||
B constructor initializes `m_j` with passed value 3
|
||
|
||
- Body of B constructor runs
|
||
- outputs "B"
|
||
|
||
### Class and Member Destruction Order
|
||
|
||
```cpp
|
||
class A {
|
||
public:
|
||
A(int i) : m_i(i) {
|
||
cout << "A" << endl;}
|
||
~A() {cout<<"~A"<<endl;}
|
||
private:
|
||
int m_i;
|
||
};
|
||
class B : public A {
|
||
public:
|
||
B(int i, int j)
|
||
: A(i), m_j(j) {
|
||
cout << "B" << endl;}
|
||
~B() {cout << "~B" << endl;}
|
||
private:
|
||
int m_j;
|
||
};
|
||
int main (int, char *[]) {
|
||
B b(2,3);
|
||
return 0;
|
||
};
|
||
```
|
||
|
||
B destructor called on object b in main
|
||
|
||
- Body of B destructor runs
|
||
- outputs "~B"
|
||
|
||
B destructor calls “destructor” of m_j
|
||
|
||
- int is a built-in type, so it’s a no-op
|
||
|
||
B destructor calls A destructor
|
||
|
||
- Body of A destructor runs
|
||
- outputs "~A"
|
||
|
||
A destructor calls “destructor” of m_i
|
||
|
||
- again a no-op
|
||
|
||
At the level of each class, order of steps is reversed in constructor vs. destructor
|
||
|
||
- ctor: base class, members, body
|
||
- dtor: body, members, base class
|
||
|
||
In short, cascading order is called when constructor is called, and reverse cascading order is called when destructor is called.
|
||
|
||
## Polymorphic function calls - function overriding
|
||
|
||
### Static vs. Dynamic type
|
||
|
||
The type of a variable is known statically (at compile time), based on its declaration
|
||
|
||
```cpp
|
||
int i; int * p;
|
||
Fish f; Mammal m;
|
||
Fish * fp = &f;
|
||
```
|
||
|
||
However, actual types of objects aliased by references & pointers to base classes vary dynamically (at run-time)
|
||
|
||
```cpp
|
||
Fish f; Mammal m;
|
||
Animal * ap = &f; // dynamic type is Fish
|
||
ap = &m; // dynamic type is Mammal
|
||
Animal & ar = get_animal(); // dynamic type is the type of the object returned by get_animal()
|
||
```
|
||
|
||
A base class and its derived classes form a set of types
|
||
|
||
`type(*ap)` $\in$ `{Animal, Fish, Mammal}`
|
||
`typeset(*fp)` $\subset$ `typeset(*ap)`
|
||
|
||
Each type set is **open**
|
||
|
||
- More subclasses can be added
|
||
|
||
### Supporting Function Overriding in C++: Virtual Functions
|
||
|
||
Static binding: A function/operator call is bound to an implementation at compile-time
|
||
|
||
Dynamic binding: A function/operator call is bound to an implementation at run-time. When dynamic binding is used:
|
||
|
||
1. Lookup the dynamic type of the object the function/operator is called on
|
||
2. Bind the call to the implementation defined in that class
|
||
|
||
Function overriding requires dynamic binding!
|
||
|
||
In C++, virtual functions facilitate dynamic binding.
|
||
|
||
```cpp
|
||
class A {
|
||
public:
|
||
A () {cout<<" A";}
|
||
virtual ~A () {cout<<" ~A";} // tells compiler that this destructor might be overridden in a derived class (the destructor of the parent class is usually virtual)
|
||
virtual void f(int); // tells compiler that this function might be overridden in a derived class
|
||
};
|
||
class B : public A {
|
||
public:
|
||
B () :A() {cout<<" B";}
|
||
virtual ~B() {cout<<" ~B";}
|
||
virtual void f(int) override; // tells compiler that this function might be overridden in a derived class, the parent function is virtual otherwise it will be an error
|
||
//C++11
|
||
};
|
||
int main (int, char *[]) {
|
||
// prints "A B"
|
||
A *ap = new B;
|
||
// prints "~B ~A" : would only
|
||
// print "~A" if non-virtual
|
||
delete ap;
|
||
return 0;
|
||
};
|
||
```
|
||
|
||
Virtual functions:
|
||
|
||
- Declared virtual in a base class
|
||
- Can override in derived classes
|
||
- Overriding only happens when signatures are the same
|
||
|
||
- Otherwise it just overloads the function or operator name
|
||
|
||
When called through a pointer or reference to a base class:
|
||
|
||
- function/operator calls are resolved dynamically
|
||
|
||
Use `final` (C++11) to prevent overriding of a virtual method
|
||
|
||
Use `override` (C++11) in derived class to ensure that the signatures match (error if not)
|
||
|
||
```cpp
|
||
class A {
|
||
public:
|
||
void x() {cout<<"A::x";};
|
||
virtual void y() {cout<<"A::y";};
|
||
};
|
||
class B : public A {
|
||
public:
|
||
void x() {cout<<"B::x";};
|
||
virtual void y() {cout<<"B::y";};
|
||
};
|
||
int main () {
|
||
B b;
|
||
A *ap = &b; B *bp = &b;
|
||
b.x (); // prints "B::x": static binding always calls the x() function of the class of the object
|
||
b.y (); // prints "B::y": static binding always calls the y() function of the class of the object
|
||
bp->x (); // prints "B::x": lookup the type of bp, which is B, and x() is non-virtual so it is statically bound
|
||
bp->y (); // prints "B::y": lookup the dynamic type of bp, which is B (at run-time), and call the overridden y() function
|
||
ap->x (); // prints "A::x": lookup the type of ap, which is A, and x() is non-virtual so it is statically bound
|
||
ap->y (); // prints "B::y": lookup the dynamic type of ap, which is B (at run-time), and call the overridden y() function of class B
|
||
return 0;
|
||
};
|
||
```
|
||
|
||
Only matter with pointer or reference
|
||
|
||
- Calls on object itself resolved statically
|
||
- E.g., `b.y();`
|
||
|
||
Look first at pointer/reference type
|
||
|
||
- If non-virtual there, resolve statically
|
||
- E.g., `ap->x();`
|
||
- If virtual there, resolve dynamically
|
||
- E.g., `ap->y();`
|
||
|
||
Note that virtual keyword need not be repeated in derived classes
|
||
|
||
- But it’s good style to do so
|
||
|
||
Caller can force static resolution of a virtual function via scope operator
|
||
|
||
- E.g., `ap->A::y();` prints “A::y”
|
||
|
||
Potential Problem: Class Slicing
|
||
|
||
When a derived type may be caught by a catch block, passed into a function, or returned out of a function that expects a base type:
|
||
|
||
- Be sure to catch by reference
|
||
- Pass by reference
|
||
- Return by reference
|
||
|
||
Otherwise, a copy is made:
|
||
|
||
- Loses original object's "dynamic type"
|
||
- Only the base parts of the object are copied, resulting in the class slicing problem
|
||
|
||
## Class (implementation) Inheritance VS. Interface
|
||
|
||
Inheritance
|
||
Class is the implementation of a type.
|
||
|
||
- Class inheritance involves inheriting interface and implementation
|
||
- Internal state and representation of an object
|
||
|
||
Interface is the set of operations that can be called on an object.
|
||
|
||
- Interface inheritance involves inheriting only a common interface
|
||
- What operations can be called on an object of the type?
|
||
- Subclasses are related by a common interface
|
||
- But may have very different implementations
|
||
|
||
In C++, pure virtual functions make interface inheritance possible.
|
||
|
||
```cpp
|
||
class A { // the abstract base class
|
||
public:
|
||
virtual void x() = 0; // pure virtual function, no default implementation
|
||
virtual void y() = 0; // pure virtual function, no default implementation
|
||
};
|
||
class B : public A { // B is still an abstract class because it still has a pure virtual function y() that is not defined
|
||
public:
|
||
virtual void x();
|
||
};
|
||
class C : public B { // C is a concrete derived class because it has all the pure virtual functions defined
|
||
public:
|
||
virtual void y();
|
||
};
|
||
int main () {
|
||
A * ap = new C; // ap is a pointer to an abstract class type, but it can point to a concrete derived class object, cannot create an object of an abstract class, for example, new A() will be an error.
|
||
ap->x ();
|
||
ap->y ();
|
||
delete ap;
|
||
return 0;
|
||
};
|
||
```
|
||
|
||
Pure Virtual Functions and Abstract Base Classes:
|
||
|
||
A is an **abstract (base) class**
|
||
|
||
- Similar to an interface in Java
|
||
- Declares pure virtual functions (=0)
|
||
- May also have non-virtual methods, as well as virtual methods that are not pure virtual
|
||
|
||
Derived classes override pure virtual methods
|
||
|
||
- B overrides `x()`, C overrides `y()`
|
||
|
||
Can't instantiate an abstract class
|
||
|
||
- class that declares pure virtual functions
|
||
- or inherits ones that are not overridden
|
||
|
||
A and B are abstract, can create a C
|
||
|
||
Can still have a pointer or reference to an abstract class type
|
||
|
||
- Useful for polymorphism
|
||
|
||
## Review of Inheritance and Subtyping Polymorphism in C++
|
||
|
||
Create related subclasses via public inheritance from a common superclass
|
||
|
||
- All subclasses inherit the interface and its implementation from the superclass
|
||
|
||
Override superclass implementation via function overriding
|
||
|
||
- Relies on virtual functions to support dynamic binding of function/operator calls
|
||
|
||
Use pure virtual functions to declare a common interface that related subclasses can implement
|
||
|
||
- Client code uses the common interface, does not care how the interface is defined. Reduces complexity and dependencies between objects in a system.
|