C++篇(12)多态
一、多态的概念
多态,通俗来说就是多种形态。多态可分为编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模版,它们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。
运行时多态,具体来说就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达成多种形态。
二、多态的定义和实现
2.1 多态的构成条件
多态是一个继承关系下的类对象去调用同一函数,产生了不同行为。比如,Student继承了Person,Person对象买票全价,Student对象买票半价。
实现多态有两个必须条件:
·必须是基类的指针或引用调用虚函数
·被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖
2.2 虚函数
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。
注意:非成员函数不能加virtual修饰。
2.3 虚函数的重写/覆盖
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
注:在重写基类虚函数时,派生类的虚函数不加virtual关键字仍然可以构成重写,但是该写法不是很规范,不建议这样使用。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:
virtual void buyTicket()
{
cout << "全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void buyTicket()
{
cout << "半价买票" << endl;
}
};
void Func(Person* ptr)
{
ptr->buyTicket();
}
int main()
{
Person p;
Student s;
Func(&p);
Func(&s);
return 0;
}
可以看到,虽然都是Person指针ptr在调用buyTicket函数,但是和ptr没有关系,而是由ptr指向的对象决定的。
2.4 多态场景的一个选择题
下面程序输出的结果是什么()
A、A->0 B、B->1 C、A->1 D、B->0 E、编译出错 F、以上都不正确
#include <iostream>
using namespace std;
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
virtual void test()
{
func();
}
};
class B
{
public:
virtual void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
B* p = new B;
p->test();
return 0;
}
答案是B!
首先要思考的问题是,是否构成多态?我们可以看到,是一个B*类型的指针p去调用test函数,那么,test函数参数部分隐藏的this指针是A*还是B*呢?有人可能会这么想,B类继承了A类,所以是B*类型的。这种想法是错误的,继承不是把基类成员拷贝一份到派生类当中,在编译时,派生类对象调用成员函数是先在派生类中查找,再去基类中查找。所以这里的this指针是A*类型的,也就是基类的指针。其次我们发现,虚函数完成了重写。两个条件都满足,所以构成多态,也就是说,这里调用的是B::func()。
那么看到这里,有人会想,那不应该是选D吗,怎么会选B呢?注意,构成多态时,重写的是虚函数的实现,所以这里val的值才会是1而不是0(如下图)

2.5 虚函数重写的一些其他问题
2.5.1 协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。协变的实际意义不大,了解即可。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class A{};
class B:public A{};
class Person
{
public:
virtual A* buyTicket()
{
cout << "全价买票" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* buyTicket()
{
cout << "半价买票" << endl;
return nullptr;
}
};
void Func(Person* ptr)
{
ptr->buyTicket();
}
int main()
{
Person p;
Student s;
Func(&p);
Func(&s);
return 0;
}
2.5.2 析构函数的重写
我们先来看下面这段代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
~B()
{
cout << "~B()->delete" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
运行结果如下图所示:

我们发现,只析构了A类,却没有析构B类,这不就造成内存泄漏了吗?那为什么没有调用到B的析构呢?首先,我们知道delete操作符本质是先调用对应的析构函数,再调用operator delete(p1)。我们希望指向谁就调用谁的析构函数,也就是多态调用。而编译器对析构函数的名称统一处理成了destructor,所以基类的析构函数加了virtual关键字,派生类的析构函数就构成了重写。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B :public A
{
public:
virtual ~B()
{
cout << "~B()->delete" << _p << endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
三、纯虚函数和抽象类
在虚函数的后面加上 =0,则这个函数为纯虚函数。纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
更多推荐


所有评论(0)