[C++] Early binding, destruction and polymorphism | Record of a multiple-choice question about polymorphism

Today when I was chatting with a group of friends, I saw a very confusing question, so I would like to share it with you.

1. Read the questions!

Let’s take a look at the title first

struct Dad
{
    
    
public:
    Dad(){
    
     echo();}
    ~Dad(){
    
     echo();}
    virtual void echo() {
    
    
        cout << "DAD ";
    }
};

struct Son:Dad
{
    
    
public:
    void echo() const override {
    
    
        cout << "SON ";
    }
};

Son ss;

What is the output of this?

A  "DAD DAD "
B  "DAD SON "
C  "SON DAD "
D  "SON SON "
E  编译出错
F  运行出错

The answer is E, compilation error!

2. Knowledge points involved

2.1 Knowledge points

First, let’s talk about what knowledge points are involved in this question.

  • polymorphic call;
  • What conditions need to be met for polymorphic rewriting functions;
  • The function of adding functions within a class const;
  • The function of adding functions within a class override;
  • What is early binding and late binding

Review them one by one!

  • Polymorphic calling means that when the parent class pointer/reference points to a subclass, calling a virtual function will call the rewritten version of the subclass.
  • Conditions for polymorphically rewriting functions: function names/parameters/return values ​​must be the same (note that there is covariance)
  • What is modified after the function within the class constis the pointer of the object this. The member variables within the class cannot be modified in the modified function.
  • Adding the function after the class overrideallows the compiler to strictly check whether it constitutes an overload.
  • Early binding: static binding; late binding: dynamic binding (see the CPP polymorphism blog for details)

2.2 Analysis question

echo()Pay attention to the difference between these two functions in the parent class and the subclass

virtual void echo(){
    
    }//父类
void echo() const override {
    
    }//子类

virtualThe first thing to note is that keywords can be omitted in subclass functions , but even if they are omitted, the function is still a virtual function.

There are more modifications in the function of the subclass here const, and this const modification is the implicit thispointer in the function. At this time, the parameters of the function in the subclass echo()have changed!

virtual void echo(Son* this) {
    
     } // 不加const
virtual void echo(const Son* this) {
    
     } // 加const

It is precisely because the this pointer here is modified with const, so the parameter types of the echo of the subclass and the echo of the parent class are different, and it does not constitute a virtual function rewrite! Coupled with overridestrict keyword checking, an error will be reported directly during compilation!

The correct way to write it is to delete the const in the subclass echo or add const to the parent class echo function.

3. Let’s look at the question again

Okay, now that we’ve finished reading the tricky bits, let’s look at the “regular” ones, which is to change the above questions into ones that can be compiled and passed. Who should you choose at this time?

struct Dad
{
    
    
public:
    Dad(){
    
     echo();}
    ~Dad(){
    
     echo();}
    virtual void echo() const{
    
    
        cout << "DAD ";
    }
};

struct Son:Dad
{
    
    
public:
    void echo() const override {
    
    
        cout << "SON ";
    }
};

Son ss;
A  "DAD DAD "
B  "DAD SON "
C  "SON DAD "
D  "SON SON "

image-20230822211806379

Compile and run, you can see that the result is that DAD DADA should be selected

3.1 Analysis

When Sondefining constructors and destructors for a class, there is no specification to call the corresponding constructors and destructors of the parent class. Therefore, when Sonan object is created, the constructor and destructor of the class ssare called by default .Dad

Since Dadthe constructor and destructor in the class call virtual functions echo(), and this virtual function is overridden in the subclass Son, the corresponding overridden function will be called according to the object type. However, in constructors and destructors, the virtual function mechanism does not work as expected.

When a virtual function is called in the constructor, the dynamic binding mechanism will be ignored and the function version of the parent class will be called directly. Therefore, Dadcalling in the constructor of echo()is actually calling the function Dadin the class echo(), not Sonthe overridden version in the class.

Similarly, the dynamic binding mechanism will be ignored in the destructor and the function version of the parent class will be called directly. Therefore, when Dadcalled in the destructor , the function in the class echo()is still called .Dadecho()

So when Sonan object is created and output is printed, the class's constructor ssis called first and printed , and then the class's destructor is called and printed again .Dad"DAD "Dad"DAD "

3.2 Conclusion

In the construction and destruction of the parent class, the version of the object is determined to be the version of the parent class, and early binding is used to call the parent class's own function instead of the rewritten function of the subclass;

Simple memory: If a virtual function appears in the construction and destruction of the parent class, only the parent class's own function will be called!


This is because the compiler needs to ensure the correct order of construction and destruction. If the virtual function of the subclass is called in the destruction of the parent class , the following scenario may occur.

struct Dad
{
    
    
public:
    Dad(){
    
     echo();}
    ~Dad(){
    
     echo();}
    virtual void echo() const{
    
    
        cout << "DAD ";
    }
};

struct Son:Dad
{
    
    
public:
    Son() {
    
    
        _a = new int(3);
    }
    ~Son() {
    
    
        delete _a;
    }
    void echo() const override {
    
    
        cout << "SON ";
        delete _a;
    }
private:
    int _a;
};

Son ss;

If the destructor in the parent class echo()calls a function rewritten by the subclass, it will appear that the subclass has been destroyed (the destructor of the subclass is earlier than the destructor of the parent class) and is destroyed _atwice delete, deletein the same space twice. An error will be reported!

So in order to avoid this situation, early binding is used in the destructor of the parent class, and the virtual functions overridden by the subclass will not take effect!

This behavior is to ensure that the constructors and destructors of each class are called in the correct order during the construction and destruction of the object , and to avoid calling functions of subclasses when the object is in an incompletely initialized or partially destroyed state.

The structure of the parent class can also be understood in this way. If the virtual function of the subclass can be called in the parent class structure, new space may be used for a subclass object twice, which will cause a memory leak;

However, the constructor is also related to the initialization of the virtual function table. At this time, the virtual function table has not been fully initialized, the subclass object has not yet been constructed , and there are no conditions for polymorphic calling, so the rewritten virtual function of the subclass cannot be called. .

Guess you like

Origin blog.csdn.net/muxuen/article/details/132437789