C++ polymorphism, virtual function, pure virtual function, abstract class

The concept of polymorphism
Generally speaking, it is a variety of forms. The specific point is to complete a certain behavior. When different objects complete it, different states will be produced.
To give a simple example: grabbing a red envelope, each of us only needs to click on the red envelope to grab the amount. Some people can grab dozens of yuan, while some people can only grab a few yuan or even a few cents. It also shows that different people do the same thing, but the results are different. This is polymorphism.

There are two types of polymorphism in C++, one is static polymorphism and the other is dynamic polymorphism;

Static polymorphism: Function overloading, it seems that calling the same function has different behaviors. Static: The principle is implemented at compile time.

Dynamic polymorphism: When a parent class reference or pointer calls the same function, passing different objects will call different functions. Dynamic: The principle is implemented at runtime.

1. Foreword

Polymorphism literally means multiple forms. Polymorphism is used when there is a hierarchy between classes and classes are related through inheritance. C++ polymorphism means that when a member function is called, different functions are executed depending on the type of the object on which the function is called. In the following example, the base class Shape is derived into two classes, as follows:

#include <iostream>
using namespace std;

classShape {
public:
    void area()
    {
        cout << "Parent class area :" << endl;
    }
};
class Rectangle : public Shape {
public:
    void area()
    {
        cout << "Rectangle class area:" << endl;
    }
};
class Triangle : public Shape {
public:
    void area()
    {
        cout << "Triangle class area :" << endl;
    }
};

void func(Shape & amp; p) {
    p.area();
}
//The main function of the program
int main()
{
    Rectangle Rec;
    //Call the area function of the rectangle area
    func(Rec);


    Triangle Tri;
    //Call the triangle area function area
    func(Tri);

    return 0;
}

When the above code is compiled and executed, it produces the following results:

Parent class area:
Parent class area:

The reason for the error output is that the calling function area() is set by the compiler to the version in the base class, which is called static polymorphism, or static linking – function The call is prepared before the program is executed. This is sometimes called early binding because the area() function is set during program compilation.

But now, let us modify the program slightly. In the Shape class, place the keyword virtual before the declaration of area(), leaving the rest unchanged, as follows:

#include <iostream>
using namespace std;

classShape {
public:
   virtual void area()
    {
        cout << "Parent class area :" << endl;
    }
};
class Rectangle : public Shape {
public:
    void area()
    {
        cout << "Rectangle class area:" << endl;
    }
};
class Triangle : public Shape {
public:
    void area()
    {
        cout << "Triangle class area :" << endl;
    }
};

void func(Shape & amp; p) {
    p.area();
}
//The main function of the program
int main()
{
    Rectangle Rec;
    //Call the area function of the rectangle area
    func(Rec);


    Triangle Tri;
    //Call the triangle area function area
    func(Tri);

    return 0;
}

After modification, when the previous example code is compiled and executed, it produces the following results:

Rectangle class area:
Triangle class area:

At this point, the compiler is looking at the contents of the pointer, not its type. Therefore, since the addresses of objects of class tri and rec are stored in *shape, the respective area() functions are called.

As you can see, each subclass has an independent implementation of the function area(). This is how polymorphism is generally used. With polymorphism, you can have several different classes, all with functions with the same name but different implementations, and the parameters of the functions can even be the same.

2. Definition and implementation of polymorphism

1.Conditions for polymorphism

There are two conditions for polymorphism in inheritance:

(1) Virtual functions must be called through pointers or references of the base class.

(2) The called function must be a virtual function, and the derived class must override the virtual function of the base class.

2. Virtual function

A virtual function is a function declared in a base class using the keyword virtual. When you redefine a virtual function defined in a base class in a derived class, you tell the compiler not to statically link to the function.
What we want is that at any point in the program, we can choose the function to be called based on the type of object being called. This operation is called dynamic linking, or late binding .

Once a virtual function is defined, the function with the same name in the derived class of the base class automatically becomes a virtual function. That is to say, there is a function in the derived class with the same name as the base class. As long as the base class is modified with virtual modification, the derived class without virtual modification is also a virtual function. A virtual function can only be a member function in a class, not a static member or ordinary function.

Note: In order to solve the problem of data redundancy and ambiguity in inheritance, we need to use virtual inheritance. The keyword is also virtual, which has nothing to do with virtual in polymorphism.

