多态的概念,通俗地来讲就是多种形态。当我们要完成某个行为的时候,不同的对象去完成时会产生不同的状态,这就叫做多态。
比如说我们写一个买票的程序,买票其实就是一个多态的体现,普通人买票就是正常买,没有优惠,学生买票有学生价优惠,儿童买票可以半价或者免票,军人可以优先买票,我们可以设计多态来实现这些不同对象的不同状态。
被virtual关键字修饰的类成员函数成为虚函数:
virtual 返回值 函数名()
{
函数体
}
虚函数的重写也叫做虚函数的覆盖,指的是在子类中有一个跟父类完全相同的虚函数(这里的完全相同指的是返回值类型、函数名字、参数列表都完全相同),这个子类的虚函数就叫做父类虚函数的重写。
父类的虚函数必须写virtual,否则不构成多态。子类的虚函数可以写virtual也可以不写,依旧构成虚函数,只要满足返回值类型、函数名字、参数列表都完全相同即可。我们自己在写子类虚函数的时候最好还是加上virtual。
在继承关系中要想形成多态,必须满足下面两个条件:
#include
using namespace std;
class Person
{
public:
virtual void buyTicket()
{
cout << "普通票" << endl;
}
};
class Student : public Person
{
public:
virtual void buyTicket()
{
cout << "学生票" << endl;
}
};
class Soldier : public Person
{
public:
virtual void buyTicket()
{
cout << "军人票" << endl;
}
};
void payForTicket(Person* ptr)
{
ptr->buyTicket();
}
int main()
{
// 普通人买票
Person p;
payForTicket(&p);
// 学生买票
Student st;
payForTicket(&st);
// 军人买票
Soldier so;
payForTicket(&so);
return 0;
}
协变(父类与子类的虚函数返回值类型不同):
子类在重写虚函数的时候有一种情况可以让子类虚函数的返回值类型与父类虚函数的返回值类型不同,那就是父类虚函数返回值类型是一个对象的指针或者引用,子类虚函数返回值类型是另一个对象的指针或者引用。但有一个条件就是:这两个对象必须是父子关系。
class A{};
class B : public A {};
class Person
{
public:
virtual A* f() {return new A;}
};
class Student : public Person
{
public:
virtual B* f() {return new B;}
};
析构函数的重写(父类与子类的析构函数函数名不相同):
如果父类的析构函数是虚函数,此时子类的析构函数只需要定义,无论是否加virtual关键字,都与父类的析构函数构成重写。虽然它们两个析构函数的函数名字不相同,但其实编译器对析构函数的名称做了处理,编译后析构函数的名字统一被处理成destructor。
我们在设计父类的时候,析构函数需要设计成虚函数,这样子类在写虚函数的时候构成重写,在释放资源的时候才不会出现错误。
多态是接口继承,子类重写父类的虚函数只有函数体是自己写的,函数接口还是继承自父类。比如下面这个例子,B类是A类的子类,构成多态条件后用A类指针调用B类的func函数,最后的输出结果是:B->1。原因是函数体用的是B类的,但接口是继承自A类的,这其中就包括val的缺省值。
#include
using namespace std;
class A
{
public:
virtual void func(int val = 1)
{
cout << "A->" << val << endl;
}
};
class B : public A
{
public:
virtual void func(int val = 0)
{
cout << "B->" << val << endl;
}
};
int main()
{
// 构成多态条件
A* p = new B;
p->func();
return 0;
}
final:
修饰虚函数时,表示该虚函数不能再被重写,如果子类对其进行重写会编译报错。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
// 这里重写了,会编译报错
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
修饰类时,表示该类不能被继承,如果被继承了会编译报错。
class Car final
{
public:
virtual void Drive(){}
};
// 不能继承Car,会编译报错
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
override:
修饰子类的虚函数,用来检查子类是否已经完成了重写,如果没有符合重写要求就会编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
在虚函数的后面加上=0,则这个函数称为纯虚函数。包含纯虚函数的类叫作抽象类,也叫作接口类。抽象类不能实例化出对象,子类继承了以后子类也不能实例化出对象,只有重写了纯虚函数,子类才可以实例化出对象。
纯虚函数规范了子类必须重写,纯虚函数更体现出多态的接口继承,因为抽象类的纯虚函数也可以定义函数体,但是没有意义,一般只需要给声明就可以了,目的是让子类继承其接口,定义了函数体根本就没有任何对象可以使用它。
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;
}
};
一个含有虚函数的类中都至少含有一个虚函数表指针,虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。
我们以下面这份代码来看一看虚表,Derive类继承了Base类,Base类中Func1和Func2是虚函数,Func3是普通函数,Derive类中重写了父类的Func1函数。我们创建一个Base类的对象b和一个Derive类的对象d。
#include
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;
}
通过调试我们可以看看对象b和对象d里面包含什么内容:
对象b和对象d里都含有一个_vfptr,这个是虚表指针,虚表其实本质上是一个指针数组,每一个元素代表的是每一个虚函数的地址。
b对象中的虚表有两个元素,第一个是Base类中的Func1函数地址,第二个是Base类中的Func2函数地址,没有Func3的函数地址,因为Func3并不是虚函数,所以不会放在虚表中。
d对象中的虚表是继承自父类Base的,它先会将父类的虚表拷贝下来,然后看自己对哪些虚函数实现了重写,d对象对Func1函数实现了重写,所以d对象的虚表就会覆盖原来Base类的Func1,写成Derive类的Func1函数地址。
为什么构成多态的条件一定是要父类的指针或者引用,子类对象切片给父类对象就不能构成多态呢?
我们看下面这份代码示例:首先定义一个子类Derive对象d,然后分别定义父类Base对象b让d赋值给b,定义一个父类Base对象指针指向对象d,定义一个父类Base对象引用为对象d,再分别调用Func1。
#include
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()
{
Derive d;
Base b = d;
b.Func1();
Base* ptr = &d;
ptr->Func1();
Base& ref = d;
ref.Func1();
return 0;
}
运行程序查看结果我们可以发现,只有父类对象的指针和引用构成了多态,而子类对象切片给父类对象并不能构成多态。
原因是子类对象切片给父类对象时,子类只会拷贝成员给父类,不会拷贝虚表指针。如果拷贝了虚表指针,那么父类对象就乱套了,因为当父类对象再去调用Func1虚函数的时候,不知道该调用父类的Func1还是子类的Func1,这就不能构成多态了。
所以多态的原理其实就是在程序运行的时候,我们要调用虚函数时,对象就会到虚表中去找虚函数的地址,然后调用对应的虚函数。多态属于运行时决议。