一 . 多态
1.1 概念
当不同的对象去完成同一件事时,会产生不同的结果。例如学生和普通人一起买票时,学生可以买到学生票但是普通人就只能买到原价票。
1.2 虚函数
在一个类中使用 virtual 关键字修饰的类中的成员函数被称为虚函数。这一点和隐藏很像但是比隐藏更苛刻一些。
1.2.1 虚函数重写(覆盖)
虚函数要求父类中的函数名、参数类型、返回值和子类相同。(三同)
但是 有时父子类的返回值可以不相同,但是返回值必须是父子类的指针或者是引用。(协变)
cpp
class person{
public:
virtual void buy_ticket()
{
cout<<"全价"<<endl;
}
int _id;
}
class student:public person{
public:
virtual void buy_ticket()
{
cout<<"半价"<<endl;
}
}
void func(person * ptr)
{
ptr->buy_ticket();
}
int main()
{
person p1;
student p2;
func(p1);
func(p2);
return 0;
}
1.2.2 虚函数重写的特殊写法
(1)子类继承父类后,子类中的虚函数可以不写关键字 virtual ,因为子类继承了父类的函数,因此子类中与父类的虚函数完全相同的函数就是天生的虚函数(例如析构函数),不过在写虚函数时最好不要少virtual!!
(2)子类和父类的析构函数为什么是天生的虚函数?
在编译器在编译时会将父类对象的析构函数和子类对象的析构函数名都转换成一个名(destructor)这样在两者函数名相同、参数相同、返回值类型相同,就构成了虚函数。这样做的目的是调用析构函数时就可以调用正确类型的析构函数,不然在析构子类对象时调用的都是基类的析构函数,就出现了内存泄漏的情况。
cpp
#include<iostream>
using namespace std;
class person {
public:
virtual void buy_ticket()
{
cout << "全价" << endl;
}
virtual ~person()
{
.....
}
int _id;
};
class student :public person {
public:
virtual void buy_ticket()
{
cout << "半价" << endl;
}
virtual ~student()
{
.....
}
int _stu;
};
void func(person* ptr)
{
ptr->buy_ticket();
}
int main()
{
person* p = new person;
delete p;
p = new student;
delete p;
}
1.3 多态构成的条件(important!!)
(1)被调用的函数一定是虚函数,并且子类一定对父类的虚函数进行重写。
(2)必须通过父类的指针或者引用调用虚函数。
1.4 注意!
这道题定义的是B类的指针但是调用的是A类中的test函数,所以作为一个普通的调用 p->test( ) 这步里传输的this指针是A*类型的接着调用func( )函数,func函数在A类和B类中都存在且满足三同关系,这里是一个虚函数重写,且B类型继承了A类型,所以在调用时采用基类的接口(包括缺省值),但是函数体执行的是B中函数体的实现。选B
构成多态以后,子类在调用虚函数的时候,继承父类的接口(除了函数体以外的)重写了自己的实现(函数体)
1.5 final 和 overide
final:修饰虚函数,表示该虚函数不能再被重写。或者不想让自己被继承加final
override:检查子类虚函数是否重写了父类的虚函数,没写就报错。间接让子类必须重写虚函数。
1.6 重载、覆盖(虚函数重写)、隐藏(重定义)的对比
二.抽象类
只要在父类的虚函数后面写一个 =0 ,则称这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,但是可以定义该类型的指针。派生类继承后也不能实例化处对象,只有重写纯虚函数,派生类才能实例化出对象。
cpp
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
void Func(Car* p) {
p->Drive();
}
int main() {
Benz b;
Func(&b);
return 0;
}
三.多态的原理
3.1 虚函数表指针
cpp
class Base
{
public:
virtual void Func1()
{
cout<<"base::func1";
}
int _a;
}
class derive:public Base
{
public:
virtual void Func1()
{
cout<<"derive::func1";
}
int _d;
}
Base类型的大小是8,除了存放int类型的_a变量,还存放了了一个4个字节的虚函数表指针(vfptr)。虚函数指针本质上是一个数组指针,他指向了类中继承来的函数以及自身包含所有函数。
(1)重写(覆盖)的原理
当derive类对象继承了base类但是没有重写func1函数时,derive 对象的vfptr指针会拷贝一份base类的vfptr的指针,且调用func1时都是输出base::func1。
cpp
class Base
{
public:
virtual void Func1()
{
cout<<"base::func1";
}
int _a;
}
class derive:public Base
{
public:
int _d;
}
底层的结构如下图所示
当derive类对象继承了base类且重写func1函数时 ,derive类对象依然会拷贝一份base的虚表,但是会将 func1 的地址修改,且将函数的功能替换为虚函数重写的内容。虚函数重写的这个过程在底层其实就是将新的函数地址覆盖掉原来函数的地址,所以虚函数重写也被称为覆盖。覆盖完后将自己类中新的函数继续插入到vfptr指向的函数指针数组中。
cpp
class Base
{
public:
virtual void Func1()
{
cout<<"base::func1";
}
int _a;
}
class derive:public Base
{
public:
virtual void Func1()
{
cout<<"derive::func1";
}
void Func2()
{
cout<<"derive::func2";
}
int _d;
}
子类的指针和引用只能指向子类,因此子类无法调用父类中的虚函数始终只能调用子类的函数,这也就无法实现多态。
将子类赋值给父类对象时,并不会拷贝虚表,因此调用时始终是会去调用父类的函数,若拷贝虚表,那么父类对象中的虚表是父类的还是子类的就不确定了,这样便会引起混乱,因此不能是父类对象。
3.2多继承中的虚函数表
cpp
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
int main() {
Derive d;
Base1 * p1 = &d;
p1->func1();
Base * p2 = &d;
p2->func1();
}
derive 类继承了base1和base2,derive会先拷贝base1的虚表然后将自己类中的函数插入到第一章虚表中,在拷贝第二张base2 的虚表。