3. Rewriting of virtual functions

Rewriting (overwriting) of virtual functions: There is a virtual function in the derived class that is exactly the same as the base class (that is, the return value type, function name, and parameter list of the derived class virtual function and the base class virtual function Identical), it is said that the virtual function of the subclass overrides the virtual function of the base class.

By rewriting virtual functions, polymorphism can be achieved:

#include<iostream>
using namespace std;

//Buy tickets
class Person
{
public:
virtual void BuyTicket() { cout << "Buy ticket - full price" << endl; }
};

//Students buy tickets
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "Buy ticket - half price" << endl; }
};

//Soldiers buy tickets
class Soldier : public Person
{
public:
void BuyTicket() { cout << "Priority-buy tickets-half price" << endl; }

};

//To form polymorphism, which type of object is passed, the virtual function of this type is called --- related to the object
//Does not constitute polymorphism, the call is the type of P --- It depends on the type
void Func(Person & amp; p) //or void Func(Person* p)
{
p.BuyTicket(); //p->BuyTicket();
}

int main()
{
Person ps;
Func(ps); //If you buy a ticket without any identity, it must be full price

Student st;
Func(st); //Buying tickets as a student is half price

Soldier so;
Func(so); //Buying tickets as a soldier is priority and half price

return 0;
}

4. Two exceptions to virtual function rewriting

(1). Covariance (the return value types of base class and derived class virtual functions are different)

When a derived class overrides a base class virtual function, the return value type is different from the base class virtual function. That is, when a base class virtual function returns a pointer or reference to a base class object, and a derived class virtual function returns a pointer or reference to a derived class object, it is called covariance.

Another explanation:

Covariance in C++ means that the return type of a derived class can be a subtype of the return type of a base class function. When a derived class inherits a base class and overrides a virtual function in the base class, covariance can be used to change the return type.

Specifically, if the return type of a base class function is a pointer or reference, then when the function is overridden in a derived class, the return type can be a derived type of the type pointed to or referenced by the base class return type.

The following conditions must be met to achieve covariance:

  • Functions in base classes must be virtual (declared using the virtual keyword).
  • Overridden functions in derived classes must have the same function signature (function name, parameter list, and constancy).
  • The return type of a function overridden in a derived class must be a subtype of the return type of the base class function.

Example:

Quoted from: C++ covariant-CSDN blog

Suppose there is a base class Animal and two derived classes Dog and Cat. There is a virtual function makeSound() in the Animal class, which returns a pointer to the Animal object. In a derived class Dog, you can override the makeSound() function and return a pointer to the Dog object. Likewise, the makeSound() function can be overridden in a derived class Cat and return a pointer to the Cat object.

#include <iostream>
class Animal {
public:
    virtual Animal* makeSound() {
        std::cout << "Animal makes a sound." << std::endl;
        return this;
    }
};
class Dog : public Animal {
public:
    virtual Dog* makeSound() {
        std::cout << "Dog barks." << std::endl;
        return this;
    }
};
class Cat : public Animal {
public:
    virtual Cat* makeSound() {
        std::cout << "Cat meows." << std::endl;
        return this;
    }
};
int main() {
    Animal* animal;
    Dog dog;
    Cat cat;
    animal = &dog;
    animal->makeSound(); // Output: "Dog barks."

    animal = &cat;
    animal->makeSound(); // Output: "Cat meows."
    return 0;
}

The difference between covariance and polymorphism:

Covariance and polymorphism in C++ are closely related. Polymorphism means that the same function can behave differently when called on different objects.

In C++, the syntax mechanism of runtime polymorphism is realized through the use of virtual functions. Virtual functions in the base class can be overridden (overridden) in the derived class. When the derived class object calls the virtual function, which virtual function is called will be determined based on the actual type of the object.

Covariance means that a derived class can change the return type inherited from a base class function, so that the return type becomes a derived type of the type pointed to or referenced by the base class return type.

By combining covariance and polymorphism, we can override the virtual function of the base class in the derived class and return a type specific to the derived class. This allows us to use more specific return types in derived classes in the case of polymorphism.

(2). Rewriting of destructors (The names of destructors of base classes and derived classes are different)

