Detailed explanation of the principle of C++ polymorphism (static polymorphism, dynamic polymorphism, virtual function, virtual function table)

Detailed explanation of the principle of C++ polymorphism (static polymorphism, dynamic polymorphism, virtual function, virtual function table)

First give the definition: polymorphism is the ability to have multiple different manifestations or forms of the same behavior.

1 Link

Binding is also called binding, which refers to the step of "stitching" executable code together in the process of compiling and linking a source program into an executable file. Among them, the one that is completed before the program runs is called static binding (early binding) ; the one that is completed while the program is running is called dynamic binding (late binding) .
The polymorphism supported by static binding is called compile-time polymorphism (static polymorphism) , which is realized through function overloading or function template; the polymorphism supported by dynamic binding is called runtime polymorphism (dynamic polymorphism) , and Virtual function table implementation.

2 Static polymorphism

2.1 Function overloading

First look at a situation in code engineering:

void Swap1(int* a, int* b);
void Swap2(float* a, float* b);
void Swap3(char* a, char* b);
void Swap4(double* a, double* b);

When several functions with the same implementation function but different details are used, the C language will use different function names to distinguish them as shown in the above example, but this not only affects the beauty, but also makes it difficult to call and code management.
Therefore, function overloading was introduced in C++: overloading allows several similar functions with the same name to be declared in the same scope. The formal parameter lists (number of parameters, types, and order) of these functions with the same name must be different, and they are often used to handle the realization of functions. Similar data types are different.
The above example can be rewritten in C++ as:

void Swap(int* a, int* b);
void Swap(float* a, float* b);
void Swap(char* a, char* b);
void Swap(double* a, double* b);

In C++, not only functions can be overloaded, but operators can also be overloaded. An operator can be understood as a function name. For example, a+=1 can be regarded as: a.+=(1), where a is an object and += is its attribute. Since operators can be examined in this way, they can naturally be overloaded. An example of operator overloading is as follows, where operator is the keyword that declares the operator.

void operator +=(int number);

2.2 Function template

Consider the function overloading in Section 2.1. If the overloaded function only differs in interface data type, and the function body and function name are exactly the same, then you can use the function template to further simplify the code.

First, give the format of the function template:

template <typename 类型参数1 , typename 类型参数2 , ...> 返回值类型  函数名(形参列表){
函数体
}

For example, the swap overloaded function in section 2.1 can be replaced by a function template:

template <typename T>void swap(T* a, T* b)
{
	T temp = *a;
	*a = *b;
	*b = temp;
}

Among them, template and typename are keywords used to declare function templates. Use the above function template to process any legal input data type.
It should be noted that the current compilation does not support the separation of the declaration and implementation of the template function, so the template function body is generally also written in the header file.

3 Dynamic polymorphism

Dynamic polymorphism allows the configuration of the parent class object with the properties of one or more derived class objects. With the support of polymorphism, a certain interface of the parent class object will perform different operations depending on the derived class object.

3.1 Implementation principle

Consider the following code project first.

class Father
{
public:
	virtual void func1(){
		std::cout << "FUNC1:Father" << std::endl;
	}
	virtual void func2(){
		std::cout << "FUNC2:Father" << std::endl;
	}
	void func3(){
		std::cout << "FUNC3:Father" << std::endl;
	}
};

class Son :public Father
{
public:
	virtual void func1(){
		std::cout << "FUNC1:Son" << std::endl;
	}
	void func3(){
		std::cout << "FUNC3:Son" << std::endl;
	}
};

void testFunc(Father* ptr)
{
	ptr->func1();
	ptr->func3();
}

int main() {
	Son* testSon = new Son;	
	Father* testFather = new Father;

	std::cout << "==============子类测试=============\n";
	testFunc(testSon);
	std::cout << "\n==============父类测试=============\n";
	testFunc(testFather);
}

The results of its execution are:

==============子类测试=============
FUNC1:Son
FUNC3:Father

==============父类测试=============
FUNC1:Father
FUNC3:Father

First, clarify the reason to configure the parent class through the properties of the subclass. As shown in the example shown in Figure 1, mobile hard disks, U disks, and SD cards are all subclasses derived from the parent storage device. In practical applications, only one storage peripheral interface needs to be designed, and by judging which one is used A peripheral to determine the driver that the interface will execute. This interface-oriented design idea can enhance reusability and modularity, otherwise a peripheral needs to correspond to a kind of interface, which is too complicated.

Insert picture description here

figure 1

The following analysis of the code results to illustrate the realization principle of polymorphism.

The function testFunc() hopes to exhibit polymorphism. The subclass instance testSon reflects the characteristics of the subclass when executing func1(), but still retains the characteristics of the parent class when executing func3()—that is, it does not show polymorphism. It is because func1() is modified by the keyword virtual. The keyword virtual applies for post-linking, and the modified function is called virtual function, and dynamic polymorphism is supported under post-linking, so only func1() shows polymorphism.

At this point, the conditions for the small structure to become dynamic polymorphism: (a) The object calling the function must be a pointer or reference; (b) the function being called must be a virtual function.

In the later binding, dynamic polymorphism is realized through the virtual function table V-Table, which is an instance virtual function address table. If a virtual function exists in an instance, a virtual function table is automatically allocated in the memory of the instance, indicating the function that the instance should actually call. In the example, the virtual function pointer is used to point to the memory location where the virtual function table is located.

Insert picture description here

(a) Single inheritance and no virtual function coverage

Insert picture description here

(b) Single inheritance and virtual function coverage

Insert picture description here

(c) Multiple inheritance and no virtual function coverage

Insert picture description here

(d) Multiple inheritance and virtual function coverage
figure 2

Figure 2 shows various types of polymorphism. The virtual function table will be updated during instantiation-inheritance and overwriting are completed. During the running of the program, the compiler will look up the virtual table and link to the function that the instance should actually execute.

The sample code is of the type shown in Figure 2(b). Figure 3 shows the variable monitoring area of ​​the sample code. It can be seen that the virtual function table of the parent class and the derived class are different. If the derived class overrides the virtual function, the virtual table will be updated (func1) , The virtual function (func2) that is not overridden in the parent class is still inherited by the child class.
Insert picture description here

3.2 Pure virtual function

The pure virtual function can be represented by making the virtual function 0. The pure virtual function only defines the function interface, and the class containing the pure virtual function is called the abstract class.

An abstract class defines a prototype of the actions that a class may emit, but it has neither implementation nor any state information. The reason for introducing abstract classes is that in many cases the instantiation of the base class itself is unreasonable. For example, as a base class, an animal can derive subclasses such as tigers and peacocks, but instantiation of the animal itself is meaningless. In this case, the animal class can be defined as an abstract class.

class Animal
{
public:
	virtual void eat() = 0;
	virtual void run() = 0;
	virtual ~Animal() = default;
};

Since abstract classes only provide prototypes and cannot be instantiated, derived classes must provide specific implementation of the interface, otherwise they cannot be instantiated.

Guess you like

Origin blog.csdn.net/FRIGIDWINTER/article/details/114271713