【C++】多态

在这里插入图片描述

write in front
所属专栏: C++学习
️博客主页:睿睿的博客主页
️代码仓库:VS2022_C语言仓库
您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我你们将会看到更多的优质内容!!


文章目录

  • 一.多态的概念:
  • 二.多态的定义及实现:
    • 1.多态的构成条件:
      • 1.1虚函数:
      • 1.2重写:
      • 1.3指针调用或引用调用:
    • 2.多态的一些特殊情况:
      • 2.1.重新虚函数virtual:
      • 2.2.协变:
      • 2.3.析构函数的重写:
    • 3.C++11 override 和 final:
      • 3.1override:
      • 3.2.final:
      • 设计一个不想被继承的类,如何设计:
        • 1.基类构造函数(或者析构函数)私有 (C++98)
        • 2.在基类加个final (C++11)
    • 4. 重载、覆盖(重写)、隐藏(重定义)的对比
  • 三.多态的原理:
    • 1.虚函数表
    • 2.多态原理:
    • 动态绑定与静态绑定:
  • 五.单继承和多继承关系的虚函数表
    • 1.单继承:
    • 2.多继承:
  • 总结:

一.多态的概念:

  通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如下面这两个例子,不同对象去完成同一件事情,结果是不一样的:
【C++】多态_第1张图片

二.多态的定义及实现:

1.多态的构成条件:

 在继承中要构成多态还有两个条件:

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  2. 必须通过基类的指针或者引用调用虚函数

1.1虚函数:

  先来说第一个,虚函数的构成就是在一个类成员函数(必须是一个类的成员函数才可以,普通函数加了会报错)前面加virtual即可,但是这个virtual虚拟继承的virtual是没有关系的,一定不要搞混。

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

1.2重写:

  派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
  这里一定一定不要和继承的隐藏搞混!!!

举个例子:

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

  这种情况下,StudentBuyTicket函数就对PersonBuyTicket函数进行了重写。

1.3指针调用或引用调用:

  要形成多态,就只能通过指针或引用来调用,不然和之前的普通调用没什么区别:

多态调用:

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


void func(const Person& p)
{
	p.BuyTicket();
}
int main()
{
	func(Person());
	func(Student());
}

结果:

【C++】多态_第2张图片
普通调用(非指针引用):

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


void func(Person p)
{
	p.BuyTicket();
}
int main()
{
	func(Person());
	func(Student());
}

结果:

在这里插入图片描述
  在这里我们就会发现,多态调用时,不同的对象传过去,调用不同的函数。

  • 多态调用函数看的是指向的的对象
  • 普通调用函数看到是当前类型!!

2.多态的一些特殊情况:

  我们知道,虚函数的重写的要求就是虚函数+三同,但是下面的情况有点特殊:

2.1.重新虚函数virtual:

派生类的重写虚函数可以不加virtual(最好加上)

2.2.协变:

  协变,返回的值可以不同,但是要求返回值必须是父子关系指针和引用,并且父类只能返回父类指针,子类只能返回子类指针,并且必须同时返回指针或同时返回引用:

class A{};

class B: public A
{};

class Person 
{
	public:
		virtual	A* BuyTicket() const 
		{ 
			cout << "买票-全价" << endl;
			return 0;
		}
};
	
class Student : public Person 
{
public:
	virtual B* BuyTicket() const { 
		cout << "买票-半价" << endl;
		return 0;
	}
};


void func(Person p)
{
	p.BuyTicket();
}
int main()
{
	func(Person());
	func(Student());
}

2.3.析构函数的重写:

  编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

但是为什么要这样处理呢?

其实就是为了让析构函数构成重写。

但是为什么要对其进行重写呢?

我们来看看下面这个例子就懂了:

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;
		delete[] ptr;
	}

protected:
	int* ptr = new int[10];
};

int main()
{
	Person* a = new Person;
	delete a;
	Person* b = new Student;
	delete b;
}

  运行这段代码的结果如下:
