【C++】多态

?个人主页:平凡的小苏
?学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘
?C++专栏C++内功修炼基地
> 家人们更新不易,你们的?点赞?和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。 欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!

在这里插入图片描述

一、多态的概念

1.1、概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票

1.2、多态的定义及实现

1.2.1、多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价

在继承中构成多态需要两个必要条件

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

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

#include<iostream>
using namespace std;
class Person 
{
public:
	virtual	void BuyTicket() const { cout << "买票-全价" << endl; }
};

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

void func(const Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person pp;
	func(&pp);

	Student st;
	func(&st);

	return 0;
}
  • 多态,不同对象传递过去,调用不同函数

  • 多态调用看的指向的对象

  • 普通对象,看当前者类型

在这里插入图片描述

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

1.3、虚函数重写的例外

1.3.1协变

  1. 协变(基类与派生类虚函数返回值类型不同)

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

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

class Person 
{
public:
    virtual A* f() {return new A;}
};
class Student : public Person 
{
public:
    virtual B* f() {return new B;}
};
  1. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则**,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成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* p = new Person;
	p->BuyTicket();
	delete p;

	p = new Student;
	p->BuyTicket();
	delete p; // p->destructor() + operator delete(p)

	// 这里我们期望p->destructor()是一个多态调用,而不是普通调用

	return 0;
}

注意:如果我们不让析构函数进行重写的话,它就构成了隐藏了,所以就调用不到子类的析构函数 这样会导致在子类进行堆申请的空间得不到释放,导致了内存泄漏。

1.4、C++11override和final

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
    virtual void Drive() final {}
};
class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};

2.final:修饰类,表示不能被继承

class A final
{
public:
private:
};

class B : public A
{};

int main()
{
	return 0;
}

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

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

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

在这里插入图片描述

二、多态的原理

多态的条件中:

2.0、父类的指针和引用

问题:为什么不是子类的指针和引用。为什么不是父类的对象

因为父类的指针和引用能够指向父类或者子类,而子类的指针和引用只能指向子类

#include<iostream>
using namespace std;

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

	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 BuyTicket() { cout << "买票-半价" << endl; }

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

int main()
{
	Person ps;
	Student st;
	st._a = 10;

	ps = st;
	Person* ptr = &st;
	Person& ref = st;

	return 0;
}

问题:为什么不是父类的对象

因为子类赋值给父类对象切片不会拷贝虚表,如果拷贝虚表那么父类的对象虚表中是父类虚函数还是子类虚函数就不确定了

注意:所以不能是父类的对象,而父类的指针和引用为什么可以呢,因为子类继承了父类的虚表当发生重写后,他就会指向子类的虚函数,如下图:

在这里插入图片描述

2.虚函数的重写

虚函数的重写是继承了接口,重写了实现。因为重写后,指向父类调用父类的函数,指向子类就可以调用子类的函数。

2.1、虚函数表

#include<iostream>
using namespace std;

typedef void(*FUNC_PTR) ();

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

	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 BuyTicket() { cout << "买票-半价" << endl; }

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

// 打印函数指针数组
// void PrintVFT(FUNC_PTR table[])
void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);

		FUNC_PTR f = table[i];
		f();
	}
	printf("n");
}
int main()
{
	Person ps;
	Student st;

	int vft1 = *((int*)&ps);
	PrintVFT((FUNC_PTR*)vft1);

	int vft2 = *((int*)&st);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

在这里插入图片描述

通过观察好像student里面没有func3,这时候我们需要通过内存窗口来查看它的地址

在这里插入图片描述

从图中可以看出多出了一个地址,我们有理由相信它是func3的地址,那么代码验证结果如下图所示:
在这里插入图片描述

那么最后一个全0是什么呢

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

总结

总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

2.2、虚表的存放位置

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

	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 BuyTicket() { cout << "买票-半价" << endl; }

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


int main()
{
	Person ps;
	Student st;

	int a = 0;
	printf("栈:%pn", &a);

	static int b = 0;
	printf("静态区:%pn", &b);

	int* p = new int;
	printf("堆:%pn", p);

	const char* str = "hello world";
	printf("常量区:%pn", str);

	printf("虚表1:%pn", *((int*)&ps));
	printf("虚表2:%pn", *((int*)&st));

	return 0;
}

在这里插入图片描述

由图可知:虚表我们更愿意认为它存在代码区,因为它的地址与代码区是最接近的。

2.3、动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载

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

2.4、多继承的虚函数表

#include<iostream>
using namespace std;

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 (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);

		FUNC_PTR f = table[i];
		f();
	}
	printf("n");
}

