今天在和群友聊天的时候看到了一道很坑的题目,分享给大家
1.看题!
先来看看题目
struct Dad
{
public:Dad(){ echo();}~Dad(){ echo();}virtual void echo() {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 "
E 编译出错
F 运行出错
答案是E,编译出错!
2.涉及到的知识点
2.1 知识点
先来说说这道题目里面涉及到了什么知识点
- 多态调用;
- 多态重写函数需要满足什么条件;
- 类内函数后加
const的作用; - 类内函数后加
override的作用; - 什么是早绑定和晚绑定
一个一个复习吧!
- 多态调用是父类指针/引用指向子类时,调用虚函数会调用子类重写后的版本
- 多态重写函数的条件:函数名/参数/返回值都必须相同(注意还有协变)
- 类内函数后加
const修饰的是这个对象的this指针,被修饰的函数中无法修改类内成员变量 - 类内函数后加
override是让编译器来严格检查是否构成重载 - 早绑定:静态绑定;晚绑定:动态绑定(具体请看CPP多态的博客)
2.2 分析题目
注意看父类和子类中这两个echo()函数的区别
virtual void echo(){}//父类
void echo() const override {}//子类
首先需要说明的是,子类函数中virtual关键字是可以省略的,但即便省略了,这个函数依旧是个虚函数。
这里子类的函数中多了const修饰,而这个const修饰的就是函数中隐含的this指针,此时子类中echo()函数的参数就发生了变化!
virtual void echo(Son* this) { } // 不加const
virtual void echo(const Son* this) { } // 加const
正是因为这里的this指针出现了const的修饰,所以子类的echo和父类echo的参数类型不同,不构成虚函数重写!再加上override关键字的严格检查,会直接编译报错!
正确的写法是删除子类echo中的const或者给父类echo函数加上const
3.再来看题
好了,坑人的点看完了,再来看个「常规」的,就是把上面的题干改成能编译通过的。此时又应该选谁呢?
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 "

编译运行,可以看到,结果是DAD DAD,应该选A
3.1 分析
在给 Son 类定义构造函数和析构函数时,没有指定调用父类的对应构造函数和析构函数。因此,在创建 Son 对象 ss 时,会默认调用 Dad 类的构造函数和析构函数。
由于 Dad 类中的构造函数和析构函数调用了虚函数 echo(),而这个虚函数在子类 Son 中被重写,所以会根据对象类型调用相应的重写函数。然而,在构造函数和析构函数中,虚函数机制不会按照预期工作。
构造函数中调用虚函数时,会忽略动态绑定机制,直接调用父类的函数版本。因此,在 Dad 的构造函数中调用 echo(),实际上调用的是 Dad 类中的 echo() 函数,而不是 Son 类中的重写版本。
同样地,析构函数中也会忽略动态绑定机制,直接调用父类的函数版本。所以,在 Dad 的析构函数中调用 echo(),依然调用的是 Dad 类中的 echo() 函数。
因此,当创建 Son 对象 ss 并打印输出时,会先调用 Dad 类的构造函数并打印 "DAD ",然后调用 Dad 类的析构函数并再次打印 "DAD "。
3.2 结论
在父类的构造和析构中,对象的版本都被确定为父类的版本,会采用早绑定来调用父类自己的函数,而不是子类的重写后的函数;
简单记忆:父类的构造和析构中如果出现虚函数,只会调用父类自己的函数!
这是因为编译器需要保证正确的构造和析构顺序,如果父类析构里调用子类的虚函数,可能会出现下面的场景
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;
如果父类中的析构echo()调用子类重写的函数,此时就会出现子类已经被销毁(子类的析构函数早于父类析构调用)的_a被二次delete,两次delete同一片空间是会报错的!
所以为了避免这种情况,父类的析构中采用早绑定,子类重写的虚函数不会生效!
这种行为是为了确保在对象的构造和析构过程中,按照正确的顺序调用各个类的构造和析构函数,避免在对象处于未完全初始化或已部分销毁状态时调用子类的函数。
包括父类的构造也可以这么理解,如果父类构造里面可以调用子类的虚函数,可能会出现两次对一个子类对象进行new空间,会产生内存泄露;
但构造函数还和虚函数表的初始化有关系,此时虚函数表还没有完全初始化,子类对象尚未构造完成,没有多态调用的条件,所以也不能调用到子类重写后的虚函数。