Inheritance
- One of the important features of C++ is code reuse. Through the inheritance mechanism, new data types can be defined using existing data types. The new data type not only has the members of the old class but also has new members.
- Class B inherits from class A, also known as deriving class B from class A, where class A is the base class and class B is the derived class.
- The inherited members exhibit their commonality, while the new members reflect their individuality.
- Private inheritance means that inherited members become private members of the subclass. The default inheritance modifier is private.
- Protected inheritance means that inherited members become protected members of the subclass.
- Public inheritance means that inherited members retain their access level from the base class in the subclass.
- All private attributes of the base class are inaccessible in the subclass.
- Order of subclass constructor calls:
- Base class constructor -> Subclass member constructor -> Subclass constructor -> Subclass destructor -> Subclass member destructor -> Base class destructor
- When instantiating a subclass object, the default constructors of member objects and base class objects are automatically called.
- The subclass must use an initializer list to call the parameterized constructor of the member objects and the base class.
- In the initializer list, the base class is referred to by its class name, and member objects are referred to by their object names.
- When subclass members and base class members have the same name:
- The default access is to its own members.
- To access base class members, the scope resolution operator must be used.
- Redefinition
- The subclass can redefine functions with the same name as those in the base class, with different parameters, for non-virtual functions.
- Once a subclass redefines a function with the same name as a base class function, regardless of whether the parameters are the same, all functions with the same name in the base class will be hidden in the subclass. The scope resolution operator can be used to access them.
- Functions that cannot be inherited:
- Constructors
- Destructors
- Operator overload functions
class Parent {
int a;
public:
Parent() {}
Parent(int a) {
this->a = a;
}
void fn() {}
};
class Son : public Parent {
int sa;
public:
Son() {}
Son(int a) : Parent(a) {
sa = a;
}
void fn() {}
};
Son sonObj;
sonObj.Parent::fn(); // Call base class method when names are the same
- Multiple Inheritance
- In C++, a class can inherit from one or multiple classes, separated by commas, which is called multiple inheritance.
- Inheriting from multiple classes may lead to many name clashes for functions and variables.
class Parent1 {};
class Parent2 {};
class Son : public Parent1, protected Parent2 {};
- Diamond Inheritance
- Inheritance with a common ancestor is called diamond inheritance.
- The lowest subclass contains multiple copies of the common ancestor’s data.
class A {};
class B : public A {};
class C : public A {};
class D : public B, protected C {};
- Virtual Inheritance
- This is used to solve the problem of multiple copies of data that can occur in diamond inheritance.
- The keyword is virtual.
- In the subclass, only one copy of the common ancestor’s data will be stored.
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, protected C {};
Polymorphism
- Polymorphism is the third fundamental feature of object-oriented programming languages, alongside data abstraction and inheritance. Polymorphism provides another layer of separation between interfaces and implementations, thus separating what and how. It improves code readability and organization, while also making the created programs extensible.
- Static polymorphism (compile-time polymorphism, early binding): function overloading, operator overloading, redefinition.
- Dynamic polymorphism (runtime polymorphism, late binding): virtual functions.
Virtual Functions
- A base class pointer/reference can hold the address of a subclass object.
- It is marked with the keyword virtual.
- The dynamic binding mechanism of virtual functions:
- If a member function of a class is marked as virtual, then that function is a virtual function, and the class will generate a virtual function pointer (vfptr) that points to a virtual function table (vftable). If this class is not inherited, the virtual function table will record the entry addresses of the virtual functions.
- If the class is inherited, the subclass will also inherit the addresses of the base class’s virtual functions. If the subclass overrides a base class’s virtual function, then during instantiation, the entry address of the subclass’s virtual function will replace that in the virtual function table. Thus, when the virtual function is called, the entry address found in the virtual function table will be that of the subclass’s overridden function, indirectly calling the subclass’s function.
class Parent {
public:
virtual void fn(int n) { // 1 Add virtual before member function
cout << "parent" << n << endl;
}
};
class Son1 : public Parent {
public:
// virtual can be omitted, return type, function name, and parameters must all match the base class
virtual void fn(int n) { // 2 Redefine the same-named function in the subclass
cout << n << "son 1" << endl;
}
};
class Son2 : public Parent {
public:
virtual void fn(int n) {
cout << n << "son 2" << endl;
}
};
Parent * op = new Son1; // Base class pointer holds subclass address, achieving personalized functionality through virtual functions
Parent * op2 = new Son2;
void testFn(Parent * p) {
p->fn(100);
}
testFn(op); // 100 son 1
testFn(op2); // 100 son 2
Pure Virtual Functions
- If a base class is guaranteed to have derived subclasses, and those subclasses must override the base class’s virtual functions, then the base class’s virtual function can be declared without a function body, known as a pure virtual function.
- A class with pure virtual functions is an abstract class and cannot be instantiated directly.
- The main purpose of an abstract class is to design the interface for subclasses.
- Subclasses of an abstract class must override all pure virtual functions of the base class.
class Base {
public:
virtual void fn(int a) = 0; // Use =0 to replace function body
};
Virtual Destructors
- Used to release the space of a subclass through a base class pointer.
- Pure virtual destructor:
- The essence of a pure virtual destructor is a virtual destructor that completes the cleanup of various classes, and destructors cannot be inherited.
- A pure virtual destructor must have a function body defined outside the class.
- A class with a pure virtual destructor is also an abstract class.
class Parent {
public:
virtual ~Parent() {
cout << "Parent destructor" << endl;
}
};
class Son : public Parent {
public:
~Son() {
cout << "Son destructor" << endl;
}
};
Parent * p = new Son;
delete p; // If the base class destructor is not marked as virtual, it will only release the memory occupied by the base class members, calling only the base class destructor. With virtual, it can release all memory, calling both base and subclass destructors.
// Pure virtual destructor
class Parent2 {
public:
virtual ~Parent() = 0;
};
Parent2::~Parent() {
cout << "Parent2 destructor" << endl;
}
Templates
- There are two programming paradigms in C++:
- Object-oriented programming
- Generic programming (templates)
Overview of Templates
- C++ provides function templates, which essentially create a function where the function type and parameter types are not specifically defined, using a virtual type to represent them. This generic function is called a function template. Any function with the same body can be replaced by this template, eliminating the need to define multiple functions. During function calls, the system will replace the virtual type in the template with the actual parameter types, thus achieving different functionalities. C++ provides two template mechanisms: function templates and class templates.
Function Templates
- Keyword template
- Function templates are compiled twice.
- Compile the function template itself.
- At the function call site, the virtual type is concretized.
The goal of function templates is to achieve generics, reducing programming workload and enhancing function reusability.
Points to note:
- When both function templates and regular functions are recognized, the regular function is prioritized.
- When both function templates and regular functions are recognized,
<> can be added to force the use of the function template.
- When automatically deducing types in function templates, implicit type conversion cannot be performed on function parameters.
Function templates can be overloaded.
template <typename T> void fn(T &a, T &b) {}
fn(1, 2); // Automatic type deduction
fn('a', 'b'); // Automatic type deduction
template <typename T> // T can only be one type at a time
void fn2(T &a, T &b) {} // Template function
void fn2(int &a, int &b) {} // Regular function
fn2(1, 2); // Regular function
fn2<>(1, 2); // Force use of function template
fn2<int>(1, 2); // Force use of function template, explicitly declaring generic as int type
fn2(1, 'a'); // Regular function
fn2<>(1, 'a'); // Recognized as regular function, function template cannot perform implicit type conversion, T can only be one type at a time
fn2<int>(1, 'a'); // Function template, effectively forcing type conversion of the parameter
- Limitations of Function Templates
- When the template function deduces the virtual type as an array or other custom data types, it may lead to unrecognized operators.
- Solutions:
- Operator overloading
- Specializing function templates
class CA {
friend ostream & operator<<(ostream & out, CA ob);
private:
int num;
public:
CA() {}
CA(int num) {
this->num = num;
}
};
friend ostream & operator<<(ostream & out, CA ob) {
out << ob.num << endl;
return out;
}
template <typename T>
void myPrint(T a) {
cout << a << endl;
}
// template <> void myPrint<CA>(CA a) { // Specializing function template, when the parameter is of type CA, this function's logic is executed
// cout << a.num << endl;
// }
myPrint(10); // 10
CA caObj(100);
myPrint(caObj); // 100
Class Templates
- Sometimes there are two or more classes with the same functionality but different data types. Class templates are used to parameterize the data types required for declaring classes.
- Class templates support automatic type deduction when instantiating objects, but the specific type of the virtual type must be manually specified.
- When the declaration and definition of class templates are separated (e.g., definition in xxx.cpp file and declaration in xxx.h file), both files need to be included to use them in other files. In development, it is common to write the declaration and definition of class templates in one file with a .hpp suffix, allowing direct inclusion with
#include <xxx.hpp>.
template <class T, class T2> class CA {
private:
T a;
T2 b;
public:
CA() {}
CA(T a, T b) {
this->a = a;
this->b = b;
}
void fn(T a, T2 b);
};
CA<int, char> caObj(10, 'a');
- When member functions of class templates are implemented outside the class, they need to be separately declared as templates because the template only applies to the nearest statement.
template <class T, class T2> class CA {
private:
T a;
T2 b;
public:
CA() {}
CA(T a, T b);
void fn(T a, T2 b);
};
template <class T, class T2>
CA<T, T2>::CA(T a, T b) {
this->a = a;
this->b = b;
}
template <class T, class T2>
void CA<T, T2>::fn(T a, T2 b) {
cout << a << b << endl;
}
- Function templates as friends of class templates
template <class T, class T2>
class CA {
template <class T3, class T4>
friend void mySprint(CA<T3, T4> &ob);
private:
T a;
T2 b;
public:
CA() {}
CA(T a, T b) {
this->a = a;
this->b = b;
}
};
template <class T3, class T4>
void mySprint(CA<T3, T4> &ob) {
cout << ob.a << ob.b << endl;
}
CA<int, char> caObj(10, 'a');
mySprint(caObj);
- Regular functions as friends of class templates must specify concrete types.
template <class T, class T2>
class CA {
friend void mySprint(CA<int, char> &ob);
private:
T a;
T2 b;
public:
CA() {}
CA(T a, T b) {
this->a = a;
this->b = b;
}
};
void mySprint(CA<int, char> &ob) {
cout << ob.a << ob.b << endl;
}
CA<int, char> caObj(10, 'a');
mySprint(caObj);
// You can practice creating custom array class templates when you have time.
- Inheritance of Class Templates
template <class T, class T2>
class Parent {};
// Class template derived from a regular class, type must be specified
class Son : public Parent<int, char> {};
// Class template derived from another class template
template <class K, class K2, class K3>
class Son2 : public Parent<K, K2> {
private:
K1 pa;
K2 pb;
K3 pc;
};