If the destructor of the base class is a virtual function, as long as the destructor of the derived class is defined, regardless of whether the virtual keyword is added, it will be overridden with the destructor of the base class. Although the destructor names of the base class and the derived class different. Although the function names are different, it seems to violate the rewriting rules. In fact, it is not the case. It can be understood that the compiler has done special processing for the name of the destructor. After compilation, the name of the destructor is unified into destructor.

In C++, a destructor is a special member function that performs cleanup work when an object is destroyed. Normally, the destructor is automatically generated by the compiler and executes the destructor of the object’s member variables and base class by default.

When additional cleanup work or resource release is required for a derived class, this can be achieved by overriding the destructor of the base class.

Overriding destructors in derived classes requires following the following rules:

  1. The function name is exactly the same as the destructor of the base class.
  2. The parameter list is empty.
  3. The return type is void.
  4. You can add the override keyword (optional) to explicitly indicate that you are overriding the base class’s destructor.

Here is a sample code:

class Base {
public:
    virtual ~Base() {
        //Destructor of base class
    }
};

class Derived : public Base {
public:
    ~Derived() override {
        // The destructor of the derived class overrides the destructor of the base class
    }
};

In the above code, the base class Base defines a virtual destructor, and the derived class Derived implements its own cleanup logic by overriding the destructor of the base class.

It should be noted that in an inheritance relationship, if the destructor of the base class is a virtual function, the destructor of the derived class should also be declared as a virtual function. In this way, when a base class pointer or reference is used to point to a derived class object, and the destructor is called through the pointer or reference, the destructor of the derived class can be correctly called.

In summary, additional cleanup work or resource release can be achieved by overriding the base class’s destructor in the derived class. Overriding destructors requires following specific rules, and it is recommended to declare the base class’s destructor as a virtual function.

Another explanation:

(Quoted from: C++ Polymorphism (1): Polymorphic conditions, final, override, covariance, destructor rewriting, abstract class_c++ Polymorphism override-CSDN Blog)

Although the destructors have different function names, they can still constitute overriding, becausefrom the compiler’s perspective, the names of the destructors it calls are all called destructor.

Why does the compiler use this method to allow the destructor to also constitute an override?

Suppose I use a base class pointer or reference to point to a derived class object. What will happen if it does not constitute polymorphism?

class Human
{
public:
~Human()
{
cout << "~Human()" << endl;
}
};

class Student : public Human
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};

int main()
{
Human* h = new Student;
delete h;

return 0;
}

Output results:

~Human()

Analysis:

The above code will only call the destructor of the Human class. That is, if it does not constitute polymorphism, then what type of pointer is will determine what type of destructor will be called. This also leads to a situation where if the destructor of the derived class There are resources released in the constructor, but those resources are not released here, which will lead to memory leaks.

So in order to prevent this situation, the destructor must be defined as a virtual function. This is why the compiler renamed the destructor to destructor.

class Human
{
public:
virtual ~Human()
{
cout << "~Human()" << endl;
}
};

class Student : public Human
{
public:
virtual ~Student() // The virtual keyword can be omitted
{
cout << "~Student()" << endl;
}
};

int main()
{
Human* h = new Student;
delete h;

return 0;
}

Output result:

~Student()
~Human()

5.C++11 override and final

As can be seen from the above, C++ has strict requirements for function rewriting. However, in some cases, due to negligence, the function name may be written in alphabetical order but cannot constitute an overload. This error will not be reported during compilation. Otherwise, debugging only when the program does not get the expected results when running will not be worth the gain. Therefore: C++11 provides two keywords, override and final, which can help users detect whether to rewrite.

(1) final

In the C++11 standard, final is a keyword that disables inheritance and overriding of virtual functions of a class. When a class or a member function of a class is declared as final, it means that it can no longer be inherited by other classes or that its virtual functions cannot be overridden by derived classes.

The benefits of using the final keyword are:

  1. Can enhance the security of the code: using the final keyword can prevent inappropriate inheritance and overwriting.
  2. Readability: Using the final keyword can enhance the readability and maintainability of the code and clarify the intent of the class or function.

Final: Modifies a virtual function, indicating that the virtual function can no longer be overridden.

#include<iostream>
class Car
{
public:
virtual void Drive() final
{}
};

class Benz: public car
{
public:
virtual void Drive() override //Check whether rewriting is completed
{
std::cout << "Benz-Comfortable" << std::endl;
}
};
int main() {
Benz benz;
benz.Drive();
}