在这里插入图片描述

  很显然,这里造成了内存泄漏,因为我们堆里面的Student对象没有清除掉。我们来看看重写析构函数后的样子:

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;
		delete[] ptr;
	}

protected:
	int* ptr = new int[10];
};

int main()
{
	Person* a = new Person;
	delete a;
	Person* b = new Student;
	delete b;
}

运行结果:
【C++】多态_第3张图片
  这就是对析构函数重写的意义所在,当我们希望对一个对象进行多态调用,在有动态内存管理的情况下调用析构函数,如果不重写,就会造成内存泄漏。

3.C++11 override 和 final:

  在C++11标准中引入的 override 和 final 关键字,用于增强面向对象编程的语法和语义,帮助开发者在继承和多态性方面更加清晰地表达意图和管理代码。它们分别用于指示虚函数的重写和禁止派生类进一步继承或函数的重写

3.1override:

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

class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
}; 

3.2.final:

  修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};

设计一个不想被继承的类,如何设计:

1.基类构造函数(或者析构函数)私有 (C++98)

  在C++98里面,我们将构造函数或析构函数放在private成员里面,在继承时没有对private的访问权,就无法调用父类的构造函数,就无法继承:

class A
{
	A()
	{}
public:
	static A CreateObj()
	{
		return A();
	}
};

class B:public A
{};

int main()
{
	//B bb;
	
	//想创建A对象如何创建?
	A a = A::CreateObj();
}
2.在基类加个final (C++11)

  在基类后面直接加final就无法继承了:

class A final
{};

class B:public A
{};

【C++】多态_第4张图片

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

【C++】多态_第5张图片

三.多态的原理:

1.虚函数表

  我们先来看看一道经典面试题:sizeof(Base)是多少?

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

【C++】多态_第6张图片

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析

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;
};
int main()
{
Base b;
Derive d;
return 0;
}

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表
  4. 虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr
  5. 总结一下派生类的虚表生成:
    a.先将基类中的虚表内容拷贝一份到派生类虚表中
    b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
    c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

  但是,虚函数表是存在哪里的呢?我们知道,虚函数存在代码段,通过下面这段代码可以验证,虚函数表存在常量区
【C++】多态_第7张图片

【C++】多态_第8张图片

2.多态原理:

  说了这么久,多态形成的原理到底是什么呢?我们以下面代码为例子:

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

	int _a = 1;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	int _b = 1;
};

