多态是c++面向对象三大特性之一,关于什么是多态,我们需要先来了解一下虚函数和重写。
在类的成员函数前面加上virtual
关键字,就构成了虚函数。
当在子类定义一个与父类完全相同的虚函数时,我们就称,子类的虚函数重写(覆盖)了父类的虚函数。
重写(也称覆盖),想要构成重写,需要如下几个条件:
重写是专门为多态而生的,至于虚函数重写能够实现什么功能,我们在第三点多态的简单举例中详细明。
下面是实现多态的例子:
#include
#include
using namespace std;
class Person//父类
{
public:
virtual Person& BuyTicket()
{
cout << "买票全价" << endl;
return *this;
}
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;
Student s;
// 普通调用:跟类型有关
p.BuyTicket();
s.BuyTicket();
//多态调用:跟传入对象有关
Func(p);
Func(s);
system("pause");
return 0;
}
在上面的代码中,子类student继承了父类people,且父子类中都有虚函数BuyTicket,构成虚函数重载。又在类外定义了一个函数Func,在里面同样调用BuyTicket。
让我们运行一下:
我们发现,前两句输出来此与对象p和对象s的成员函数调用,后两次输出来自func函数。
这里就需要重点注意一下func函数了,func函数的参数是一个父类对象的引用,但是如果给他传一个子类对象,他会调用子类中的BuyTicket函数,输出“买票半价”,这就是多态,“一类代码,多种形态”。
当使用基类的指针或引用调用重写的虚函数时,使用父类对象调用调的就是父类的虚函数,子类对象调用的就是子类的虚函数。
1.函数的参数一定是父类的指针或引用
2.子类一定要重写父类的虚函数
这两个条件缺一不可,少任意一个都不能构成多态。
虚函数表是通过一块连续内存来存储虚函数的地址,这张表解决了继承,虚函数(重写)的问题,在有虚函数的对象实例中都存在一张虚函数表,虚函数表就像一张地图,指明了实际应该调用的虚函数。
#include
#include
using namespace std;
class Person//父类
{
public:
virtual Person& BuyTicket()
{
cout << "买票全价" << endl;
return *this;
}
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;
Student s;
// 普通调用:跟类型有关
p.BuyTicket();
s.BuyTicket();
//多态调用:跟传入对象有关
Func(p);
Func(s);
system("pause");
return 0;
}
为了更方便观察虚表,在上面的代码中,我向父类person中多定义一个虚函数vfunc,该函数没有进行任何操作。
在父类person和子类student中,都有一个变量_vfptr存储在头部,这是一个数组指针(虚表指针),所指的数组,就是我们说的虚表。
由上面的图片可以看出,虚表中依次存储了各个虚函数的地址,且存放的顺序和代码中定义的虚函数的顺序一致。当进行多态调用时,编译器根据传入对象的类别,找到对应的vfptr(虚表指针),再查看你要调用的函数在类中定义的位置,来找到该虚函数在虚表中存储的位置,实现调用。
为了更加深入的认识虚表,我又多创建了一个对象s1,它和s都是student类的实例化。
我们根据上图可以看到:
s和s1都继承了p,但他两个的vfptr与p的vfptr值不同,所以得出结论:编译器会根据类的不同而分别为2种类各自构建一个虚表。即虚表是从属于类的!
而同一类的s和s1,他们的vfptr值相同,且对应虚函数的地址也相同,所以得出结论:虚表指针是从属于对象的。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
再看p和s,s继承p,s中重写了虚函数Buyticket,而不重写虚函数vfunc,在监视中,我们发现:s类中的虚函数Buyticket地址与p中的不同,而虚函数vfunc地址与p中的相同,所以得出结论:当子类继承父类的虚函数,不进行重写的话,虚函数地址不变,而如果重写(覆盖),那么就会改变对应虚函数的地址,分配一块新内存存储重写后的虚函数,这很像写时拷贝。这也说明了为什么重写又叫覆盖(新的虚函数地址覆盖了虚表中原有的父类虚函数地址)。
虚表在编译时就开始创建,构造函数实际是初始化_vfptr指向的位置。也就是说虚表不可能放在堆,栈中的。虚表存放在代码段和数据段都可以,都有可能。
一般虚表放在静态区(数据段中)。
Student s;
Person *p=s;//父类指针可以指向子类对象,子类对象发生切片行为
delete p;//此时对p调用析构函数,如果析构函数不是虚函数,则调用的是父类的析构,没有释放全部空间
//倘若定义成虚函数,则调用的是子类的析构函数,空间全部释放
上面谈到重写的条件时说到,必须函数名,参数列表,返回值都一样才能构成重写,但是协变除外。
协变是一种特殊的重写方式,他可以允许返回值不同,但是返回的一定要是父类的指针或引用。
在成员函数的形参后面写上=0,则成员函数为纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化对象,纯虚函数在子类中重新定义后,子类才能实例化出对象。
class Person
{
virtual void Display() = 0;//纯虚函数
protected:
string _name;
};
class :Student : public Person//抽象类会强制让子类重写他的纯虚函数,否则子类就无法创建
{};