1.多态的概念

多态,简称多种形态。

多态被分为编译时的多态,静态多态;运行时的多态,动态多态。

静态多态,通过函数重载、运算符重载和函数模板,在编译时根据参数类型确定调用的具体函数。

动态多态,运行时,通过基类指针/引用调用虚函数,根据对象实际类型执行对应的派生类方法,实现多态。

2.多态的定义

多态是一个继承关系下的类对象,在调用同一函数下,产生了不同情况的行为。

同一接口,产生不同行为。

2.1多态的构成条件

实现多态的两个必要重要条件。

1.必须是基类的指针或引用。

因为只有基类的指针/引用才能既指向基类又指向派生类对象。

2.被调用的函数必须是基类的虚函数,且该虚函数完成了重写/覆盖。

因为只有达到重写/隐藏时,才能实现不同的功能。

2.2虚函数,virtual

在类成员函数前加一个关键字virtual修饰,就变成虚函数了,非成员函数不能加。

//简单多态的实现
class person
{
public:
	//虚函数,用于实现运行时的多态,动态多态
	virtual void buytick() { cout << "person" << endl; }
};

2.3虚函数的重写/隐藏/协变

虚函数的重写/覆盖,当派生类有一个与基类完全相同的虚函数,函数名,返回类型,参数类型,称为派生类的虚函数重写/覆盖了基类的虚函数。

派生类对虚函数的重写不依赖于关键字virtual的显式重复声明,因为当基类声明了一个虚函数时,派生类会自动继承该函数的虚特性。

即便子类不写虚函数,派生类会直接继承基类虚函数

//简单多态的实现
class person
{
public:
	//虚函数,用于实现运行时的多态,动态多态
	virtual void buytick() { cout << "person" << endl; }
};

class student :public person
{
public:
	//基于基类的虚函数与子类的虚函数达到三重,构成重写状态
	//即便子类不写虚函数,派生类会直接继承基类虚函数
	virtual void buytick() { cout << "student" << endl; }
};

接下来看一道题。

下列程序输出的结果是什么

a.A->0                     b.B->0                   c.A->1                  d.B->1                 e.编译出错

class A
 {
 public:
 virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
 virtual void test(){ func();}
 };
 
 class B : public A
 {
 public:
 void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
 };
 
 int main(int argc ,char* argv[])
 {
 B*p = new B;
 p->test();
 return 0;
 }

在看这道题之前,我们需要再次理解重写是什么。

重写后的虚函数 = 基类的虚函数声明(含虚特性) + 派生类的函数体实现。

B是A的子类,两个类中的func构成重写关系,并且B继承了A中的test虚函数,当通过 B 类型指针p调用test()时,由于 B 未重写test,实际执行的是 A 的test函数。

又因为p是B*类型的指针,指向的对象是B对象,因此test函数内部会调用B对象中的func函数。

协变,指的是基类虚函数返回类型与派生类重写虚函数返回类型不一致。

即基类虚函数返回基类的指针/引用,派生类重写虚函数返回派生类指针/引用。

2.4动态失效的三个场景

当虚函数被static修饰时,无this指针无法访问虚函数表,不支持多态。

虚函数在构造/析构中调用,构造时虚表指针未完全初始化,析构时虚表指针已失效,此时调用虚函数会执行当前类(构造 / 析构所在类)的版本,而非实际对象类型的版本。

虚函数被inline修饰,inline仅为编译器建议,若虚函数被重写,编译器会忽略inline,但若未被重写且通过非多态方式调用,可能按普通内联函数处理,不触发多态。

2.5关键字override与final

关键字override是用来检查是否正确重写的,因为三重出错导致无法重写是不会在编译期间报出而是在运行当中。

class student :public person
{
public:
	virtual void buytick() override { cout << "student" << endl; }
};

final在继承中有所提到是用来声明一个类为最终类无法继承,既然不能继承那么也能让派生类不能重写这个虚函数,毕竟重写的首要条件便是继承。

class person final
{
public:
	virtual void buytick() { cout << "person" << endl; }
};

2.6重载/继承/隐藏/重写/覆盖

重载,在同一作用域下函数名相同,参数不同构成函数重载。

继承,在public继承下,子类对于父类的非私有成员的复用及拓展。

