P27-c++ class inheritance-04 static binding and dynamic binding

1. Static and dynamic binding

When the program calls a function, which executable code block will be used? The compiler is responsible for answering this question.

Interpreting function calls in the source code as executing specific function code blocks is called function name binding.
In C language, this is very simple, because each function name corresponds to a different function. In C++, this task is more complicated due to function overloading.
The compiler must look at the function parameters and function name to determine which function to use. However, the C/C++ compiler can complete this kind of binding during the compilation process. In the process of compiling build is called static binding (static binding), also known as Early Binding (early binding). However, virtual functions make this task more difficult.

As shown in the program listing 13.10, which function to use cannot be determined at compile time, because the compiler does not know which type of object the user will choose. Therefore, the compiler must be able to generate a virtual method to select the correct program runtime code, which is called dynamic binding (dynamic binding), also known as late Binding (late binding).

After knowing the behavior of virtual methods, let's discuss this process in depth, and first introduce how C++ handles the compatibility of pointers and reference types.

2. Compatibility of pointers and reference types

In C++, dynamic binding is related to calling methods through pointers and references. To some extent, this is controlled by inheritance. One way public inheritance establishes an is-a relationship is how to deal with pointers and references to objects.
Generally, C++ does not allow one type of address to be assigned to another type of pointer, nor does it allow one type of reference to point to another type:

double x = 2.5;
int * pi = &x; //invalid assignment, mismatched pointer types
long & r1 = x; // invalid assignment, mismatched reference type

However, as you can see, a reference or pointer to a base class can refer to a derived class object without having to perform an explicit type conversion. For example, the following initialization is allowed

BrassPlus dilly("Annie Dill", 493222, 2000)
Brass *pb = &dilly; // ok
Brass &rb = dilly; // ok

Converting derived class references or pointers to base class references or pointers is called upcasting , which eliminates the need for explicit type conversions for public inheritance.
This rule is part of the is-a relationship. BrassPlus objects are Brass objects, because it inherits all the data members and member functions of the Brass object.
Therefore, any operation that can be performed on the Brass object is applicable to the BrassPlus object. Therefore, functions designed to handle Brass references can perform the same operations on BrassPlus objects without worrying about causing any problems. The same is true when a pointer to an object is used as a function parameter.

Upcasting is transitive, that is, if the BrassPlusPlus class is derived from BrasPlus, the Brass pointer or reference can refer to the Brass object, the BrassPlus object, or the BrassPlusPlus object.

The reverse process of converting a base class pointer or reference to a derived class pointer or reference is called downcasting.

If you do not use explicit type conversion, down-casting is not allowed. The reason is that the is-a relationship is usually irreversible.

Derived classes can add new data members, so the class member functions that use these data members cannot be applied to the base class. For example, if the Singer class is derived from the Employee class, and the data member representing the singer's range and the member function range() used to report the value of the range are added, it is meaningless to apply the range() method to the Employee object. But if implicit downcasting is allowed, you may unintentionally set the pointer to Singer to the address of an Employee object and use the pointer to call the range() method (see Figure 13.4).
Insert picture description here

For function calls that use base class references or pointers as parameters, up-conversion will be performed. Please look at the code snippet below, where it is assumed that each function calls the virtual method ViewAcct()

void fr(Brass & rb); //uses rb.ViewAcct()
void fp(Brass *pb); // uses pb->ViewAcct()
void fv(Brass b); //uses b.ViewAcct()
int main()
{
    
    
	Brass b("Billy Bee",123432,10000.0);
	BrassPlus bp("Betty Beep", 232313, 12345.0);
	fr(b); //uses Brass::ViewAcct(
	fr(bp); // uses BrassPlus::ViewAcct()
	fp(b); //uses Brass::ViewAcct()
	fp(bp); // uses BrassPlus::ViewAcct()
	fv(b); //uses Brass::ViewAcct()
	fv(bp); //uses Brass::ViewAcct()
}

Passing by value causes only the Brass part of the BrassPlus object to be passed to the function fv(). But the implicit up-conversion that occurs with references and pointers causes the functions fr() and fp() to use Brass::ViewAcct() and BrassPlus::ViewAcct() for Brass objects and BrassPlus objects, respectively.

Implicit upward casting makes base class pointers or references point to base class objects or derived class objects, so dynamic binding is required. C++ uses virtual member functions to meet this need.

3. Virtual member functions and dynamic binding

Let's review the process of calling methods using references or pointers. Please see the code below:

