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
const
is the pointer of the objectthis
. The member variables within the class cannot be modified in the modified function. - Adding the function after the class
override
allows 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 {
}//子类
virtual
The 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 this
pointer 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 override
strict 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 "
Compile and run, you can see that the result is that DAD DAD
A should be selected
3.1 Analysis
When Son
defining constructors and destructors for a class, there is no specification to call the corresponding constructors and destructors of the parent class. Therefore, when Son
an object is created, the constructor and destructor of the class ss
are called by default .Dad
Since Dad
the 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, Dad
calling in the constructor of echo()
is actually calling the function Dad
in the class echo()
, not Son
the 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 Dad
called in the destructor , the function in the class echo()
is still called .Dad
echo()
So when Son
an object is created and output is printed, the class's constructor ss
is 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 _a
twice delete
, delete
in 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. .