ps:本篇章和继承关系十分紧密,如果继承理解的不够透彻建议先去看一下C++ 继承(先给自己打个广告)
一 什么是多态
1.1 多态的概念
多态,顾名思义,就是多种形态,再具体一点就是 对于某些事情,不同的人去完成时体现出不同的状态。
在C++种 ,我们把不同类中的函数实现出这一特性,也就是一个函数,在不同类中将发挥不一样的作用,这就是多态。
1.2 多态的种类
多态又分为静态多态和动态多态:
- 静态多态:主要指函数重载。
- 动态多态:主要指父类的指针或引用调用、重写虚函数。如果父类的指针或引用指向父类,就调用父类的虚函数;如果父类的指针或引用指向某个子类,就调用那个子类的虚函数。
因为静态多条大多是指函数重载,我相信除了我,大家都会,因此本篇章不做过多讨论。
二 多态的定义及实现
2.1 多态的定义
定义: 多态大多是指不同继承关系的类对象,调用同一函数时产生了不同的行为。
比如 :学生和社畜都属于人,但是他们如果去买火车票,学生就会有学生票,但是社畜就得买成人票。
这个例子映射到C++ 上则是,Student类和Adult类都继承了Person类,Adult对象在买票这一行为上需要购买成人票,而Student对象买票则购买学生票。
在继承要构成多态还有两个条件:
必须通过父类的指针或者引用调用虚函数。
被调用的函数必须是虚函数,且派生类必须对父类的虚函数进行重写。
2.2 virtual关键字和虚函数
在类中的成员函数前加virtual关键字后,该函数就变为虚函数(注意一般的函数前不可以加virtual)。
比如:
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTickets()
{
cout << "全价票" << endl;
}
};
在这个类中 BuyTickets就是一个虚函数。
错误示例:
不在类 内部+ virtual 则会报错。
注意:
- 只有类的非静态成员函数才可以成为虚函数。(这个一会讲到虚函数原理时会解释)。
虚函数和虚继承都用到了virtual关键字,但二者之间没有任何关系。虚函数使用virtual是为了实现多态,而虚继承是为了解决菱形继承中产生的数据冗余和二义性问题,它们之间没有关联。
三 虚函数的重写
3.1 重写的概念和方法
重写的概念:
派生类中有一个跟基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,但是参数在内部实现的是不同行为的代码,这时称子类的虚函数重写了基类的虚函数。
如:
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTickets()
{
cout << "是个人就得买票" << endl;
}
};
class Student:public Person
{
public:
virtual void BuyTickets()
{
cout << "学生买半价票" << endl; //虚函数复写
}
};
class Adult:public Person
{
public:
virtual void BuyTickets()
{
cout << "成年人买全价票" << endl; //虚函数复写
}
};
void func(Person& p) //用引用示例 指针同理
{
p.BuyTickets(); // 表面上调用同一个函数,实际上每个类中的函数功能各不相同
}
int main()
{
Student s1;
Adult a1;
Person p1;
func(p1);
func(s1);
func(a1);
return 0;
}
代码结果为:
注意:只有指针和引用可以实现多态,多态的本质实际上也是利用指针实现的,如果我们把引用去掉则会没有多态的使用效果。
3.2 虚函数重写的三个例外
3.2.1 协变
协变的定义
子类重写父类虚函数时,与父类虚函数仅有返回值类型不同,且父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时,称为协变。
例如:
#include<iostream>
using namespace std;
class Person //父类
{
public:
virtual Person* pointer() //返回值为Person*
{
cout << "Person* pointer()" << endl;
return new Person;
}
};
class Student : public Person //派生类继承父类
{
public:
virtual Student* pointer() //返回值为Student*
{
cout << "Student* pointer()" << endl;
return new Student;
}
};
class Adult : public Person //派生类继承父类
{
public:
virtual Adult* pointer() //返回值为Adult*
{
cout << "Adult* pointer()" << endl;
return new Adult;
}
};
void func(Person& p)
{
p.pointer();
}
int main()
{
Person p;
Student s;
Adult ptr;
func(p);
func(s);
func(ptr);
return 0;
}
代码结果如下:
可以看出依然实现了多态。
3.2.2 析构函数的重写
由于析构函数的函数名是有要求的,所以从代码角度看,子类和父类析构函数的函数名不同,违反了多态的要求。
但实际上,编译器对所有析构函数的函数名都做了特殊处理,编译后析构函数的名称统一处理成destructor。
但是析构函数的重写有必要吗?
在Effective C++中作者曾指出过:
带多态性质的父类应该声明一个虚析构函数,也就是说,如果类带有任何一个virtual函数,那它就应该拥有一个virtual析构函数。 (但我说实话,如果一个类不带有virtual函数,那么他也就基本上不会成为父类了)
建议,如果一个类注定会成为一个父类,一定要有virtual析构函数,反之则一定不需要virtual析构函数。
我们来看一下需要virtual函数的情况:
#include<iostream>
using namespace std;
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student:public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
//假设这里还有很多元素
};
int main()
{
Person *p1 = new Person;
Person* s1 = new Student; //这里使用一下继承中的切片原理
delete p1;
delete s1;
return 0;
}
代码结果为:
C++ 明确指出,当子类对象经过一个父类指针而被删除,并且该父类带着非virtual析构函数,其结果未定义————实际上,派生类中只删除了父类元素,也就是会形成一个诡异的“局部销毁”对象,这可真是一个引来灾难的秘诀,更是形成资源泄漏,败坏之数据结构,在调试器上浪费许多时间的绝佳途径!!!
当然其解决方法也十分简单:
把父类的非virtual析构函数变成 virtual析构函数即可:
#include<iostream>
using namespace std;
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student :public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
//假设这里还有很多元素
};
int main()
{
Person* p1 = new Person;
Person* s1 = new Student; //这里使用一下继承中的切片原理
delete p1;
delete s1;
return 0;
}
结果为:
要不要写virtual析构函数?
建议:如果一个类注定会成为一个父类,一定要有virtual析构函数,反之则一定不需要virtual析构函数。
3.2.3 子类的虚函数可以不用virtual修饰
如果父类的某一个函数已经定义为虚函数,那么子类与之相同函数不需要用virtual修饰,也认为是完成了重写。但实际上这也是C++的一个小坑,为了方便代码的维护,建议还是只要需要用到虚函数的位置全部都用virtual修饰。
3.3 重载、重写、重定义的对比
四 C++ 11的新特性
4.1 final
修饰父类的虚函数,使该虚函数无法被重写。
class Person
{
public:
virtual void BuyTickets() final //注意final位置
{
cout << "是个人就得买票" << endl;
}
};
class Student:public Person
{
public:
virtual void BuyTickets()
{
cout << "学生买半价票" << endl;
}
};
如:
4.2 override
修饰子类的虚函数,检查该虚函数是否重写,若没有重写则编译报错。
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTickets()
{
cout << "是个人就得买票" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTickets(int i) override //注意override位置,因为函数参数列表不同,不构成重写,所以报错
{
cout << "学生买半价票" << endl;
}
};
五 抽象类
5.1 抽象类的定义
在虚函数的后面写上 “= 0” ,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
例如:
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTickets()=0
{
cout << "是个人就得买票" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTickets()
{
cout << "学生买半价票" << endl;
}
};
5.2 抽象类的使用
为什么要有抽象类呢?
首先,我们这个世界上有很多抽象的事物,他们虽然在现实世界中没有确定的实体,却又处处体现出和现实世界的种种联系。比如植物类,虽然没有确定的植物,但却可以让其它派生类去继承他的各种特性,从而达到复用的作用。
建议:当一个类无法new一个合适的对象,但是却可以成为很多类的父类的时候,我们就可以让它成为一个抽象类,从而达到最大限度复用代码的作用。
5.3.接口继承和实现继承
普通函数的继承是一种实现继承,子类继承了父类函数,继承的是函数的实现。而虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。尤其是抽象类的产生,更是强制子类重写父类(否则子类也无法实例化出对象,类的功能大打折扣)。
六 多态的原理
6.1 虚函数表指针
下面这段代码的结果是多少呢? 64位平台下
#include<iostream>
using namespace std;
class A
{
public:
virtual void Func()
{
cout << "virtual void Func()" << endl;
}
private:
int _b = 0;
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
答案是:
是不是和兄弟们想的有点不一样?
我们直接打开监视窗口,查看一下a
发现多了一个_vfptr指针,这个指针,我们叫做虚函数表指针(v代表virtual,f代表function) 。
6.2 虚函数表
一个虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址是要放到虚函数表中的,虚函数指针即指向这个虚函数表的开头。
虚函数表也简称虚表,虚函数表指针简称为虚表指针。
接下来我们来探究一下虚表里面的内容:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1() //定义两个虚函数
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1() //只重写一个虚函数
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
父类虚表
子类虚表
通过观察和测试,我们发现了以下几点问题:
1.每个在多态体系中的类都有一个虚表指针,但是派生类对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
2.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,原理是 重写后的函数覆盖了父类的函数,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也通过继承体系存在在子类中,但是不是虚函数,所以不会放进虚表。
4.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中; b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6.3 多态的原理
我们再次回到买票的代码:
#include<iostream>
using namespace std;
class Person
{
public:
virtual void BuyTickets()
{
cout << "是个人就得买票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTickets()
{
cout << "学生买半价票" << endl;
}
};
int main()
{
Person p;
p.BuyTickets();
Student s;
s.BuyTickets();
return 0;
}
看一下虚表:
父类虚表:
子类虚表
我们可以看出:
本质上:
通过调试可以看到,p和s中的虚表指针指向的地址不同,也就是说两个类中定义的BuyTickets()不是同一个,所以在调用时其实是调用各自定义的函数,实现了多态。
6.4.再次理解多态构成的条件
为什么必须要是父类的指针或引用来调用虚函数,为什么不能是对象调用?
指针和引用调用、对象调用本质上是子类切片完成的,指针和引用的切片只针对子类中父类的那一部分,其他都不管,只管与父类一样的内容(虚表中)切片过去;如果是对象切片,就是将虚表里的内容全部切片过去(因为都是要调用构造函数的),那么就有可能父类也指向了子类的虚表,那就乱套了。
可以理解为:指针和引用是将虚表的指针拷贝过去了,对象是将虚表中的内容拷贝过去了。
6.5 静态函数
我们再来回答为什么静态函数不能实现多态:
我们知道静态函数最主要的特点就是:
静态成员函数最主要的特性是其没有this指针
在多态体系中,我们需要this指针先去找到虚表,再去调用虚函数,静态成员函数连指针都没有,怎么实现多态?