生活中我们是否会见到这样的场景,比如说我们想要去某个景区玩玩,再买票的时候,门口就有着学生半价,成人是全票,小孩免费什么的。对于这种场景,我们肯定不会对每一种人群写一个专门的函数,这样代码的复用性就变得很差。
这个时候就需要使用多态来解决
通俗的来说,多态就是一个事物的多个形态。当我们想要实现某个功能时,对于这个功能的不同对象都有着不同的状态。
这有个常见的例子,就是买票。经常会看的成人全票,学生证半价的情况。针对这种情况,定义两个多态的成员类
class People
{
public:
virtual void BuyTicket()
{
cout << "成人,买全价票" << endl;
}
};
class Student:public People
{
public:
virtual void BuyTicket()
{
cout << "学生,买半价票" << endl;
}
};
函数调用的方法
void Buy(People* Pobj)
{
Pobj->BuyTicket();
}
void text1()
{
People Pobj;
Student Sobj;
//成人
Buy(&Pobj);
//学生
Buy(&Sobj);
}
被virtual
修饰的类成员函数称为虚函数
class People
{
public:
virtual void BuyTicket()
{
cout << "成人,买全价票" << endl;
}
};
虚函数的重写也叫做虚函数的覆盖
,就是在子类中有一个跟父类完全相同的虚函数。
也就是子类的虚函数与父类的虚函数返回值类型,函数名字,参数列表完全相同,称子类的虚函数重写了父类的虚函数
class Student:public People
{
public:
virtual void BuyTicket()
{
cout << "学生,买半价票" << endl;
}
};
注意:子类在重写虚函数的时候,函数前是可以不用加virtual
关键字,但是子类继承的父类是一定要加virtual
关键字的。
虚函数重写的两个例外
子类在重写父类虚函数的时候,和父类虚函数返回值类型不同。也就是父类虚函数返回父类对象的指针或者引用,而子类虚函数返回子类对象的指针或者引用。
class A
{};
class B : public A
{};
class People
{
public:
virtual A* BuyTicket()
{
cout << "成人,买全价票" << endl;
return nullptr;
}
};
class Student :public People
{
public:
virtual B* BuyTicket()
{
cout << "学生,买半价票" << endl;
return nullptr;
}
};
也就是子类与父类虚函数的返回值可以不相同,但是这个返回值必须也是一个继承的关系,父类返回父类,子类返回子类
可以发现,当我们的父类是用子类强转过来的对象时,在进行析构函数的过程中,只是析构了切片后的父类,没有析构剩余子类部分。这里没有报错是因为类中并没有指针类型的成员变量。
这个时候就需要使用虚函数使析构函数构成多态,虽然析构函数中子类和父类的函数名看上去不同,不满足构成多态的条件,但实际上,编译后析构函数的名称统一都是destructor
。
这个时候,只需要让子类与父类的析构函数也满足多态,等待调用析构函数的时候,其实是从虚表中找到各自的析构函数,然后调用。
如果我们定义一个虚函数,我们不想让他满足继承关系,这个时候就可以使用C++11中的这两个关键字
2. override,检测子类虚函数是否重写了父类的某个虚函数,如果没有重写就会报错
不加override
,编译器没有报错,这时构成了隐藏,不是重写
抽象,也就是那些看起来很奇怪的事情,很难理解的事物。
我们在虚函数的后面加一个=0
,那么这个函数就构成了纯虚函数
。包含纯虚函数的类叫做抽象类
,也叫接口类
。抽象类不能实例化出对象,他只是一个声明,没有定义。
想要让抽象类发挥作用,只能在子类中重写该纯虚函数,利用子类来实例化出对象
//抽象类
class computer
{
public:
virtual void Brand() = 0;
};
//子类进行实例化
class DELL :public computer
{
public:
virtual void Brand()
{
cout << "Dell 戴尔" << endl;
}
};
class Mac :public computer
{
public:
virtual void Brand()
{
cout << "Mac" << endl;
}
};
抽象类的实现可以看成一种接口继承
接口继承
,就是纯虚函数的基础,子类继承的是父类的虚函数接口,子类的目的就是为了重写而达到多态,子类继承的是接口
实现继承
,普通函数的基础都是实现的继承,子类继承了父类的函数,可以对继承的函数进行使用或者重写,他继承的是函数的实现。
通过调试,可以看到在这个类的成员中,多了一个指针
,而这个指针指向的是一个指针数组
的地址。
除了_P成员
,还多一个__vfptr
放在对象的前面,对象中的这个指针我们叫做虚函数表指针
(v代表virtual,f代表function)。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表
中,虚函数表也简称虚表
我们可以用一个函数指针来完成对虚表的打印工作,利用int*
来取对象的前四个字节,然后再进行解引用,最后把这个int型的数据转为函数指针的类型。
有时候,这个虚表的结尾可能不是一个0,所以可能会存在打印好多地址的情况。
虚表中存储的是每个虚函数的地址,当我们让子类继承父类的时候,其实在子类的虚表中存储的是子类中虚函数的地址,如果发生了重新,那就换成重写后的虚函数的地址,没有重写,那么就是父类虚函数的地址。
如果子类中还有自己的虚函数,那么这个虚函数在虚表中的位置是在父类虚函数的后面
而对于多态而言,当我们把子类对象的指针强转为父类对象时,同时被转过去的还有这个虚表
,然后调用父类的对应虚函数时,其实调用的函数是虚表中子类重写的函数,这个时候就可以完成多态的功能。对于不同的情况,完成不同的操作。
虚函数时存在虚表中的,而虚表中存储的是函数的地址,在我们的程序中函数的地址都是存在代码段中的,所以虚表也是在代码段中的
就像上面Show函数作为一个普通的函数,他的地址就是在代码段中,我们通过打印,发现Show函数的地址和虚表中函数的地址比较接近,所以可以判断出,虚表存在于代码段
多继承设计的三个类
class Student
{
public:
virtual void BuyTicket()
{
cout << "学生,买半价票" << endl;
}
virtual void StudentFun4()
{
cout << "StudentFun4()" << endl;
}
protected:
int _S;
};
class Teacher
{
public:
virtual void BuyTicket()
{
cout << " 教师, 三折" << endl;
}
virtual void TeacherFun4()
{
cout << "TeacherFun4()" << endl;
}
};
class Grade : public Student, public Teacher
{
public:
virtual void BuyTicket()
{
cout << "团体折扣" << endl;
}
virtual void GradeFun4()
{
cout << "GradeFun4()" << endl;
}
};
我们可以看到。多继承中,子类自己的虚函数在第一个继承的父类虚表的后边
·多态·就是一个事物的多个形态。当我们想要实现某个功能时,对于这个功能的不同对象都有着不同的状态
而对于inline函数和static函数,他是不能成为虚函数的
类名::成员函数
的方法是无法访问虚表的,因此也不能放在虚表中至于为什么多态在调用的时候,必须是子类的指针或者引用类型,而不能是普通类型?
我们构成多态,是利用了this指针中的虚表,但是当我们使用普通赋值的时候,却发现实际上是调用了父类的构造函数,重新构造了一个父类,并不是切片的过程。
所以说,如果使用普通的赋值,就会造成赋值过去的变量的虚表和原本的虚表不同
所以说,使用指针类型或者引用类型,使得在强制类型转换的时候,使用切片的操作,就会完成多态