一、多态的概念
多态:通俗的来说,多态就是多种形态,具体就是去完成某个行为,当不同的对象去完成时的所产生的不同的状态。
就比如买票的行为,对于普通人是全价票,对于学生是按折扣买票,根据不同的对象所产生的状态也就不同。
再比如一个人在某个平台是否是会员,它能不能看相应的影视,也是多态的表现。
二、多态的定义以及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,但是产生了不同的行为。比如Student类继承了Person类,Person类对象全价买票,Student类对象折扣买票。
构成多态的两个必要条件:
- 必须通过基类的指针或者是引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须要对基类的虚函数进行重写。
构成重写和隐藏的区别:
- 不构成多态的情况下是隐藏,构成多态并且函数的返回值类型,函数名字,参数列表完全相同的才是重写。
- 重写是隐藏的一种特殊情况。
代码实例:
#include<iostream>
using namespace std;
class Person
{
public:
virtual void ButTicket()
{
cout << "全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void ButTicket()
{
cout << "折扣买票" << endl;
}
};
void Func(Person& p)
{
p.ButTicket();
}
void test()
{
Person ai;
Func(ai);
Student ra;
Func(ra);
}
int main()
{
test();
return 0;
}
2.2 虚函数
虚函数:被virtual修饰的类成员函数称为虚函数。
class Person
{
public:
virtual void ButTicket()
{
cout << "全价买票" << endl;
}
};
2.3 虚函数的重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回类型、函数名、参数列表完全相同),这个过程就成为子类的虚函数重写了基类的虚函数。
注意:派生类的重写虚函数可以不加virtual,但是建议加上virtual。
class Person
{
public:
virtual void ButTicket()
{
cout << "全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void ButTicket()
{
cout << "折扣买票" << endl;
}
};
void Func(Person& p)
{
p.ButTicket();
}
void test()
{
Person ai;
Func(ai);
Student ra;
Func(ra);
}
int main()
{
test();
return 0;
}
2.4 虚函数重写的两个例外
协变(基类和派生类虚函数返回值不同)
派生类重写虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,这种行为成为协变。class A { public: virtual A* func() { cout << "new A" << endl; return new A; } }; class B : public A { public: virtual B* func() { cout << "new B" << endl; return new B; } }; void test1() { A* a = new A; B* b = new B; a->func(); b->func(); }
析构函数的重写
如果基类的析构函数为虚函数,此时派生类的析构函数只要定义了,无论是否加virtual关键字都与基类的析构函数构成重写。
基类和派生类的析构函数的名字表面上不同,其实编译器会对析构函数的名称做特殊处理,编译后析构函数的名称统一处理成destructor。class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; void test2() { Person* p1 = new Person; Person* p2 = new Student; delete p1; delete p2; }
2.5 C++11 override和final
final:修饰虚函数,表示该虚函数不能再被重写
class Car { public: virtual void Drive() final { } }; class Benz : public Car { public: virtual void Drive() { cout << "Benz" << endl; } };
被修饰后的虚函数再次进行重写操作就会报错。如下所示:
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写就编译报错
class A { public: virtual void func() { } }; class B : public A { public: virtual void func() override { cout << "B" << endl; } };
##2.6 重载、重写(覆盖)、重定义(隐藏)三者对比
- 重载:
- 两个函数在同一作用域
- 函数名相同,参数不相同
- 重写(覆盖):
- 两个函数分别在基类和派生类的作用域
- 函数名/参数/返回值都必须相同(协变例外)
- 两个函数必须都是虚函数
- 重定义(隐藏):
- 两个函数分别在基类和派生类的作用域
- 函数名相同
- 两个基类和派生类的同名函数不构成重写就是重定义
三、抽象类
3.1 抽象类的概念
在虚函数的后面跟上 = 0,则这个函数就是纯虚函数。包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承的概念。
class Car
{
public:
virtual void Deive() = 0;
};
class Benz : public Car
{
public:
virtual void Deive()
{
cout << "Benz" << endl;
}
};
class BMW : public Car
{
public:
virtual void Deive()
{
cout << "BMW" << endl;
}
};
void test3()
{
Car* p1 = new Benz;
Car* p2 = new BMW;
p1->Deive();
p2->Deive();
}
3.2 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现,虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,**达成多态,继承的是接口。**所以如果不实现多态,就不要把函数定义成虚函数。
注意:普通函数函数名就是其地址,成员函数的地址要加上取地址符号。
四、多态的原理
4.1 虚函数表
我们来看以下的一段代码:
class Base
{
public:
virtual void func()
{
cout << "func" << endl;
}
private:
int _b = 1;
};
通过测试我们发现Base类的一个对象在64位的环境下是16个字节,这是因为出了b成员外,还多了一个_vfptr放在对象的前面(有些平台可能放在最后面,跟平台的实现相关),对象中存放的这个指针被称为虚函数表指针。(v代表virtual, f代表function)。
如下图所示:
一个含有虚函数的类里面都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。
注意:
- 虚函数表本质:函数指针数组
- 同一类型的对象共享虚表,对象中有虚表指针。
我们再来看下面的一段代码:
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;
};
void test5()
{
Base b;
Derive d;
}
通过测试发现:
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
- 基类对象b和派生类对象d的虚表是不一样的,在派生类中Fun1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法层的叫法,覆盖是原理层的叫法。
- Func2继承下来就是虚函数,所以放进了虚表中,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
- 虚函数表的本质是一个存放虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr。
- 派生类的虚表生成过程:
a. 先将基类中的虚表内容拷贝一份到派生类虚表中
b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c. 派生类自己增加的虚函数按其在派生类中的声明次序增加到派生类虚表的后面 - 虚函数和普通函数一样存在于代码段(常量区)中,虚表也是存在于代码段(常量区)中,虚表指针是存在对象里面的,虚表指针指向虚表地址,虚表中存的是虚函数指针,不是虚函数。
4.2 多态的原理
满足多态的以后的函数调用,不是在编译时确定的,是在运行起来后在对象中取找的,不满足多态的函数调用是编译时确定的。
4.3 动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载。
- 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c. 派生类自己增加的虚函数按其在派生类中的声明次序增加到派生类虚表的后面
6. 虚函数和普通函数一样存在于代码段(常量区)中,虚表也是存在于代码段(常量区)中,虚表指针是存在对象里面的,虚表指针指向虚表地址,虚表中存的是虚函数指针,不是虚函数。
4.2 多态的原理
满足多态的以后的函数调用,不是在编译时确定的,是在运行起来后在对象中取找的,不满足多态的函数调用是编译时确定的。
4.3 动态绑定与静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载。
- 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。