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

12 KiB
Raw Permalink Blame History

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.

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

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

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

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)

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.

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)

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.

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.