Polymorphism
Polymorphism Overview
Polymorphism refers to different behaviors when the same message is received by different types of objects. A message refers to calls to class member functions, and different behaviors refer to different implementations, which is calling different functions.
The simplest example is the “+” operator, which can implement addition between integers, floating-point numbers, and double-precision floating-point numbers, including mixed-type addition. The same “+” message, when received by different types of objects, uses different methods for addition operations. This is polymorphism.
Types of Polymorphism
Object-oriented polymorphism can be divided into four categories:
- Overloading Polymorphism
- Coercion Polymorphism
- Inclusion Polymorphism
- Parametric Polymorphism
The first two are called ad-hoc polymorphism, and the latter two are called universal polymorphism.
- Overloading polymorphism refers to the overloading of ordinary functions and class member functions learned before, and of course includes operator overloading.
- Coercion polymorphism, simply put, is when + involves mixed types, it will perform type coercion, which is an instance of coercion polymorphism.
- Inclusion polymorphism mainly refers to the polymorphic behavior of member functions with the same name defined in different classes in a class hierarchy, implemented through virtual functions.
- Parametric polymorphism is associated with class templates and must be given actual types when used to be instantiated.
This article will introduce operator overloading and virtual functions.
Implementation of Polymorphism
Polymorphism can be divided into two categories from an implementation perspective: compile-time polymorphism and runtime polymorphism. The former, as the name suggests, determines the specific operation object of the same-named operation during compilation. This process of determining the specific object of an operation is called binding.
Binding refers to the process of associating computer programs with each other, the process of connecting an identifier with a storage address. In object-oriented terms, it is the process of combining a message with an object’s method.
Binding is also divided into two types: static binding and dynamic binding.
When binding is completed during the compilation and linking phase, it is called static binding. Also known as early binding.
With static binding, dynamic binding, as the name suggests, is binding completed during program execution. Also known as late binding. It corresponds to runtime polymorphism.
Operator Overloading
Operator overloading is giving multiple meanings to existing operators, where the same operator causes different behaviors when acting on different types of data.
Rules for Operator Overloading
- In C++, except for a few operators, all operators can be overloaded, and only existing operators can be overloaded.
- After overloading, the precedence and associativity of operators will not change.
- Operators are appropriately modified for the actual needs of new type data. The overloaded functionality should be similar to the original functionality, cannot change the number of operands, and at least one must be a custom type (otherwise it’s not called overloading).
Several operators that cannot be overloaded: class membership operator “.”, member function pointer operator “.*“, scope resolution operator”::“, and ternary operator”?:”
There are two forms of overloading: overloading as non-static member functions of a class and overloading as non-member functions.
General form for overloading as non-static member functions and non-member functions of a class:
1 | 返回类型 operator 运算符(形参表) |
When overloading as a non-member function, sometimes you need to access members in the class, in which case it can be declared as a friend function.
Note:
When an operator is overloaded as a member function of a class, the number of function parameters is one less than the original number of operands (except for postfix “++” and “–”); when overloaded as a non-member function, the number of parameters equals the number of original operands. The reason is that when overloaded as a member function of a class, the first operand becomes the target object of the function call, so it doesn’t need to appear in the parameter list, and the function body can directly access the members of the first operand. When overloaded as a non-member function, all operands of the operator must be explicitly passed through parameters.
Operator Overloading as Member Functions
Mainly the difference between unary and binary operators.
For binary operators, the data type of the preceding class should be a member function of that class, and the data type of the following class should be placed in the formal parameters.
Unary operators are divided into two types: prefix unary operators and postfix unary operators. (++, – are unary operators, different names depending on placement)
For prefix unary operators, the overloaded member function has no formal parameters;
For postfix unary operators, the function must have an int formal parameter. This int parameter serves no purpose in the operation, it’s only used to distinguish between prefix and postfix.
Operator Overloading as Non-Member Functions
For binary operators, of the two data types before and after, only one needs to be a custom data type to enable operator overloading, and both data types need to be function formal parameters.
For prefix unary operators, the formal parameter is the data type being operated on.
For postfix unary operators, there are two formal parameters: one is the data type being operated on, and the other is an int data type.
It’s not hard to see that the main difference between member functions and non-member functions is that member functions implicitly treat the previous operand as the object of the function call, while non-member functions do not.
This section uses a Complex (complex number class) to illustrate
Click to view problem image (CUG experiment problem)
Virtual Functions
Virtual functions are the foundation of dynamic binding. Virtual functions must be non-static member functions. After virtual functions are derived, polymorphism during runtime can be implemented in the class hierarchy.
General Virtual Function Members
Declaration syntax
1 | virtual 函数类型 函数名(形参表); |
Virtual function declarations can only appear in function prototype declarations in class definitions, not in member function implementations.
Conditions required for polymorphism during runtime:
1. Assignment compatibility rules
1. Declare virtual functions
1. Call through member functions or access virtual functions through pointers or references
If the derived class does not explicitly declare virtual functions, the system will follow these rules to determine whether a function member in the derived class is a virtual function:
- Whether the function has the same name as the base class virtual function.
- Whether the function has the same number of parameters and corresponding parameter types as the base class virtual function.
- Whether the function has the same return type as the base class virtual function or satisfies assignment compatibility rules for pointer and reference type return values.
If the above conditions are met, the virtual function in the derived class will override all functions with the same name in the base class, which is scope hiding. Of course, it can also be distinguished through the scope resolution operator “::”.
It should be emphasized that dynamic binding only occurs when virtual functions are called through base class pointers or references.
Actually, it doesn’t matter whether you add the virtual keyword before virtual functions in derived classes, but it’s recommended to add it to make it clearer that they are virtual functions.
Object slicing refers to using an instance of a derived class to initialize a base class object, which calls the base class copy constructor. That is, only the data members of the derived class that are the same as the base class will be copied, and the rest will be ignored. At this point, this base class object has no relationship with the derived class object, which is very consistent with type compatibility rules.
Virtual Destructors
Virtual constructors cannot be declared, but virtual destructors can be declared. The syntax is to add a virtual keyword before the normal destructor.
So when do you need to use virtual destructors?
When a base class pointer points to a derived class object, if
delete(base class pointer) is performed at this time, it
will call the base class destructor instead of the derived class
destructor, causing memory leaks. (I actually think this situation is
almost never encountered, just understand it) The difference from
polymorphism of ordinary member functions is not much.
Pure Virtual Functions and Abstract Classes
Pure Virtual Functions
Pure virtual functions are virtual functions declared in the base class that have no definition in the base class but require any derived class to define its own implementation. The method to implement pure virtual functions in the base class is to add “=0” after the function prototype, for example:
1 | virtual void func() = 0 |
My big question is why introduce pure virtual functions?
- To facilitate the use of polymorphism features, we often need to define virtual functions in the base class.
- In many cases, it is unreasonable for the base class itself to generate objects.
For example, animals as a base class can derive subclasses like tigers and peacocks, but animals themselves generating objects is obviously unreasonable.
To solve the above problems, the concept of pure virtual functions was introduced. When a function is defined as a pure virtual function, the compiler requires that it must be overridden in derived classes to achieve polymorphism. Classes containing pure virtual functions are called abstract classes and cannot generate objects. This solves the above two problems well. Classes that declare pure virtual functions are abstract classes. Therefore, users cannot create instances of abstract classes, only instances of their derived classes (which implement the definitions of pure virtual functions in the base class). The most significant feature of pure virtual functions is: they must redeclare the function in inherited classes (without the =0 at the end, otherwise the derived class cannot be instantiated), and they often have no definition in abstract classes. The purpose of defining pure virtual functions is to make derived classes only inherit the function interface. The meaning of pure virtual functions is to allow all class objects (mainly derived class objects) to execute the actions of pure virtual functions, but the class cannot provide a reasonable default implementation for pure virtual functions. So the declaration of pure virtual functions in a class is telling the designer of the subclass, “You must provide an implementation of the pure virtual function, but I don’t know how you will implement it.”
Abstract Classes
Very simple, classes with pure virtual functions are abstract classes and cannot be instantiated
If you want to learn more, see the blog.