void Func(Person& p)
{
	// 符合多态,运行时到指向对象的虚函数表中找调用函数的地址
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

通过监视窗口可以看到,他们两个有不同的虚函数表指针,这是很关键的一点。
【C++】多态_第9张图片

我们知道,构成多态有两个条件:

  1. 必须有虚函数,并且重新了父类虚函数
  2. 父类指针或引用调用虚函数。

  其实第一个条件,是为了让这个类里面的虚函数和普通函数不一样,虚函数会产生虚表指针,要调用虚函数时是通过这个虚表指针来找到虚表,在通过虚表调用虚函数;而普通函数不会,普通函数只是普通调用代码段里面的函数。而这个虚表指针就非常的重要,他就可以帮助我们形成多态。那么怎么帮助呢?第二个条件的价值就来了,我们通过父类的指针或引用指向子类,这个时候指针或引用要访问子类虚函数的时候,还是通过这个虚表指针访问那个虚函数,这样才形成了多态。

  说到这我们就不得不在说一下普通函数虚函数调用时候的区别了:

  • 不符合多态的(普通函数):编译时期就已经从符号表确认了函数的地址,直接call 地址。(这个地址也正好在虚表里面,至于为什么可以直接知道,我也不清楚)
  • 符合多态的 : 运行时期才到指向对象的虚函数表中找调用函数的地址,call的是eax,而eax存的就是虚函数表指针,所以多态调用不是在编译时确定。

【C++】多态_第10张图片

那么为什么必须是父类指针或引用呢?

  子类赋值给父类对象切片,不会拷贝虚表指针。如果拷贝了虚表指针,那么父类对象虚表指针是子类虚表指针,一个父类对象调用子类的函数,就非常的奇怪。

动态绑定与静态绑定:

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

其实上面的多态函数的调用和普通调用就很好的说明这一点。

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

1.单继承:

class Person {
public:
	virtual void Func1() 
	{
		cout << "Person::Func1()" << endl;
	}

	virtual void Func2() 
	{
		cout << "Person::Func2()" << endl;
	}

//protected:
	int _a = 0;
};
class Student : public Person {
public:
	virtual void Func3()
	{
		//_b++;
		cout << "Student::Func3()" << endl;
	}
protected:
	int _b = 1;
};

观察下图中的监视窗口中我们发现看不见func3。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。【C++】多态_第11张图片

那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数

typedef void(*Func_PTR)();

class Person {
public:
	virtual void Func1() 
	{
		cout << "Person::Func1()" << endl;
	}

	virtual void Func2() 
	{
		cout << "Person::Func2()" << endl;
	}

//protected:
	int _a = 0;
};

class Student : public Person {
public:
	virtual void Func3()
	{
		//_b++;
		cout << "Student::Func3()" << endl;
	}
protected:
	int _b = 1;
};


void PrintVFT(Func_PTR* table)
{
	for (int i = 0; table[i]; i++)
	{
		printf("[%d]->%p",i, table[i]);
		Func_PTR p = table[i];
		p();
	}
	printf("\n");
}


int main()
{
	Student ss;
	int ptr1 = *((int*)&ss);
	PrintVFT((Func_PTR*)ptr1);

	Person pp;
	int ptr2 = *((int*)&pp);
	PrintVFT((Func_PTR*)ptr2);
}

取出Person对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

  1. 先取对象的地址,强转成一个int*的指针
  2. 再解引用取值,就取到了对象头4bytes的值,这个值就是指向虚表的指针
  3. 再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
  4. 虚表指针传递给PrintVTable进行打印虚表
  5. 需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了

结果:
【C++】多态_第12张图片

2.多继承:

那么同样的问题在多继承里面会发生什么呢?

typedef void(*Func_PTR)();

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

void PrintVFT(Func_PTR* table)
{
	for (int i = 0; table[i]; i++)
	{
		printf("[%d]->%p",i, table[i]);
		Func_PTR p = table[i];
		p();
	}
	printf("\n");
}


int main()
{
	Derive dd;
	int ptr1 = *((int*)&dd);
	//int ptr2=  *((int*)(char*)&dd+sizeof(Base1));
	Base2* pp = &dd;
	int ptr2= *((int*)pp);

	PrintVFT((Func_PTR* )ptr1);
	PrintVFT((Func_PTR* )ptr2);

}

【C++】多态_第13张图片

我们会发现,派生类继承两个类,并且这两个类都有多态,那么他就会有两个虚函数表指针,如果在派生类里面多加一个虚函数,会加在第一个虚函数表里面。
并且我们会发现,对于两个父类在子类中同时构成多态的函数,子类可以重写,但是在重写之后会发现,这两个虚函数表里面重写的这个函数在代码段里面好像地址不统一?

其实原因很简单,我们通过以下代码来解释:
【C++】多态_第14张图片
当用父类Base2指针来调用虚函数形成多态的时候,此时调用的是Derive的函数,要访问Derive的所有成员,就必须用Derive的this指针,而此时如果直接通过Base2指针调用func1,this指针传的就是Base2类型的,所以要将其地址指向this指针。这就是其指向其他位置的原因,为了修正其位置,看汇编就会知道,最后调用的函数地址和Base1一样。而Base1就不用,因为this指针指向的和Base1指向的是同一个位置。

这里我们就要知道,内存里面不分类型,只分数据。

总结:

  更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

专栏订阅:
每日一题
C语言学习
算法
智力题
初阶数据结构
Linux学习
C++学习
更新不易,辛苦各位小伙伴们动动小手,三连走一走 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

你可能感兴趣的:(c++,c++,java,开发语言)