前言
自虚函数之后,多态会带来一些小问题
正文
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class father{ public: int x; int y;
virtual void Move(){ std::cout << "father move\n"; } void test(){ Move(); } };
class son :public father{ public: void Move() override{ std::cout << "son move\n"; } };
|
当我们实例化son,并且调用test的时候,这个test当然是继承过来的。
第一感觉可能会以为输出father的move。
但是,这个test是被继承过来的,son类中也有一个move函数,这个move是通过虚函数继承的,也就是说move只要在这个派生类中重写了,那么就不会跟基类的move冲突。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class father{ public: int x; int y;
virtual void Move(){ std::cout << "father move\n"; } void test(){ Move(); }
father(){ std::cout << this << std::endl; } };
class son :public father{ public: void Move() override{ std::cout << "son move\n"; } son(){ std::cout << this << std::endl; } };
|
为了好的了解,分别在构造函数的时候打印this指针。
可以看到打印的this指针的地址是相同的。
随便画个草图,基类的属性通过继承就像拷贝到派生类中一样,当实例化这个派生类的时候,由于发生了继承,基类要先完成构造,然后把这个x和y传给son。
然后还是实例化,这里实例化的是谁,son,那么this指针就是从son的地址开始的,所以两次this都是一样的。
注意派生类不会继承基类的构造函数,但是派生类在类中是可以访问的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class father{ public: int x; int y;
virtual void Move(){ std::cout << "father move\n"; } void test(){ this->Move(); }
father(){ std::cout << this << std::endl; this->Move(); } };
class son :public father{ public: void Move() override{ std::cout << "son move\n"; } son(){ std::cout << this << std::endl; } };
|
当我们在基类的构造函数中也调用一次move函数。
然后实例化son执行test。又会发生不同的情况。
可以看到,他居然执行了基类的move函数。
原因也在构造函数上,继承的时候,基类先完成初始化,所以在基类的构造函数里面调用move,它只能找到自己成员中的move函数。而后面test调用的move是因为派生类完成构造了,它里面重写了虚函数,这样就可以指定成自己的move函数了。
1 2 3 4 5 6 7 8 9 10
| class son :public father{ public: void Move() override{ std::cout << "son move\n"; } son(){ std::cout << this << std::endl; son::test(); } };
|
作用域指定调用肯定是调用自己,且不说派生类已经完成构造了。
1 2 3 4 5 6 7 8 9 10
| class son :public father{ public: void Move() override{ std::cout << "son move\n"; } son(){ std::cout << this << std::endl; father::test(); } };
|
因为虚函数被重写了,所以即便是通过基类调用test还是调用派生类重写的move。
1 2 3 4 5 6 7 8 9 10
| class son :public father{ public: son(){ std::cout << this << std::endl; father::test(); } };
|
当注释掉son类中的move,自然没有重写的部分,就只能继承基类的move函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class father{ public: int x; int y;
virtual void Move(){ std::cout << "father move\n"; } void test(){ this->Move(); }
father(){ std::cout << this << std::endl; this->Move(); } ~father(){ Move(); } };
class son :public father{ public: void Move() override{ std::cout << "son move\n"; } son(){ std::cout << this << std::endl; father::test(); } ~son(){ Move(); } };
|
当增加了析构函数之后,再依次执行move函数。
顺序其实也很好猜,从son实例化,基类先完成构造,派生类在完成构造,派生类先完成释放,基类在完成释放。
所以说,father析构函数调用的move是基类中的move,而son的析构函数则还是调用自己的。
并且析构函数是类结束生命周期的象征,所以基本上都是属于静态绑定了,即便是this指针也还是自己。
如果真的要调用基类的虚函数,就直接通过作用域访问就像了
虚函数默认参数
1 2 3
| virtual void AutoMove(int step = 2){ std::cout << "auto move" << step << std::endl; }
|
往father类中添加个带默认参数的虚函数,然后在类中重写。
1 2 3
| void AutoMove(int step = 3){ std::cout << "--auto move" << step << std::endl; }
|
看到automove的step是2,却不是3,我们指针指向son的对象了。
虚函数的默认参数,即使函数被重写了,也不会改变这个默认参数
析构问题
1 2 3 4
| son s1; father *p = new son(); p->AutoMove(); delete p;
|
看到他先执行的father的析构函数,但是我们这个指针是指向son类的。
这是因为指针是father类型的,delete p就相当于调用了p的析构函数,所以调用的是father的析构函数。
当然这会造成一个内存泄漏的问题,son类作为派生类,谁能保证成员是不是多一点,至少会比基类大点,而释放的大小是参考father的。
所以虚函数又派上了用场。
1 2 3 4
| virtual ~father(){ std::cout << "~father\n"; Move(); }
|
看到将析构函数也设置成虚析构函数之后,son类型的析构函数也会继承,这样一来释放的时候就会参考new的类型。
结语
虚继承这种动态的绑定关系真的要好好理一理。。
至于虚析构函数,如果没有特定内容,我们也可以用default指定。virtual ~father() = default;