隐藏,在继承体系当中,子类与父类只要具有相同函数名就会隐藏父类的函数,若要使用则需要显式声明父类的类域,成员变量也是一样的。

重写/覆盖,两个函数分别在继承体系下父类和子类的两个不同的作用域下,父子类中虚函数,函数名,参数类型,返回值相同,或满足协变的称为重写。

2.7析构函数的重写

在继承篇中我们提到,析构函数名经过编译器特殊处理统一称为destructed()。

基类的析构函数为虚函数时,所以在派生类中析构函数只要定义,无论是否加关键字virtual修饰它都能与之构成重写关系。

int main()
{
    //a父类,b子类
	a* p1 = new a;
	a* p2 = new b;
	delete p1;
	delete p2;
	return 0;
}

3.纯虚函数与抽象类

纯虚函数就是虚函数后面加上个=0,它是可以被定义实现的。

包含纯虚函数的叫做抽象类,它不能实例化,如果派生类继承后不重写,该派生类也是抽象类,某种意义上就是在强迫你重写虚函数

class person
{
public:
	virtual void buytick() =0;
};

4.多态的原理

//简单多态的实现
class person
{
public:
	//虚函数,用于实现运行时的多态,动态多态
	virtual void buytick() { cout << "person" << endl; }
};

class student :public person
{
public:
	//基于基类的虚函数与子类的虚函数达到三重,构成重写状态
	//即便子类不写虚函数,派生类会直接继承基类的虚函数
	virtual void buytick() { cout << "student" << endl; }
};

void fun(person* ptr)
{
	//这里可以看到指针ptr在调用buytick但是跟ptr无关,而是由ptr指向的对象决定
	ptr->buytick();
}

int main()
{
	person a;
	student b;
	//通过不同对象
	fun(&a);
	fun(&b);
	return 0;
}

4.1虚函数表

虚函数表是一个数组,专门用来存储虚函数指针的,是一个指针数组,也可以叫做函数指针数组。

虚函数表是用来存储对象中所有虚函数的地址,同类型的对象共用同一张虚函数表,不同类型的对象即便是在继承体系下用不同的虚函数表。

派生类由继承下来的基类部分和自己构成,一般继承下来的基类部分是有着自己的虚函数表指针,但与基类的所指向的并不是同一个。

派生类中重写基类的虚函数,在虚函数表中对应的虚函数地址就会被覆盖为重写后虚函数地址。

派生类的虚函数表中包含,基类的虚函数地址,被覆盖后的虚函数地址,派生类自己的虚函数地址。

在VScode编译器中编译器一般会给虚函数表这个指针数组后面放一个标记位0x00000000,类似它对数组越界检查哨兵位机制。

虚函数与普通函数一样编译后是一段指令,会被存储在代码段中,只是虚函数地址会被存储在虚函数表中。

VScode编译器中,虚函数表被存储在代码段中。

以下这段代码可以对比验证。

#include<iostream>
using namespace std;

class person
{
	public:
		virtual void func()
		{
			cout<<"person"<<endl;
		}
};

class student:public person
{
	public:
		virtual void func()
		{
			cout<<"student"<<endl;
		}
};

void func(person* ptr)
{
	int x=10;
	ptr->func();
}
int main()
{
	person a;
	student b;
	func(&a);
	func(&b);
	int x=10;
	static int y=1;
	person* a1=&a;
	student* b1=&b;
	cout<<&x<<endl;
	cout<<&y<<endl;
	cout<<"虚表指针"<<*(int*)a1<<endl;
	cout<<"虚表指针"<<*(int*)b1<<endl;
	cout<<&person::func<<endl;
	cout<<&student::func<<endl;
	return 0;
}

4.2虚函数表指针

来看程序输出结果为多少

class Base
{
public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
protected:
 int _b = 1;
 char _ch = 'x';
};
int main()
{
 Base b;
 cout << sizeof(b) << endl;
 return 0;
}

因为一个类中所有的虚函数地址都要放在虚函数表中,而虚函数表的地址又通常是起始于对象头部,所以除了常规的成员_b,_ch以及内存对齐外我们还要计算这个虚函数表指针的大小。

5.静态绑定与动态绑定

对不满⾜多态条件(指针/引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤ 函数的地址,叫做静态绑定。

满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数 的地址,也就做动态绑定。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