目录

一、什么是多态

二、多态的定义及实现

1.多态构成条件

2.虚函数的重写和协变

​编辑

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

 4.final 和 override

三、抽象类

协变

四.多态的原理

1.  虚函数表

2.   引用和指针如何实现多态 

3.虚函数表存放位置


一、什么是多态

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,不同的对象去完成时会 产生出不同的状态。基类的虚函数进行重写

二、多态的定义及实现

1.多态构成条件

在继承中要构成多态还有两个条件(二者缺一不可):

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{ 
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

接受对象为父类的指针或者引用,你传递的是父类就调用父类的函数,传递的是子类就调用子类的函数。

注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.虚函数的重写和协变

2.1协变

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

 2.2析构函数的重写  (析构函数名统一处理成destructor

如果析构函数不处理成virtual,看下面代码

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	~Student()
	{
		cout << "~student()" << endl;
	}
};

int main()
{
	
	Person* p = new Student;
	delete p;
	return 0;
}

 上面代码是将一个子类对象赋给一个父类的对象,但在析构的时候,只析构了父类的对象,就可能发生内存泄漏。

这是为什么呢? 

因为这里发生了隐藏,~Person()变为 this->destructor()  而~Student()为this->destructor() 

编译器将他们两个的函数名都统一处理成了destructor,因此调用的时候只看自身的类型,是Person就调用Person的函数,是Student就调用Student的函数,根本不构成多态,才会出现内存泄漏问题。

但如果加上virtual,就可以了

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	virtual ~Student()
	{
		cout << "~student()" << endl;
	}
};

int main()
{
	
	Person* p = new Student;
	delete p;
	return 0;
}

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

 4.final 和 override

1、final     

                 在添加父类虚函数后面添加final代表不能再被重写

                 final修饰类,代表不能被继承

2、override

                override代表必须要重写虚函数,如果没有重写便会报错

三、抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

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

只要类里面有一个有纯虚函数,就是抽象类,就无法实例化对象,间接强制派生类重写

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

协变

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

class A//基类A
{};
class B :public A//派生类B
{};
class Person {
public:
	virtual A* t() { return new A; }
	
};
class Student : public Person {
public:
	virtual B* t() { return new B; }
	
};
int main()
{
	Person* p = new Student;
	delete p;
	return 0;
}

四.多态的原理

1.  虚函数表

在对象b中有一个__vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。

虚函数会放到虚函数表中,普通函数不会,并且表里面的内容是一个数组,是函数指针数组

如果子类重写了父类的虚函数,子类重写的函数的地址就会覆盖父类在虚表中的函数的地址

如果没有重写,子类在虚表中的函数地址与父类中的函数地址相同

2.   引用和指针如何实现多态 

可以分析,为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?

传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去找到这个虚函数。

对于编译器来说,他并不清楚这个对象是子类的,还是父类的,只是单纯的去调用虚表中的虚函数而已。

3.虚函数表存放位置

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
 
void func()
{}
 
int main()
{
	Base b1;
	Base b2;
	static int a = 0;
	int b = 0;
	int* p1 = new int;
	const char* p2 = "hello world";
	printf("静态区:%p\n", &a);
	printf("栈:%p\n", &b);
	printf("堆:%p\n", p1);
	printf("代码段:%p\n", p2);
	printf("虚表:%p\n", *((int*)&b1));
	printf("虚函数地址:%p\n", & Base::func1);
	printf("普通函数:%p\n", func);
}

​vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节

b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址

虚函数和普通函数地址非常接近,存在于代码段中。

--------------------------------------------------------------------------------------------------------------------------------

 本文的讲解到此结束,谢谢大家的观看,有问题欢迎给我留评论。

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