BrassPlus ophelia; //derived-class object
Brass *bp; // base-class pointer
bp = &ophelia; // Brass pointer to BrassPlus object
bp->ViewAcct(); // which version?

As mentioned earlier, if the base class did not ViewAcct () declared empty, then bp-> ViewAcct()the pointer according to the type of call Brass (Brass *) :: ViewAcct (( ).
Pointer type is known at compile time, the compiler in When compiling, you can associate ViewAcct() with Brass::ViewAcct(). In short, the compiler uses static binding
for non-virtual methods.

However, if ViewAcct() is declared as virtual in the base class, then bp-> ViewAcct()BrassPlus::ViewAcct() is called according to the object type (BrassPlus).

In this example, the object type is BrassPlus, but usually (as shown in the program listing 13.10) the type of the object can only be determined when the program is run. Therefore, the code generated by the compiler will associate ViewAcct() with
Brass::Viewacct() or BrassPlus::ViewAcct() according to the object type when the program is executed .
In short, the compiler uses dynamic binding for virtual methods.
In most cases, dynamic binding is good because it allows the program to choose the method designed for a particular type. Therefore, you may ask:

  • Why are there two types of links?
  • Since dynamic binding is so good, why not set it as the default?
  • How does dynamic binding work?

Let’s take a look at the answers to these questions.

4. Why are there two types of bindings and why are static bindings by default?

If dynamic binding allows you to redefine class methods, and static binding is very poor in this regard, why not abandon static binding?
There are two reasons- efficiency and conceptual model.

First look at efficiency. In order for the program to make decisions during the running phase, some methods must be adopted to track the type of object pointed to by the base class pointer or reference, which adds additional processing overhead (a dynamic binding method will be introduced later).
For example, if the class will not be used as a base class, there is no need for dynamic binding. Similarly, if the derived class (such as RatedPlayer) does not redefine any methods of the base class, there is no need to use dynamic binding.

In these cases, using static binding is more reasonable and more efficient. Because static binding is more efficient, it is set as the default choice for C++. Strousstrup said that one of the guiding principles of C++ is not to pay the price (memory or processing time) for unused features. Use them only when the program design really needs virtual functions.

Next look at the conceptual model. When designing a class, it may include some member functions that are not redefined in the derived class. For example, the Bras::Balance() function returns the account balance and should not be redefined. Not setting this function as a virtual function has two advantages: First, it is more efficient; second, it is pointed out that the function should not be redefined.
This means that only those methods that are expected to be redefined are declared as virtual.

Tip: If you want to redefine the method of the base class in the derived class, set it as a virtual method; otherwise, set it as a non-virtual method.
Of course, when designing a class, it is sometimes not so obvious which situation the method belongs to. Like many aspects in the real world, class design is not a linear process.

5. How virtual functions work

C++ specifies the behavior of virtual functions, but leaves the implementation method to the compiler author. You don't need to know the implementation method to use virtual functions, but understanding the working principle of virtual functions helps to better understand the concept, so here is an introduction.

Usually, the way the compiler handles virtual functions is to add a hidden member to each object. A pointer to the function address array is stored in the hidden member.

This type of array is called a virtual function table (vtbl).
The virtual function table stores the address of the virtual function declared for the class object.
For example, the base class object contains a pointer to the address table of all virtual functions in the base class. The derived class object will contain a pointer to the independent address table.
If the derived class provides a new definition of a virtual function, the virtual function table will save the address of the new function. If the derived class does not redefine the virtual function, the vtbl will save the address of the original version of the function.

If the derived class defines a new virtual function, the address of the function will also be added to vtbl (see Figure 13.5). Note that no matter whether the class contains 1 or 10 virtual functions, only one address member needs to be added to the object, but the size of the table is different.
Insert picture description here
When calling a virtual function, the program will check the vbl address stored in the object, and then turn to the corresponding function address table.
If you use the first virtual function defined in the class declaration, the program will use the first function address in the array and execute the function with that address.
If you use the third virtual function in the class declaration, the program will use the function whose address is the third element in the array.
In short, when using virtual functions, there are certain costs in terms of memory and execution speed, including:

  • Each object will increase, and the increase will be the space of the storage address;
  • For each class, the compiler creates a virtual function address table (array)
  • For each function call, an additional operation needs to be performed, which is to look up the address in the table.
    Although the efficiency of the non-virtual function is slightly higher than that of the virtual function, it does not have the function of dynamic binding.

6. Notes about virtual functions

We have already discussed some of the main points of virtual functions.

Using the keyword virtual in the declaration of a base class method can make the method virtual in the base class and all derived classes (including classes derived from derived classes).

If you use a reference or pointer to an object to call a virtual method, the program will use the method defined for the object type instead of the method defined for the reference or pointer type. This is called dynamic binding or late binding. This behavior is very important, because the base class pointer or reference can point to the derived class object.

If the defined class will be used as a base class, the class methods to be redefined in the derived class should be declared as virtual.
For virtual methods, you also need to understand some other knowledge, some of which have already been introduced. Let's take a look at these contents.

1. Constructor

The constructor cannot be a virtual function. When creating a derived class object, the constructor of the derived class will be called instead of the constructor of the base class. Then, the constructor of the derived class will use a constructor of the base class. This order is different from the inheritance mechanism.

Therefore, the derived class does not inherit the constructor of the base class, so it is meaningless to declare the class constructor as virtual.

2. Destructor

The destructor should be a virtual function, unless the class does not need to be a base class.
For example, suppose Employee is the base class, Singer is a derived class, and add a char* member, which points to the memory allocated by new. When the Singer object expires, the ~Singer() destructor must be called to release the memory.
Please see the code below:

Employee *pe = new Singer; // legal because Employee is base for Singer
delete pe; //~Employee() or -Singer()?

If you use the default static binding, the deltete statement will call the ~Employee() destructor. This will release the memory pointed to by the Employee part of the Singer object, but will not release the memory pointed to by the new class member. However, if the destructor is virtual, the above code will first call the ~Singer()destructor releases the memory pointed to Singer assembly, then called Employee ~ () function to release the destructor component directed Employee memory.

This means that even if the base class does not require an explicit destructor to provide services, it should not rely on the default constructor, but should provide a virtual destructor, even if it does nothing:

virtual	~BaseClass() {
    
    }

By the way, it is not an error to define a virtual destructor for a class, even if this class does not need to be a base class; this is just a matter of efficiency.

Tip: Usually, a virtual destructor should be provided to the base class, even if it does not need a destructor.

3. Tomomoto

Friends cannot be virtual functions, because friends are not class members, and only members can be virtual functions.
If a design problem arises for this reason, it can be solved by letting the friend function use virtual member functions.

4. No redefinition

If the derived class does not redefine the function, the base class version of the function will be used. If the derived class is in the derivation chain, the latest virtual function version will be used. The exception is that the base class version is hidden (described later)

5. Redefine the hidden method

Suppose the code shown below is created:

class Dwelling
{
    
    
public:
	virtual void showperks(int a) const;
};

class Hovel : public Dwelling
{
    
    
public:
	virtual void showperks() const;
}

This will cause problems, and compiler warnings similar to the following may appear:

Warning: Hovel::showperks(void) hides Dwelling: showperks(int)

There may also be no warning. But regardless of the result, the code will have the following meaning:

Hovel trump;
trump.showperks(); // valid
trump.showperks(5) // invalid

The new definition defines showperks() as a function that does not accept any parameters.
The redefinition does not generate two overloaded versions of the function, but hides the base class version that accepts an int parameter.
In short, redefining the method of inheritance is not overloading. If you redefine a function in a derived class, instead of overriding the base class declaration with the same function signature, you will hide the base class method with the same name, regardless of the parameter signature.

This leads to two rules of thumb:
First, if you redefine the inherited method, you should ensure that it is exactly the same as the original prototype, but if the return type is a base class reference or pointer, you can modify it to a reference or pointer to a derived class ( This exception is new).
This feature is called return type covariance (covariance of return type), because the return type is allowed to change with the change of the class type:

class Dwelling
{
    
    
public:
	// a base method
	virtual Dwelling build(int n);
}

class Hovel: public Dwelling
{
    
    
public:
// a derived method with a covariant return type
	virtual Hovel build(int n); //same function signature
}

Note that this exception only applies to return values, not to parameters.
Second, if the base class declaration is overloaded, you should redefine all base class versions in the derived class.

class Dwelling
{
    
    
public:
	// three overloaded showperks()
	virtual void showperks(int a) const;
	virtual void showperks(double x)const;
	virtual void showperks() const;
}
class Hovel: public Dwelling
{
    
    
public:
// three redefined showperks(
	virtual void showperks(int a)const;
	virtual void showperks(double x)const;
	virtual void showperks()const;
}

If only one version is redefined, the other two versions will be hidden and derived objects will not be able to use them. Note that if there is no need to modify, the new definition can only call the base class version:

void Hovel::showperks() const {
    
     Dwelling::showperks();}

Guess you like

Origin blog.csdn.net/sgy1993/article/details/113853122