一. 多态的概念
多态的概念:通俗来说,就是 多种形态 。多态分为 编译时多态(静态多态) 和 运行时多态(动态多态) ,这里我们重点讲运行时多态,编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态 。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是"(>^ω^<) 喵",传狗对象过去,就是"汪汪"。
1.1 多态的定义及实现
1.1.1 虚函数
在讲多态之前我们务必先了解一下什么是虚函数?
cpp
//虚函数
class person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
如上面的示例代码,虚函数就是在类成员函数之前+virtual。这个成员函数就被叫做虚函数,需要注意的是非成员函数不能+virtual修饰!!!
虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数( 即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同 ),称派生类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。
1.1.2 多态的定义
多态是一个继承关系的下的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象优惠买票。
实现多态还有两个重要条件:
1.必须是基类的指针或者引用调用虚函数。
2.被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生 类之间才能有不同的函数,多态的不同形态效果才能达到。
那么下面我们就用代码来更为清晰的展示这一思路:
cpp
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-打折" << endl;
}
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
void Func1(Person ptr) //不能完成多态,因为不是指针或引用传递
{
ptr.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);//调用"买票-全价"
Func(&st);//调用"买票-打折"
Func1(st); //发生切片,只能调用"买票-全价"
return 0;
}

接下来练习一道习题作为巩固:

想必大家看到答案的时候可能有很多疑惑之处,具体来听我仔细讲解:我们现在的p虽然是B类型的,但是p调用的test并没有在B类中实现重写,所以编译器会默认使用A类的test,编译器在编译 A::test() 时,看到的是调用 func(),它知道 func 是虚函数,因此不会直接编译成调用 A::func()。它会生成一段代码,在运行时根据对象的实际类型(动态类型)去查找虚函数表(vtable),然后调用对应的函数。所以运行时会动态绑定到B::func(),但是func的默认参数是编译期绑定的,所以默认参数会绑定A::func的val=1,所以最终输出的应该是B->1;
1.1.3 虚函数重写的一些其他的问题
协变
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可。
析构函数的重写
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。
下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
注意:这个问题面试中经常考察,大家一定要结合类似下面的样例才能讲清楚,为什么基类中的析构 函数建议设计为虚函数。
cpp
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A {
public:
~B()
{
cout << "~B()->delete:" << _p << endl;
delete _p;
}
protected:
int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
return 0;
}
1.1.4 override 和 final关键字
从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。
cpp
// error C3668: "Benz::Drive": 包含重写说明符"override"的⽅法没有重写任何基类⽅法
class Car {
public:
virtual void Dirve()
{
}
};
class Benz :public Car {
public:
virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}
// error C3248: "Car::Drive": 声明为"final"的函数⽆法被"Benz::Drive"重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{
return 0;
}