upgrade structures and migrate to nextra v4
This commit is contained in:
427
content/CSE332S/CSE332S_L12.md
Normal file
427
content/CSE332S/CSE332S_L12.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# CSE332S 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.
|
||||
Reference in New Issue
Block a user