一、多态的概念
- 概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。(同一件事物,在不同场景下表现出的不同的状态)
- 举个列子:比如说买票这个行为,当普通人买票时,是全价票;学生买票时,是半价买票;军人买票时是优先买票。(见人说人话,见鬼说鬼话)
二、多态的定义及实现
- 多态定义的构成条件------>必须在继承体系中:
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& p)
{
p.BuyTicket();
}
int main()
{
person ps;
student st;
Func(ps);
Func(st);
return 0;
}
-
重写的概念:
1>派生类重写基类的虚函数(被重写的函数在基类中必须为虚函数)
2>一个在基类中,一个在派生类中
3>派生类虚函数必须与基类虚函数的原型完全一致(返回值类型,函数名(参数列表))
4>派生类函数前virtual加不加都可以,建议:最好加上 -
虚函数重写的例外:
a)协变:基类虚函数返回基类的指针或引用,派生类虚函数返回派生类的指针或引用(基类和子类虚函数的返回值类型不同)
b)析构函数:只要基类中的析构函数是虚函数,派生类中的析构函数一旦提供,就可以与基类的析构函数构成重写。基类与派生类析构函数的函数名不同。(建议:在继承体系中,如果派生类中涉及到资源管理,最好将基类的析构函数设置成虚函数。) -
接口继承和实现继承:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。 -
重载、覆盖(重写)、隐藏(重定义)的对比
1、重载:必须在同一个作用域,函数名相同,参数列表必须不同(个数、次序、类型)与返回值类型是否相同无关。
2、重写:必须在继承体系中,被重写的函数在基类中必须是虚函数,派生类重写基类的虚函数时,必须要与基类的虚函数原型完全相同(返回值类型以及参数列表完全相同,类外:协变,析构函数重写)
3、重定义:必须在继承的体系中,基类与派生类具有相同名称的成员(成员函数、成员变量),只要名称相同就构成重写,与成员函数参数类型以及返回值类型是否相同无关,对基类函数是否为虚函数没有要求。
三、抽象类
- 概念:
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class WashRoom
{
public:
void GoToManRoom()
{
cout << "往前一小步,文明一大步" << endl;
}
void GoToLadyRoom()
{
cout << "往后一小步,文明一大步" << endl;
}
};
// 抽象类
class Person
{
public:
// 纯虚函数
virtual void GoToWC(WashRoom& wc) = 0;
protected:
string _name;
int _age;
};
class Man : public Person
{
public:
virtual void GoToWC(WashRoom& wc)
{
wc.GoToManRoom();
}
};
class Woman : public Person
{
public:
virtual void GoToWC(WashRoom& wc)
{
wc.GoToLadyRoom();
}
};
void Test()
{
WashRoom wc;
Person* man = new Man;
man->GoToWC(wc);
Person* woman = new Woman;
woman->GoToWC(wc);
}
四、C++11 override和final
- override:只能修饰派生类虚函数,在编译阶段,编译器会对override修饰的函数原型,在基类中进行查找,看是否能够找到原型一致的虚函数。编译器帮助检测派生类虚函数是否对基类虚函数进行重写,如果重写失败,编译器将会报错。
- final:修饰类,表示该类不能被继承;修饰成员函数(虚函数),代表该虚函数不能被重写。
五、多态的原理
- 虚函数表
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会 放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表。
- 派生类虚表构建规则:
1)将基类虚表中内容拷贝一份到派生类虚表中
2)如果派生类重写了基类某个虚函数,用派生类虚函数地址替换(覆盖)相同偏移量位置的基类虚函数地址
3)将派生类新增加的虚函数按照其在派生类中的声明次序增加到虚表的后面 - 总结:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就 是存在部分的另一部分是自己的成员。
- 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重 写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的 叫法,覆盖是原理层的叫法。
- 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会 放进虚表。
- 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。
六、多态常见面试题
- 多态常见的面试问题
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理?
- inline函数可以是虚函数吗?答:不能,因为inline函数没有地址,无法把地址放到虚函数表中。
(加上虚拟virtual关键字,编译器就会忽略inline,如果没有加,编译器就会把它当成内联函数) - 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
【假设可以为虚函数:虚函数必须通过对象调用,因为编译器必须从对象前4个字节中取到虚表地址,然后找到虚函数进行调用。1)非静态成员函数必须依靠对象进行调用,可以通过对象找到虚表调用虚函数;2)静态成员函数可以不通过对象进行调用,无法拿到虚表,如果静态成员函数可以作为虚函数时,无法拿到虚表不能调用】 - 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始 化的。
【假设构造函数可以作为虚函数:构造函数存储在虚表中。调用时必须通过对象找到虚表,从虚表中找到构造函数,但是构造函数没有调用,对象就不完整,最主要:构造函数中编译器才将虚表的地址放在对象的前四个字节】 - 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义 成虚函数。
【在继承体系中,如果派生类中涉及到资源的管理,必须将基类的析构函数设置成虚函数,否则:就会造成内存泄漏(资源泄漏)】 - 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是 引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数是在编译阶段就生成的,一般情况下存在代码段 (常量区)的。
- C++菱形继承的问题?虚继承的原理?注意:这里不要把虚函数表和虚基表搞混了。
- 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽象类体现出 了接口继承关系。