C++多态

一、概念

通俗来说,就是同一事物,在不同场景下表现出不同的形态,也可以说是去完成某个行为,当不同对象去完成时会产生不同的状态,它是面向对象编程领域的核心思想之一

二、多态的实现

多态的实现条件:

在继承体系 下
1、基类中必须有虚函数(被virtual关键字修饰的成员函数),在派生类中必须要对基类中的虚函数进行性重写
2、对于虚函数的调用,必须使用基类的指针或者引用调用虚函数
这两个条件,缺一不可,共同作用下构成了多态
在代码运行时,基类的指针或者引用指向了哪个类的对象,就调用哪个类的虚函数,如此,就是多态的体现

#include<iostream>
using namespace std;

class Base
{
public:
	//基类中定义虚函数
	virtual void TestFunc()
	{
		cout << "Base::TestFunc()" << endl;
	}
	int _b;
};
class Derived : public Base
{
public:
	//派生类中对虚函数进行重写
	virtual void TestFunc()override
	{
		cout << "Derived::TestFunc()" << endl;
	}
	int _d;
};
//用基类的指针或引用进行调用
void TestVirtualFunc(Base* pb)
{
	pb->TestFunc();
}

int main()
{
	Base b;
	Derived d;
	TestVirtualFunc(&b);
	TestVirtualFunc(&d);

	return 0;
}

多态
如上例子,就构成了多态

虚函数的重写(覆盖)

在派生类虚函数和基类虚函数的原型(返回值类型,函数名称,参数列表)完全一致的前提下,派生类重写基类的某个虚函数
在上述例子中,重写的具体表现如下图所示:
重写
【注意】
1、一定是派生类重写基类的虚函数
2、一个函数在基类中,另一个函数在派生类中
3、基类中的成员函数必须是虚函数
4、派生类同名成员函数前的virtual是否添加都可以
5、基类虚函数必须要与派生类虚函数的原型相同(除协变和析构函数重写)

重写的特例
1、协变(基类与派生类虚函数的返回值类型不同)

派生类重写基类虚函数时,与基类虚函数的返回值类型不同,即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变

class A
{};
class B : public A
{};

class Base
{
public :
	virtual A* Func()
	{}
};
class Derived : public Base
{
public:
	virtual B* Func()
	{}
};

ps:
上述代码中所示的协变,其中基类Base中虚函数的返回值不一定是A*,也可能是B*,或者其他,只要与派生类Derived中虚函数的返回值有一定的继承关系,就能够构成重写

2、析构函数(函数名字不同)

若基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名字不同,但是编译器会对析构函数的名称进行特殊处理,编译后的析构函数名字统一为destructor


class Base
{
public:
	virtual ~Base()
	{
		cout << "~Base()" << endl;
	}
};
class Derived : public Base
{
public:
	virtual ~Derived()
	{
		cout << "~Derived()" << endl;
	}
};

override和final (C++11中)

1、final:修饰虚函数,表示该虚函数不能再被继承

final是C++11中定义的继承控制关键字,它可以修饰虚函数,表示该虚函数不能被继承
若直接修饰基类,无任何意义。

2、override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译报错

抽象类

在虚函数的后面写上 “ =0 ”,则这个函数为纯虚函数,包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承


class Bike
{
public:
	virtual void take() = 0;
};
class MountainBike : public Bike
{
public:
	virtual void take()
	{
		cout << "MountainBike::Bike()" << endl;
	}
};
class RoadBike : public Bike
{
public:
	virtual void take()
	{
		cout << "RoadBike::Bike()" << endl;
	}
};
void TestBike()
{
	//抽象类不能实例化对象,但是可以创建该类的指针
	Bike* pm = new MountainBike;
	pm->take();

	Bike* pr = new RoadBike;
	pr->take();
}

多态的原理

说明:以下的多态原理部分都在VS2013环境下进行测试,分析

虚函数表

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

	int _b;
};
class Derived : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derived::Func1()" << endl;
	}

	int _d;
};
int main()
{
    cout << sizeof(Base) << endl;
    cout << sizeof(Derived) << endl;

    Base b;
    Base b1, b2;
    Derived d;
    
    return 0;
}

输出结果:
8
12

通过上述代码,先对基类和派生类取大小,会发现除了各自原有和继承的成员变量的大小外,分别都多了4个字节的空间
1、通过监视窗口可看出该4个字节的空间存放的一个指针_vfptr, 而_vfptr指向的是一个数组,该指针我们称之为虚函数表指针,该数组我们称之为虚函数表(虚表)
2、对比基类和派生类的虚表指针,发现它们指向的并不是同一片空间,说明基类和派生类有各自的虚函数表,并不共用
3、经过测试得出:同一个类定义的多个对象共用同一张虚表

虚表原理
通过对_vfptr在内存中的查看,如图a所示,发现该虚表中存放的地址只有第一个不同,其余两个完全一致,再联想到代码中派生类只是对基类中的Func1()进行了重写,那么不妨假设虚表中存放的是虚函数的入口地址,下面再在派生类中对Func3()进行重写,结果如图b所示,由此得出结论:
4、虚表中存放的是该类中虚函数的入口地址,且虚函数入口地址在虚表中的存放次序与基类中的声明次序一致
5、若派生类中重写了某基类虚函数,就用派生类自身的虚函数地址进行覆盖虚表中相同偏移量位置的函数地址
6、在此基础上,在派生类中新增加一个虚函数,经过检测验证得出:派生类新增的虚函数,按其在派生类中的声明次序增加到虚表的最后

基类和派生类虚表的构建如下

基类虚表构建
派生类虚表构建

问题

用代码的形式,证明虚表中的地址就是虚函数的入口地址

1、从对象前4个字节中获取表格地址_vfptr
2、从vfptr指向的空间中获取函数的入口地址

#include<iostream>
using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	virtual void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

	int _b;
};

class Derived : public Base
{
public:
	virtual void Func3()
	{
		cout << "Derived::Func3()" << endl;
	}

	int _d;
};
#include<string>

typedef void(*VFptr)();  //重命名,函数指针类型
void Print(Base& b, const string& s)
{
	cout << s << endl;
	//&b;  //指向对象本身
	//(int*)&b;  //指向对象的前4个字节
	//*(int*)&b;  //指向前4个字节中的内容
	VFptr* ptr = (VFptr*)(*(int*)&b);
	while (*ptr)  //VS2013编译器下的虚表以00 00 00 00结束
	{
		(*ptr)();
		++ptr;
	}
}


int main()
{
	Base b;
	Derived d;

	Print(b, "Base: _vfptr");
	Print(d, "Base: _vfptr");

	return 0;
}

输出结果:
Base: _vfptr
Base::Func1()
Base::Func2()
Base::Func3()
Base: _vfptr
Base::Func1()
Base::Func2()
Derived::Func3()

通过打印结果可以看出,同过虚表中的内容访问类的虚函数,所以,可以说明,虚表中存储的是虚函数的入口地址

动态绑定和静态绑定

1、动态绑定又称为前期绑定(晚绑定),在程序编译期间确定了程序的行为,也称为静态多态
2、动态绑定又称为后期绑定 (晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的行为,调用具体的函数,也称为动态多态

THE END

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值