Essential C++ # 05. Object-Oriented Programming
Chapter 5. Object-Oriented Programming
The object-based programming model proves cumbersome when our application begins to be filled with class types that represent an is-a-kind-of instance of a type.
5.1. Object-Oriented Programming Concepts
📌Primary Characteristics of Object-Oriented Programming⭐
- Inheritance
- Polymorphism
Inheritance allows us to group classes into families of related types, allowing for the sharing of common operations and data.
Polymorphism allows us to program these families as a unit rather than as individual classes, giving us greater flexibility in adding or removing any particular class.
📌Calling Convention
The parent is called the base class.
The child is called the derived class.
📌Abstract Base Class
The root of the class hierarchy is an abstract base class.
[IMPORTANT!!!]⭐⭐⭐In an object-oriented program, we indirectly manipulate the class objects of our application through a pointer or reference of an abstract base class rather than directly manipulate the actual derived class objects of our application.
void loan_check_in(LibraryMaterial &mat)
{
// mat here actually refers to a derived class object
// such as Book, RentalBook, Magazines, and so on...
mat.check_in();
if(mat.is_late())
{
mat.assess_fine();
}
if(mat.waiting_list())
{
mat.notify_available();
}
}
📌Key to Polymorphism and Dynamic Binding
Polymorphism and Dynamic Binding are supported ONLY when we are using a pointer or reference.
5.2. A Tour of Object-Oriented Programming
📌Example of LibraryMaterial
Base class:
x
using namespace std;
class LibraryMaterial
{
private:
public:
LibraryMaterial();
~LibraryMaterial();
// 🤚 virual function
virtual void print() const;
};
LibraryMaterial::LibraryMaterial()
{
cout << "LibraryMaterial::LibraryMaterial() default constructor!" << endl;
}
LibraryMaterial::~LibraryMaterial()
{
cout << "LibraryMaterial::~LibraryMaterial() default destructor!" << endl;
}
// 🤚 when implement virual function,
// you don't need to address the `virtual` again
void LibraryMaterial::print() const
{
cout << "LibraryMaterial::print() -- I am a LibraryMaterial Object." << endl;
}
Derived class:
xxxxxxxxxx
using namespace std;
class Book : public LibraryMaterial
{
protected:
string _author;
string _title;
public:
Book(const string& title, const string& author);
~Book();
// 🤚virtual function here!
virtual void print() const;
const string& author() const {return _author;}
const string& title() const {return _title;}
};
Book::Book(const string& title, const string& author)
:_title(title), _author(author)
{
cout << "Book::Book("
<< _title << ", "
<< _author << ") constructor." << endl;
}
Book::~Book()
{
cout << "Book::~Book() default destructor!" << endl;
}
// 🤚 when implement virual function,
// you don't need to address the `virtual` again
void Book::print() const
{
cout << "Book::print() -- I am a Book Object." << endl;
cout << "My title is: " << _title << endl;
cout << "My author is: " << _author << endl;
}
Derived class of derived class:
xxxxxxxxxx
class AudioBook : public Book
{
private:
string _narrator;
public:
AudioBook(const string& title, const string& author, const string& narrator);
~AudioBook();
// 🤚virtual function here
virtual void print() const;
const string& narrator() const {return _narrator;}
};
// 🤚Take a look at the parameter list!!
// It can combine base class constructor and other member variables
AudioBook::AudioBook(const string& title, const string& author, const string& narrator)
: Book(title, author), _narrator(narrator)
{
cout << "AudioBook::AudioBook("
<< _title << ", "
<< _author << ", "
<< _narrator << ") constructor." << endl;
}
AudioBook::~AudioBook()
{
cout << "AudioBook::~AudioBook() default destructor!" << endl;
}
// 🤚 when implement virual function,
// you don't need to address the `virtual` again
void AudioBook::print() const
{
cout << "AudioBook::print() -- I am an AudioBook object!\n"
<< "My title is: " << _title << "\n"
<< "My author is: " << _author << "\n"
<< "My narrator is: " << _narrator << endl;
}
//TODO - for const string& narrator() const {return _narrator;}
why the return type is const string&
, what is the 2nd const
?
5.3. Polymorphism without Inheritance
📌static_cast<T>
to cast A
to B
xxxxxxxxxx
enum Format
{
TEXT = 0,
PDF = 1000,
OTHER = 2000
};
Format f = Format::PDF;
int a = f; // ❌ERROR! Cannot cast directly in C++
int b = static_cast<int>(f); // ✅OK. Can cast to 1000.
5.4. Defining an Abstract Base Class
📌Standard Procedure Design an Abstract Base Class
1️⃣ The first step is to identify the set of operations common to its children.
Therefore, we can make the following:
xxxxxxxxxx
class num_sequence
{
private:
public:
// elem(pos): return element at pos
// gen_elems(pos): generate the elements up to pos
// what_am_i(): identify the actual sequence
// print(os): write the element to os
// check_integrity(pos): is pos a valid value?
// mmax_elems(): returns maximum position supported
int elem(int pos);
void gen_elems(int pos);
const char* what_am_i() const;
ostream& print(ostream &os = cout) const;
bool check_integrity(int pos);
static int max_elems();
// ...
num_sequence();
~num_sequence();
};
2️⃣ The next step is to identify which operations are type-dependent[^6] - that is, which operations require separate implementations based on the derived class type.
xxxxxxxxxx
class num_sequence
{
private:
public:
virtual int elem(int pos) const = 0;
virtual void gen_elems(int pos) = 0;
virtual const char* what_am_i() const = 0;
virtual ostream& print(ostream &os = cout) const = 0;
bool check_integrity(int pos) const;
const static int _max_elems = 1024;
static int max_elems() { return _max_elems; }
// ...
num_sequence();
~num_sequence();
};
Those operations required separate implementation must declare with virtual
. What's more, if there is no meaningful implementation of that function at current base class, must be declared as a pure virtual function[^7]. e.g.
xxxxxxxxxx
virtual void gen_elems(int pos) = 0;
For function that are common to all derived classes, no need to specify with virtual
. e.g. The function which returns the maximum length of sequence.
xxxxxxxxxx
static int max_elems() { return _max_elems; }
Also, please take into account that: "A static
member function cannot be declared as virtual
".
3️⃣ The third step is to identify the access level of each operation.
This is very similar to C# which has private
, protected
, and public
.
xxxxxxxxxx
class num_sequence
{
protected:
virtual void gen_elems(int pos) const = 0;
bool check_integrity(int pos) const;
const static int _max_elems = 1024;
public:
virtual ~num_sequence() {};
virtual int elem(int pos) const = 0;
virtual const char* what_am_i() const = 0;
virtual ostream& print(ostream &os = cout) const = 0;
static int max_elems() { return _max_elems; }
// ...
};
The derived class of num_sequence
can access members declared with protected
. While you cannot access them outside of this class.
📌Why class
with pure virtual function is called "abstract" class?
Because its interface is incomplete, a class that declares one or more pure virtual functions cannot have independent class objects defined in the program. It can only serve as the subobject of its derived classes.[^8]
📌Design of constructor and destructor in Abstract Base Class⭐
For Constructor
Why there is no constructor in num_sequence
?🤔 There are no nonstatic data members within the class to initialize, therefore there is no real benefit to providing a constructor.
For Destructor
As a general rule, a base class that defines one or more virtual
functions should always define a virtual
destructor.
xxxxxxxxxx
num_sequence *ps = new Fibonacci(21);
// ... use the sequence
delete ps;
Mechanism Behind[IMPORTANT!!!]⭐: ps
is a num_sequence
base class pointer, but it addresses a Fibonacci-derived class object. When the delete
expression is applied to a pointer to a class object, the destructor is first applied to the object addressed by the pointer. Then the memory associated with the class object is returned to the program's free store. In this case, the destructor invoked through ps
must be the Fibonacci class destructor and not the destructor of the num_sequence
class. That is, which destructor to invoke must be resolved at run-time based on the object actually addressed by the base class pointer. Therefore, we must declare the destructor virtual
.[^9]
📌Recommended Way to Define a virtual
Destructor👍
xxxxxxxxxx
inline num_sequence::~num_sequence() {}
5.5. Defining a Derived Class
📌The Composite of a Derived Class
The derived class consists of 2 parts:
- the subobject of its base class (e.g. the nonstatic base class data members)
- the derived class portion (e.g. the nonstatic derived class data members)
xxxxxxxxxx
class Fibonacci : public num_sequence
{
public:
// ...
};
📌Implement a Derived Class
xxxxxxxxxx
class Fibonacci : public num_sequence
{
public:
Fibonacci(int length = 1, int beg_pos = 1)
:_length(length), _beg_pos(beg_pos) {}
virtual int elem(int pos) const;
virtual const char* what_am_i() const {return "Fibonacci";}
virtual ostream& print(ostream &os = cout) const;
int length() const {return _length;}
int beg_pos() const {return _beg_pos;}
~Fibonacci();
protected:
virtual void gen_elems(int pos) const;
int _length;
int _beg_pos;
static vector<int> _elems;
};
A derived class MUST provide an implementation of each of the pure virtual functions inherited from its base class.⭐
See the following user code:
xxxxxxxxxx
// ✅OK
num_sequence *ns_ptr = new Fibonacci(12, 8);
// ✅OK
ns_ptr->what_am_i();
// ✅OK
ns_ptr->max_elems();
// ❌ERROR! Because the `length()` is not an interface defined in `num_sequence`
ns_ptr.length();
delete ns_ptr;
Later then, we have 2 options to retrofit our class:
- define
length()
asvirtual
function in base class - define
length()
as member function in base class
Either way, we have to declare length()
interface in base class. In real world design, this is an iterative process that evolves through experience and feedback from users.
📌virtual
keyword needn't show up again in .cpp
xxxxxxxxxx
/***************Fibonacci.h***************/
class Fibonacci : public num_sequence
{
public:
// ...
protected:
virtual void gen_elems(int pos) const;
// ...
};
Suppose you have the preceding member function, you can define in .cpp
without virtual
keyword again. This is a kind of different from C#.
xxxxxxxxxx
/***************Fibonacci.cpp***************/
void Fibonacci::gen_elems(int pos) const
{
// implement here
}
📌Explicit definition can speed up compile-time
Suppose you want to implement the elem(int pos)
function in Fibonacci
class.
xxxxxxxxxx
int Fibonacci::elem(int pos) const
{
if (!check_integrity(pos))
{
return 0;
}
if(pos > _elems.size())
{
Fibonacci::gen_elems(pos);
}
return _elems[pos - 1];
}
You may ask,🤔 gen_elems()
has been declared in the base class, why do you explicitly add Fibonacci::
as prefix? Because you are implementing the Fibonacci::elem
, so you do know you will use Fibonacci::gen_elems
rather than asking compiler to figure out. Hence, it can speed up the whole process!!
📌Duplicate Function Name without virtual
Suppose we have following 2 functions with same name but without specifying virtual
.
xxxxxxxxxx
/***************num_sequence.h***************/
class num_sequence
{
protected:
bool check_integrity(int pos) const;
public:
// ...
};
/***************Fibonacci.h***************/
class Fibonacci : public num_sequence
{
protected:
bool check_integrity(int pos) const;
public:
// ...
};
When you type the following:
xxxxxxxxxx
ps -> check_integrity(pos);
Whenever a member of the derived class reuses the name of an inherited base class member, the base class member becomes lexically hidden within the derived class. Therefore, if you want to use the base class member function, you have to explicitly declare it.
xxxxxxxxxx
inline bool Fibonacci::
check_integrity(int pos) const
{
// 🤚 Explicitly invoke the `check_integrity()` from the base class
if(! num_sequence::check_integrity(pos))
{
return false;
}
if(pos > _elems.size())
{
Fibonacci::gen_elems(pos);
}
return true;
}
You can invoke the check_integrity()
of base class by num_sequence::check_integrity(pos)
. In C#, you can use the base
keyword, like so:
xxxxxxxxxx
base.check_integrity();
📌Function Design Rule
With preceding code, we conclude that: "It is not a good practice, in general, to provide nonvirtual member functions with the same name in both the base and derived class."
5.6. Inheritance Hierarchy
There is nothing special in this section, few things I learned from the following code:
xxxxxxxxxx
class num_sequence
{
protected:
// ...
public:
friend ostream& operator<<(ostream &os, const num_sequence &ns)
{
return ns.print(os);
}
// ...
};
📌The Use of friend
I understand better what a friend
means. Since operator<<
requires a parameter which is an instance of num_sequence
, but this block of code is also inside of the class num_sequence
. Therefore, the num_sequence
is a fresh element has not been declared yet. The friend
keyword is to solve this problem.
📌Put the virtual
function in a nonvirtual function
To prevent the derived class override both operator<<
and print()
, the designer put the virtual
print()
inside the nonvirtual operator<<
. Smart.
5.7. How Abstract Should a Base Class Be?
📌What is the answer to this question?
In short, there is no absolute correct answer. This refers to Software Development.
📌Benefit of a Reference Member in a Base Class
A reference data member must be initialized within the constructor's member initialization list and, once initialized, can never be changed to refer to a different object.
📌Retrofit the num_sequence
class
With preceding theory, we can have the following:
xxxxxxxxxx
/********num_sequence.h********/
class num_sequence
{
protected:
int _length;
int _beg_pos;
vector<int> & _relems; // 🤚Take a look here!
virtual void gen_elems(int pos) const = 0;
bool check_integrity(int pos, int size) const;
public:
virtual ~num_sequence() {};
virtual const char* what_am_i() const = 0;
int elem(int pos) const;
ostream& print(ostream &os = cout) const;
int length() const {return _length;}
int beg_pos() const {return _beg_pos;}
static int max_elems() { return 64; }
};
/********Fibonacci.h********/
class Fibonacci : public num_sequence
{
protected:
static vector<int> _elems; // Take a look here!🤚
virtual void gen_elems(int pos) const;
public:
Fibonacci(int len = 1, int beg_pos = 1);
virtual ~Fibonacci() {};
virtual const char* what_am_i() const {return "Fibonacci";}
};
5.8. Initialization, Destruction and Copy
📌Good Practice of Initialization
A good practice will be a base class constructor initialize the members belong to base class. Then in the derived class, just use it in member initialization list.
5.9. Defining a Derived Class Virtual Function
📌The Declaration must match
Not match example❌
xxxxxxxxxx
class num_sequence
{
public:
virtual const char* what_am_i() const { return "num_sequence\n"; }
};
class Fibonacci
{
public:
virtual const char* what_am_i() { return "Fibonacci\n"; }
}
Not match but also error example❌
xxxxxxxxxx
class num_sequence
{
public:
virtual const char* what_am_i() const { return "num_sequence\n"; }
};
class Fibonacci
{
public:
virtual char* what_am_i() { return "Fibonacci\n"; }
}
match example✔
xxxxxxxxxx
class num_sequence
{
public:
virtual const char* what_am_i() const { return "num_sequence\n"; }
};
class Fibonacci
{
public:
virtual const char* what_am_i() const { return "Fibonacci\n"; }
}
match example but with different declaration✔
xxxxxxxxxx
class num_sequence
{
public:
virtual const num_sequence* clone() = 0;
};
class Fibonacci
{
public:
virtual const Fibonacci* clone() { return new Fibonacci(*this); }
}
📌virtual
function of derived class never invoked in base constructor
It is very easy to understand since the derived class hat not yet been initialized how can it offers help in its base class constructor.
📌Polymorphism only via reference and pointer
In C++, only pointers and references of the base class support object-oriented programming.
5.10. Run-Time Type Identification
The run-time type identification is very much the same as the System.Reflection
in C#.
📌Return the name of such class
xxxxxxxxxx
inline const char* num_sequence::
what_am_i() const
{
return typeid(*this).name();
}
📌Check if the type is correct
xxxxxxxxxx
num_sequence *ps = &fib;
// ...
if (typeid(*ps) == typeid(Fibonacci))
{
// ...
}
The preceding syntax is exactly the same as in C#.
📌static_cast
in C++
xxxxxxxxxx
if (typeid(*ps) == typeid(Fibonacci))
{
// the compiler does not confirm the conversion is correct
Fibonacci *pf = static_cast<Fibonacci*>(ps);
pf->gen_elems(64);
}
📌dynamic_cast
in C++
xxxxxxxxxx
// this is much safer which verifies the conversion
if(Fibonacci *pf = dynamic_cast<Fibonacci*>(ps))
{
pf->gen_elems(64);
}