什么是多态?
多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。
构成多态的条件
1.有继承关系
2.虚函数的重写
2.通过基类对象的指针或者引用调用虚函数
什么是虚函数
类的成员函数前加上virtual关键字,就构成了虚函数。
虚函数的重写
当在子类定义一个与父类完全相同的虚函数时,我们就称,子类的虚函数重写(覆盖)了父类的虚函数。
重写(也称覆盖),想要构成重写,需要如下几个条件:
1.不在同一个作用域(分别在父类和子类)
2.函数名相同,参数相同,返回值相同(协变除外)
3.基类函数必须带有virtual关键字
4.访问限定符不限
多态实例
#include<iostream>
#include<stdlib.h>
using namespace std;
class Person//父类
{
public:
virtual Person& BuyTicket()
{
cout << "买票全价" << endl;
return *this;
}
virtual ~Person()
{
}
};
class Student : public Person//子类
{
public:
virtual Student& BuyTicket() //重写父类的虚函数
{
cout << "买票半价" << endl;
return *this;
}
~Student()
{
}
};
int main()
{
Person p;
Person *q;
Student s;
// 普通调用:跟类型有关
p.BuyTicket();
s.BuyTicket();
//多态调用:父类指针指向不同对象调用函数不同
q = &p;
q->BuyTicket();
q = &s;
q->BuyTicket();
return 0;
}
通过上述代码和结果可以看出,前两句输出来自对象p和对象s的成员函数调用,后两次输出是由基类指针分别指向一个基类对象和一个派生类对象分别调用重写的函数,能发现调用的函数是不同的,所以,当使用基类的指针或引用调用重写的虚函数时,使用父类对象调用调的就是父类的虚函数,子类对象调用的就是子类的虚函数。
多态实现原理——虚函数表
什么是虚函数表
虚函数表是通过一块连续内存来存储函数的地址,这张表解决了继承,虚函数(重写)的问题,在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数。
#include<iostream>
#include<stdlib.h>
using namespace std;
class Person//父类
{
public:
virtual Person& BuyTicket()
{
cout << "买票全价" << endl;
return *this;
}
virtual void func(int a)
{}
virtual ~Person()
{
//cout << "~Person()" << endl;
}
};
class Student : public Person//子类
{
public:
virtual Student& BuyTicket() //重写父类的虚函数
{
cout << "买票半价" << endl;
return *this;
}
~Student()
{
// free
//cout << "~Student()" << endl;
}
};
void Func(Person& p)
{
// 多态调用:对象有关,指向哪个对象,调用就是谁的虚函数
// 1.对象父类指针/引用
// 2.重写的虚函数
p.BuyTicket();
}
int main()
{
Person p;
Person *q;
Student s;
// 普通调用:跟类型有关
p.BuyTicket();
s.BuyTicket();
//多态调用:父类指针指向不同对象调用函数不同
q = &p;
q->BuyTicket();
q = &s;
q->BuyTicket();
return 0;
}
在上述代码基类中多加入了一个虚函数func,便于观察。
在父类person和子类student中,都有一个变量_vfptr存储在头部,这是一个数组指针(虚表指针),所指的数组,就是我们说的虚表。
由上面的图片可以看出,虚表中依次存储了各个虚函数的地址,且存放的顺序和代码中定义的虚函数的顺序一致。当进行多态调用时,编译器根据传入对象的类别,找到对应的vfptr(虚表指针),再查看你要调用的函数在类中定义的位置,来找到该虚函数在虚表中存储的位置,实现调用。
虚表相关
为了更加深入的认识虚表,我又多创建了一个对象s1,它和s都是student类的实例化。
我们根据上图可以看到:
1.s和s1都继承了p,但他两个的vfptr与p的vfptr值不同,所以得出结论:编译器会根据类的不同而分别为2种类各自构建一个虚表。即虚表是从属于类的!
2.而同一类的s和s1,他们的vfptr值相同,且对应虚函数的地址也相同,所以得出结论:虚表指针是从属于对象的。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
3.再看p和s,s继承p,s中重写了虚函数Buyticket,而不重写虚函数func,在监视中,我们发现:s类中的虚函数Buyticket地址与p中的不同,而虚函数vfunc地址与p中的相同,所以得出结论:当子类继承父类的虚函数,不进行重写的话,虚函数地址不变,而如果重写(覆盖),那么就会改变对应虚函数的地址,分配一块新内存存储重写后的虚函数,这很像写时拷贝。这也说明了为什么重写又叫覆盖(新的虚函数地址覆盖了虚表中原有的父类虚函数地址)。
虚表存储在什么地方?
虚表在编译时就开始创建,构造函数实际是初始化_vfptr指向的位置。也就是说虚表不可能放在堆,栈中的。虚表存放在代码段和数据段都可以,都有可能。
一般虚表放在静态区(数据段中)。
多态中注意的地方
1.子类重写父类的虚函数实现多态,要求函数名,参数列表,返回值完全相同。(协变除外)
2.父类中定义了虚函数,在子类中该虚函数始终保持虚函数的特性。
3.只有类的成员函数才能被定义为虚函数。
4.各种类型的函数能否被定义成虚函数:
4.1 静态成员函数:不能!
静态函数不需要创建对象就可以调用,而虚表要进行初始化后,虚函数才能调用,所以没有必要;静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别。
4.2 构造函数:不能!
上面提到过,虚表是在构造函数中进行初始化的,你把构造函数弄成虚函数,此时虚表里还什么东西都没有呢,怎么找到他的地址呢,故不能定义成虚函数。
4.3 赋值运算符重载函数 :不能!
赋值运算符重载operator=虽然在定义上可以设为虚函数,但是最好不要这么做,容易在使用时引起混淆。
4.4内联函数 :不能!
我们知道内联函数的优势是在指定位置直接展开代码,省去了创建栈帧的开销,但这也同时使得内联函数没有地址,无法放进虚标内。
4.5析构函数:能!
最好把父类的析构函数声明为虚函数,子类析构和父类析构尽管不同名,但是在编译后实际会改成一个名为destructor的函数,构成同名函数。
因为在某些情况下,如果不定义成虚函数,可能会发生错误,比如:
Student s;
Person *p=s;//父类指针可以指向子类对象,子类对象发生切片行为
delete p;//此时对p调用析构函数,如果析构函数不是虚函数,则调用的是父类的析构,没有释放全部空间
//倘若定义成虚函数,则调用的是子类的析构函数,空间全部释放
5.如果在类外定义虚函数,只能在声明函数时加上virtual,类外定义函数时不能加virtual。
6.不要在构造函数和析构函数中调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
协变
重写的条件是必须函数名,参数列表,返回值都一样才能构成重写,但是协变除外。
协变是一种特殊的重写方式,他可以允许返回值不同,但是返回的一定要是父类的指针或引用。
纯虚函数和抽象类
在成员函数的形参后面写上=0,则成员函数为纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化对象,纯虚函数在子类中重新定义后,子类才能实例化出对象。