The above program will report an error due to the presence of the final keyword. The reasons for the error are:

Final: Modifies the class, indicating that the class can no longer be inherited.

Example:

#include<iostream>
class Car final
{
public:
virtual void Drive()
{}
};

class Benz: public car
{
public:
virtual void Drive() //Check whether rewriting is completed
{
std::cout << "Benz-Comfortable" << std::endl;
}
};
int main() {
Benz benz;
benz.Drive();
}

The above program reports an error:

Final cannot be used for base classes, otherwise the program will report an error!

(2) override

In C++, override is a special keyword used to explicitly identify a function in a derived class as overriding a virtual function in the base class.

When a function in a derived class has the same name, parameter list, and return type as a virtual function in a base class, the override keyword can be used to explicitly indicate that the function is an override of the base class function.

The benefits of using the override keyword are:

  1. Error checking: The compiler checks for function coverage errors at compile time. If the override keyword is used in a derived class but the virtual function in the base class is not correctly overridden, the compiler will report an error.
  2. Readability: Using the override keyword can enhance the readability and maintainability of the code and clarify the intention of the derived class function.

Here is a sample code:

#include<iostream>
class Car
{
public:
virtual void Drive()
{}
};

class Benz: public car
{
public:
virtual void Drive() override //Check whether rewriting is completed
{
std::cout << "Benz-Comfortable" << std::endl;
}
};
int main() {
Benz benz;
benz.Drive(); // Benz-comfortable
}

3. Abstract class

Pure virtual function

You may want to define a virtual function in the base class so that the function can be redefined in the derived class to better apply to the object, but you cannot give a meaningful implementation of the virtual function in the base class. In this case, Use pure virtual functions.

Write = 0 after the virtual function, then the function is a pure virtual function. A class containing pure virtual functions is called an abstract class (also called an interface class). Abstract classes cannot instantiate objects.

class Shape {
   public:
      // pure virtual function
      virtual int area() = 0;
};

= 0 tells the compiler that the function has no body and the above virtual function is a pure virtual function.

A class that includes pure virtual functions is called an abstract class, also called an interface class. Abstract classes cannot instantiate objects.

Example:

#include<iostream>
//abstract class
class Car
{
public:
virtual void Drive() = 0;//Pure virtual function
};

int main()
{
Car c;//Abstract class cannot instantiate objects
return 0;
}

When running the above program, an error occurs:

Derived classes cannot instantiate objects after inheritance.

Example:

#include<iostream>
class Car
{
public:
virtual void Drive() = 0; // pure virtual function
};
class Benz :public Car{};
int main()
{
Benz b1;
}

The above program runs with an error:

Derived classes cannot instantiate objects after inheritance. Only by overriding pure virtual functions can derived classes instantiate objects. Pure virtual functions specify that derived classes must be rewritten. In addition, pure virtual functions also reflect interface inheritance.

Example:

#include<iostream>
class Car
{
public:
//Pure virtual functions are generally only declared, not implemented (can be implemented, but of no value, because objects cannot be instantiated, pointers or references can be defined)
virtual void Drive() = 0;
};

class Benz: public car
{
public:
virtual void Drive()
{
std::cout << "Benz-Comfortable" << std::endl;
}
};

class BMW :public Car
{
public:
virtual void Drive()
{
std::cout << "BMW-control" << std::endl;
}
};

int main()
{
//Derived classes can instantiate objects only by overriding pure virtual functions
Benz b1;
BMW b2;
//Call functions of different objects through base class pointers
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
}

Output result:

Benz-Comfortable
BMW-Control

Interface inheritance and implementation inheritance

  • Inheritance of ordinary functions is a kind of implementation inheritance. The derived class inherits the ordinary member functions of the base class, can use the function, and inherits the implementation of the function.
  • The inheritance of virtual functions is a kind of interface inheritance. The derived class inherits the interface of the base class virtual function. The purpose is to override and achieve polymorphism. What it inherits is the interface.
  • So if you don’t implement polymorphism, don’t define the function as a virtual function.

Reference from (Very worth learning):

[Selected][C++]–Polymorphism_c++ Polymorphism_Xiao Mofan’s Blog-CSDN Blog

Note: The reference content is only for your own learning, no other ideas! ! !