解密C++多态属性

Posted AllenSquirrel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了解密C++多态属性相关的知识,希望对你有一定的参考价值。

解密C++特性下的多态属性

  • 多态

什么是多态?

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

实现多态的前提条件:

  1. 多态属性产生在继承过程中
  2. 必须有虚函数(virtual关键字)
  3. 调用虚函数的类型必须是指针或引用
  4. 虚函数需要被重写
  5. 一般是通过父类指针或引用进行调用虚函数

根据以下代码测试可以发现,以上5点均被涉及,感兴趣可以仔细看一下代码,仔细体会5点实现条件

#include<iostream>
using namespace std;

class person {
public:
	virtual void tickets()
	{
		cout << "全价票" << endl;
	}
};

class student :public person
{
public:
	virtual void tickets()
	{
		cout << "半价票" << endl;
	}
};

void fun(person& pt)//父类引用调用
{
	pt.tickets();
}

void test()
{
	person p;
	student stu;
	fun(p);//传入父类引用,调用父类虚函数
	fun(stu);//传入子类引用,调用子类虚函数
}

int main()
{
	test();
	return 0;
}

测试结果:

总结:多态:看所指向的对象     非多态:看调用的类型

主要区分:虚函数是virtual+函数    虚函数重写要求:函数名,参数列表,返回值和父类虚函数完全相同

返回值可以不是同一个类型,但必须是有继承关系的指针或引用,这种称之为协变

#include<iostream>
using namespace std;

class A {

};
class B :public A
{

};

class person {
public:
	virtual A* tickets()
	{
		cout << "全价票" << endl;
		return new A;
	}
};

class student :public person
{
public:
	virtual B* tickets()
	{
		cout << "半价票" << endl;
		return new B;
	}
};

void fun(person& pt)//父类引用调用
{
	pt.tickets();//协变也构成虚函数
}

void test()
{
	person p;
	student stu;
	fun(p);//传入父类引用,调用父类虚函数
	fun(stu);//传入子类引用,调用子类虚函数
}

int main()
{
	test();
	return 0;
}

  • 析构函数的多态应用

#include<iostream>
using namespace std;
class person {
public:
	void tickets()
	{
		cout << "全价票" << endl;
	}
	~person()
	{
		cout << "~person" << endl;
	}
};
class student :public person
{
public:
	void tickets()
	{
		cout << "半价票" << endl;
	}
	~student()
	{
		if (_name)
		{
			delete[] _name;
			cout << "delete" << endl;
		}
		cout << "~student" << endl;
	}
private:
	char *_name = new char[100];
};
void test()
{
	person* p = new student;
	delete p;//父类指针,调用父类析构
	
}

int main()
{
	test();
	return 0;
}

根据上述测试结果,在子类中开辟资源空间,并自定义析构函数释放资源,通过父类指针指向子类对象时,delete父类指针时,仅调用父类析构函数,并没有调用子类析构函数,对此,采用虚函数方式实现多态,指针指向子类对象则调用子类析构函数

一般情况下,会将析构函数定义为虚函数,产生多态行为,此时小伙伴可能会有疑问:子类和父类的析构函数并不同名,不满足产生多态的条件

事实上,子类析构函数经过编译器编译处理后,其实在底层二者是同名的,满足多态产生的条件

注:overide关键字  子类虚函数后,用于检查是否重写,如果没有重写基类虚函数则报错

       final关键字,基类虚函数后,用于说明此虚函数在子类中不可被重写

  • 抽象类和纯虚函数

纯虚函数:就是在基类中的一个函数接口,没有函数结构体,将其=0

抽象类:包含纯虚函数,且抽象类不能实例化,

.抽象类的派生类如果不实现纯虚函数,它也是抽象类


class A {
	//定义纯虚函数
	virtual void fun() = 0;
};
class B :public A
{
	virtual void fun()
	{
		cout <<"B:fun()" << endl;
	}
};

class C :public A
{
	virtual void fun()
	{
		cout << "C:fun()" << endl;
	}
};
class D :public A
{

};

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

  • 虚函数表

虚函数表本质是一个存虚函数指针的指针数组,数组每一个元素都存放一个指针,这个数组最后面放了一个nullptr

__vfptr为虚函数表指针,指向虚函数表首地址,则__vfptr为一个二级指针(为对象前四个字节内容即为虚表指针地址9)

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 test()
{
	Base b;
	Derive d;
}

int main()
{
	test();
	return 0;
}

根据上述测试结果发现:

  1. 子类会继承父类的虚表,fun1基类定义为虚函数,子类并重写fun1,覆盖导致在虚函数表地址变化,fun2基类定义为虚函数,子类没有重写,在虚表中地址没有变化
  2. 子类重写的虚函数,对应的重写虚函数地址会覆盖掉虚表中对应虚表指针
  3. 只有虚函数,才会将函数地址放在虚表中
  4. 虚表存放在代码段,虚表指针存放在对象中

根据对虚函数表的理解,我们也就明白,为什么多态要看所指向的对象?

就是因为,虚表指针存放在对象中,子类和父类虚表不同,继承过程父类虚表会继承,同时虚表指针此时指向子类虚表,部分发生重写,导致继承的虚函数地址改变,所以通过虚函数表指针找到对应虚函数是重写覆盖后的地址,也就是说,父类指针指向子类对象,调用子类重写的虚函数。

总结起来四个步骤:

  1. 从对象中获取虚表指针
  2. 通过虚表指针找到虚表
  3. 从虚表中找到虚函数地址
  4. 执行虚函数的指令
  • 动态绑定与静态绑定

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

  • 单继承与多继承的虚函数表

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;
};

typedef void (*vfptr)();
void printvftable(vfptr vtable[])
{
	cout << "虚表地址:" << vtable << endl;
	vfptr* fptr = vtable;
	while (*fptr != nullptr)
	{
		(*fptr)();//不为空有虚函数存在  则执行虚函数
		++fptr;
	}
}

void test()
{
	Base b;
	Derive d;
	cout << "Base: " << endl;
	vfptr* vtable = (vfptr*)(*(int*)&b);
	printvftable(vtable);
	cout << "Derive: " << endl;
	vtable = (vfptr*)(*(int*)&d);
	printvftable(vtable);
}

int main()
{
	test();
	return 0;
}

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 printvftable(vfptr vtable[])
{
	cout << "虚表地址:" << vtable << endl;
	vfptr* fptr = vtable;
	while (*fptr != nullptr)
	{
		(*fptr)();//不为空有虚函数存在  则执行虚函数
		++fptr;
	}
}

void test()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	cout << "Base1: " << endl;
	vfptr* vtable = (vfptr*)(*(int*)&b1);
	printvftable(vtable);
	cout << "Base2: " << endl;
	vtable = (vfptr*)(*(int*)&b2);
	printvftable(vtable);
	cout << "Derive第一个虚表(base1): " << endl;
	vtable = (vfptr*)(*(int*)&d);
	printvftable(vtable);
	cout << "Derive第二个虚表(base2): " << endl;
	vtable = (vfptr*)(*(int*)(char*)(&d+sizeof(Base1)));
	printvftable(vtable);
}

int main()
{
	test();
	return 0;
}

根据上图,多继承关系下,两个虚表并非连续,存在一定的偏移量,这个偏移量即为继承的第一个父类大小sizeof(Base1)

子类新定义的虚函数,其函数指针存放在第一个虚表中

以上是关于解密C++多态属性的主要内容,如果未能解决你的问题,请参考以下文章

解密C++继承属性

C++面试总结更新

C++ 虚函数相关

C++之STL

C++ 继承&多态

[C++]面向对象语言三大特性--多态