【C++】多态详解

22 篇文章 7 订阅

在这里插入图片描述

概念

多态,即多种形态,也就是说,不同的对象在完成某个行为时会产生不同的状态。
举个例子,在以前买票时,普通人正常买票,学生半价买票,军人优先买票。

定义和实现

多态的构成条件

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

而在继承中构成多态有两种条件:
·必须通过基类的指针或引用调用虚函数。
·被调用的函数必须是虚函数,且派生类需要重写基类的虚函数。

class Person
{
public:
	virtual void func()
	{
		cout << "普通人->正常买票" << endl;
	}
};

class Student : public Person
{
public:
	//子类必须重写父类的虚函数
	virtual void func()
	{
		cout << "学生->半价买票" << endl;
	}
};
//必须是父类的指针或引用去调用虚函数
//这里的参数类型不能是对象,否则是一份临时拷贝,则无法构成多态
void F(Person& ps)
{
	ps.func();
}

int main()
{
	Person ps;
	Student st;
	F(ps);
	F(st);
	return 0;
}

虚函数及其重写

虚函数,即被virtual关键字重写的类成员函数
重写(覆盖):派生类中有一个跟基类中完全相同的虚函数(三同:返回值、函数名、参数类型相同),这样则称子类重写了父类的虚函数。
【注意】若要构成多态,则父类函数必须加virtual关键字修饰,而子类由于继承父类则可以省去virtual关键字,但这样的写法不是很规范,不建议这样使用。

虚函数重写条件的两个例外

1.协变
重写的虚函数,返回值可以不同,但是返回值必须是父、子类的指针或引用类型。

class A {};
class B :public A {};
class Person
{
public:
	virtual A* func()
	{
		cout << "virtual A* func()" << endl;
		return new A;
	}
};
class Student : public Person
{
public:
	virtual B* func()
	{
		cout << "virtual B* func()" << endl;
		return new B;
	}
};

2.析构函数的重写
我们之前说过,编译器会自动将析构函数的名字处理为destructor,隐藏继承关系中的析构函数需要构成重写关系,比如说:

class Person
{
public:
	virtual ~Person()
	{
		//数据清理
	}
};
class Student : public Person
{
public:
	virtual ~Student()
	{
		//数据清理
	}
};
int main()
{
	Person* pps = new Person;
	Person* pst = new Student;
	//这里只有子类重写了父类的虚函数,
	//才能保证pps与pst指向的对象能够正确的调用析构函数
	delete pps;
	delete pst;
	return 0;
}

C++11关键字override与final

·final:修饰虚函数,表示该虚函数不能被重写

//final修饰类,则类A不能被继承
class A final
{
public:
	//virtual void func() final {}
};

//class B : public A
class B
{
public:
	//final 修饰虚函数,则虚函数func不能被重写
	//virtual void func() {}
};

·override:检查派生类虚函数是否重写了基类的某个虚函数,若未重写则编译报错。

class A
{
public:
	virtual void func() {}
};

class B : public A
{
public:
	//未重写则报错
	virtual void func() override {};
};

重载、重定义(隐藏)、重写(覆盖)的对比

在这里插入图片描述

抽象类

概念

虚函数后面加上=0就是纯虚函数,包含纯虚函数的类即为抽象类(接口类)。抽象类不能实例化出对象,派生类继承抽象类后若没有重写纯虚函数那么仍为抽象类,亦不能实例化出对象。
纯虚函数规范了派生类必须重写虚函数,并且更加体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW; 
	pBMW->Drive();
}

抽象类的意义:有的对象直接实例化对象一般没有意义,比如Car,一般不会直接用车实例化对象,而是具体到某个汽车品牌来实例化对象,此时就可以将Car设计为抽象类。

接口继承与实现继承

普通函数的继承就是接口继承,派生类可以使用基类的函数;而虚函数的重写则是实现继承,派生类继承的仅仅是基类的函数接口,目的是为了重写基类虚函数的函数体,达成多态。因此如果不实现多态,则不要将函数定义为虚函数。

原理

虚函数表

实际上对于定义了虚函数的类来说,有一个隐藏的虚函数表指针,指向一个虚函数表,这个虚函数表中存放着虚函数的地址:

class A
{
public:
	virtual void func() { cout << "A :: func() " << endl; }
protected:
	int _a;
	char _c;
};
class B : public A
{
public:
	virtual void func() { cout << "B :: func() " << endl; }
};
int main()
{
	A aa1;
	A aa2;
	B bb;
	cout << sizeof(aa1) << endl;
	return 0;
}

上面代码的运行结果为12,通过调用监视窗口可以看到aa对象中多了一个指针,这个指针指向一个函数指针数组,也就是虚函数表,这也是为什么aa大小为12字节。
在这里插入图片描述
并且可以看到的是:
·一个类的所有对象,共享一张虚表
·父子类无论是否重写虚函数,虚函数表都各自独立。
其次需要注意的是,只有虚函数会放入虚表中,普通函数并不会放入虚表中;虚表是一个函数指针数组,并且最后一般放着一个nullptr,

底层原理

那么,在完成上面的分析后,多态实现的原理究竟是什么呢?
实际上,多态调用虚函数是通过虚表指针实现的。在这里插入图片描述

静态绑定和动态绑定

其实虚表的构建是在对象实例化调用构造函数的初始化列表时实现的,这就是一种动态绑定,又称后期绑定(晚绑定)。

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

小结

·派生类的虚表生成:
1.派生类会先将基类的虚表拷贝一份到自己的虚表中
2.若派生类重写了基类的虚函数,那么派生类自己的虚函数将覆盖虚表中基类的虚函数
3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
·虚表存的是虚函数指针,不是虚函数
·虚函数和普通函数一样的,都是存在代码段的,只是其指针又存到了虚表中
·对象中存的不是虚表,存的是虚表指针。

单继承和多继承关系中的虚函数表

单继承中的虚函数表

先来看下面一段代码:

class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述
通过监视窗口可以发现监视器中d对象的虚表少了两个虚函数的地址,其实这里可以认为是vs编译器的小bug,那么我们通过函数将这两个隐藏的虚函数给打印出来。
思路就是虚函数表指针在vs环境下存储在类中的头4个字节,同时虚表以nullptr结尾,那么我们通过虚表指针调用虚函数即可。

typedef void(*VFPTR)();

void PrintVirtualTable(VFPTR vTable[])
{
	cout << "虚表地址:" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; i++)
	{
		printf("第%d个虚函数地址:%p\n", i, vTable[i]);
		VFPTR pf = vTable[i];
		pf();
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;
	PrintVirtualTable((VFPTR*)*((int*)(&b)));
	PrintVirtualTable((VFPTR*)*((int*)(&d)));
	return 0;
}

这里的思路就是:
1.取出d地址,强转成int类型
2.在解引用,这样就是d中头4个字节,即虚表指针的内容
3.将这个int
指针强转为VFPTR*类型,因为虚表就是一个存放VFPTR类型指针的数组。
4.虚表指针传递给PrintVTable进行打印虚表。
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有
放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
在这里插入图片描述
这样我们就能够看到被编译器监视窗口隐藏的func3和func4两个函数了。

多继承中的虚函数表

在多继承中,如果派生类自己有未重写的虚函数,那么这个这个虚函数将会放在哪里呢?

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	//Base1中的虚函数
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	//Base2中的虚函数
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

在这里插入图片描述
可以看到:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在这里插入图片描述

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值