1.多态的概念
当不同的对象去完成某个行为时,会产生出不同的状态;通俗来说就是多种形态。
eg:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
2.多态的定义及实现
2.1 多态定义的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
继承中构成多态的两个条件:
1.调用函数的对象必须是指针或者引用。
2.被调用的函数必须是虚函数,且完成了虚函数的重写。
虚函数:指在类的成员函数的前面加virtual关键字
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
虚函数的重写:派生类中有一个跟基类的完全相同的虚函数,我们就称子类的虚函数重写了基类的虚函数。其中,完全相同是指:函数名,参数,返回值都相同。另外,虚函数的重写也叫做虚函数的覆盖。
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student :public Person
{
public:
virtual void BuyTicket()
{
cout << "买票半价" << endl;
}
};
void Func(Person & people)
{
people.BuyTicket();
}
void Test()
{
Person per;
Student st;
Func(per);
Func(st);
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关键字,也能构成重写。因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数的属性,我们只是重写了它。(但这种行为非常不规范,不建议平时这样使用。)
class Person {
public:
virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
class Student : public Person {
public:
void BuyTicket() {cout << "买票-半价" << endl;}
};
注意:基类的析构函数最好写成虚函数。因为,如果基类中的析构函数是虚函数,那么派生类的析构函数就重写了基类的析构函数。虽然他们的函数名不相同,看起来违背了重写的规则,其实可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多 态,才能保证p1和p2指向的对象正确的调用析构函数。
int main() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
普通函数与类型有关(实现继承:派生类继承了基类函数,可以使用函数,继承的是函数的实现。)
多态调用与对象有关(接口继承:派生类继承的是基类函数的接口,目的是为了重写,达成多态,继承的是接口)虚函数的继承也是一种接口继承,所以,如果不实现多态,不能把函数定义为虚函数。
2.2 重载,覆盖(重写),隐藏(重定义)的对比
重载:两个函数在同一作用域;函数名/参数相同。
重写(覆盖):两个函数分别在基类和派生类的作用域;
函数名/参数/返回值都必须相同(协变例外);
两个函数必须是虚函数。
重定义(隐藏):两个函数分别在基类和派生类的作用域;
函数名相同;两个基类和派生类的同名函数不构成重写就是重定义。
3.抽象类
***在虚函数的后面写上 =0 ,则这个函数为纯虚函数。***包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。
***只有重写纯虚函数,派生类继承后才能实例化出对象。***纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
4.C++11中的override和final
虚函数的意义是实现多态,如果没有重写,虚函数就没有意义。所以,C++11提供override 和 final 来修饰虚函数。
建议使用纯虚函数+override(检查是否构成重写函数)的方式来强制重写虚函数。
// 1.final 修饰基类的虚函数不能被派生类重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒适" << endl; }
}
class Car{
public:
virtual void Drive(){}
};
// 2.override 修饰派生类虚函数强制完成重写,如果没有重写会编译报错
class Benz :public Car { public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
5.多态的原理
5.1虚函数表与多态的原理
虚函数表本质是一个存虚函数指针的指针数组。
虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
派生类的虚表生成步骤:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基 类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在 派生类中的声明次序增加到派生类虚表的最后。
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象中取的。不满足多态的函数调用时编译时确认好的。
5.2 动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数 重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用 具体的函数,也称为动态多态。
- 本小节之前(5.2小节)买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。
6.多态的常见面试题
- 什么是多态?答:当不同的对象去完成某个行为时,会产生出不同的状态。
- 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考上面的内容
- 多态的实现原理?答:虚函数表
- inline函数可以是虚函数吗?答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义 成虚函数。具体参考上面的(2.多态的定义)。
- 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是 引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数是在编译阶段就生成的,一般情况下存在静态区 的。
- C++菱形继承的问题?虚继承的原理?答:参考继承。注意这里不要把虚函数表和虚基表搞混了。
- 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出 了接口继承关系。
封装–>管理
继承–>复用
多态–>灵活性
**声明:**本次博客中的实例是以vs2013下的x86程序中,涉及的指针都是4bytes。如果在其他 平台下,部分代码可能需要改动。例如,在x64程序中,则需要考虑指针是8bytes问题等等。