Files
NoteNextra-origin/content/CSE332S/CSE332S_L12.md
2025-09-17 14:27:46 -05:00

428 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 objects 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 objects 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 superclasss interface and its implementation of that interface.
- Function overriding - subclasses may override the superclasss 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 its 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 its 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.