int main()
{
	Derive d;
	cout << sizeof(d) << endl;

	int vft1 = *((int*)&d);
	//int vft2 = *((int*)((char*)&d+sizeof(Base1)));
	Base2* ptr = &d;
	int vft2 = *((int*)ptr);

	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

上面的代码中:Derive对象的大小是多少了,如下图所示:

在这里插入图片描述

20是因为Derive继承了Base1和Base2的虚表指针,并且它们分别有一个变量,所以Base1是8,Base2是8,而Derive是4,加起来就为20

还有一个问题是:我们未重写了func3,那么func3是存在Base1的虚表离还是Base2的虚表里

在这里插入图片描述

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

三、抽象类

3.1、概念

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

class Person 
{
public:
    virtual void Func()=0
    {
        //纯虚函数一般只声明,不实现。因为没有对象
    }
};

子类继承了父类的纯虚函数,子类也变成了抽象类,同样不能实例化出对象。 除非子类重写纯虚函数,子类才能实例化出对象。

在这里插入图片描述

注意:抽象类的作用是强制子类进行重写。

四、关于多态的问答题

什么是多态?

多态指多种形态。不同的对象完成同一件事情,但是结果不同。例如公交刷卡行为:成人刷卡全价,学生刷卡半价。亦或是不同的客户来消费,金卡会员8折,银卡会员9折,普通会员无折扣。

什么是重载、重写(覆盖)、重定义(隐藏)?

函数重载:1.两个函数必须再同一个作用域2.函数名相同、参数列表不同,返回值没有要求

重写:1.两个函数必须位于子类和父类2.函数名、参数列表、返回值必须相同(协变除外)、两个函数均为虚函数

隐藏:1.两个函数必须位于子类和父类2.函数名相同,不构成重写就是隐藏

多态的实现原理?

对于多态的实现原理,必须先从构成多态的条件说起:
1、必须通过父类对象的引用或指针当做形参调用虚函数。2、子类必须完成对父类虚函数的重写且被调用的函数是虚函数。

子类和父类的虚函数表指针、虚函数表、重写的虚函数的地址均不相同,我们传入一个父类对象,它使用的是源自父类的虚函数,传入一个从子类切片而来的父类对象,这个对象中的虚函数是子类重写的虚函数。虽说这两个都是父类对象,但是对象体内的虚函数并不是同一个,所以会产生不同的行为,这便是多态的原理。

inline函数可以是虚函数吗?

inline可以是虚函数。调用时,如果不构成多态,这个函数就保持inline属性。如果构成多态,就不具备inline属性,因为多态是要在运行时去对象的虚函数表里面找虚函数,所以在编译时,不能使用inline进行展开。

静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,并且最好把基类的析构函数定义成虚函数。用于处理子类对象交给父类的指针管理的情况

对象访问普通函数快还是虚函数更快?

首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

什么是抽象类?抽象类的作用?

抽象类又称接口类。包含纯虚函数的类被称为抽象类,在虚函数后边加个 = 0,这个虚函数就被叫做纯虚函数抽象类不能实例化出对象。在现实世界中没有对应的实物,就可以定义为抽象类。例如职能类、Person类等。

抽象类体现接口继承的关系。子类继承抽象类后,也变成了抽象类。这就强制用户对纯虚函数进行重写,对虚函数的重写是一种接口继承,子类会继承虚函数的函数名及缺省值,但不会继承实现。