【C++】多态

【C++】多态

概念

通俗来讲,多态就是指多种形态,不同对象去做同一件事,产生的状态也不同

例如买票,普通人买票,全价买票;学生买票,半价买票

在这里插入图片描述


定义

多态的实现是建立在继承的基础上的。拥有不同继承关系的类对象,调用同一函数,产生不同的结果。

例如,Student继承了PersonStudent半价买票,而Person全价买票

构成

多态构成由两个必要条件:

  1. 必须通过父类指针引用去调用虚函数
  2. 虚函数的重写:子类必须对父类的虚函数进行重写,父类和子类的虚函数要求三个相同(函数名,参数,返回值)

虚函数

虚函数就是用关键字virtual修饰的类成员函数。注:这里的virtual和虚继承的virtual无任何关联

class Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Person->全价" << endl;
	}
};

重写(覆盖)

虚函数的重写,子类中有一个与父类完全相同的虚函数(三同,函数名、参数、返回值相同),子类就会将父类的虚函数进行重写

class Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Person->全价" << endl;
	}
};

class Student : public Person
{
public:
    // 对父类的虚函数进行了重写
	virtual void BuyTickets()
	{
		cout << "Student->半价" << endl;
	}
};

调用的时候必须用父类的指针或者引用

// 父类指针调用
void func1(Person* p)
{
	p->BuyTickets();
}
// 父类引用调用
void func2(Person& p)
{
	p.BuyTickets();
}

int main()
{
	Person p;
	Student s;

	func1(&p);
	func1(&s);

	func2(p);
	func2(s);
}

运行结果:

在这里插入图片描述

虚函数重写的例外

虚函数重写要求子类和父类虚函数三同(函数名、参数、返回值),但是有三个例外

1.协变(子类和父类虚函数的返回值类型不同)

子类和父类虚函数的返回值类型可以不同,可以返回父子类关系的指针或引用。即父类虚函数返回父类的指针或引用,子类虚函数返回子类的指针或引用

class Person
{
public:
	// 返回父类引用
	virtual Person& BuyTickets()
	{
		cout << "Person->全价" << endl;
		return *this;
	}
};

class Student : public Person
{
public:
	// 返回子类引用
	virtual Student& BuyTickets()
	{
		cout << "Student->半价" << endl;
		return *this;
	}
};

另外,返回其他父子类关系的指针或引用,也可以构成协变

// AB父子类关系
class A {};
class B : public A {};

class Person
{
public:
    // 返回A类引用
	virtual A& BuyTickets()
	{
		cout << "Person->全价" << endl;
	}
};

class Student : public Person
{
public:
    // 返回B类引用
	virtual B& BuyTickets()
	{
		cout << "Student->半价" << endl;
	}
};

2.析构函数的重写(子类与父类的析构函数名字不同)

在学习继承时我们就知道,析构函数表面上的名字无论是什么,最终都会被改为destructor。所以在析构函数重写时,子类与父类的析构函数名可以不同

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

class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

所以我们现在知道了,析构函数的名字暗中被改为destructor,就是为了实现虚函数重写,为了实现多态。

可是为什么析构函数一定要实现多态呢?来看一下这种情况:

父类和子类中的析构函数没有构成重写,子类中开辟了空间,子类析构函数将开辟的空间清理掉

class Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Person->全价" << endl;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Student->半价" << endl;
	}

	~Student()
	{
		// 清理开辟的空间
		cout << "delete[]" << _ptr << endl;
		delete[] _ptr;
		cout << "~Student()" << endl;
	}
protected:
	// 开辟空间
	int* _ptr = new int[10];
};

实例化一个Person对象和一个Student对象,将他们的指针赋给Person指针,再delete这两个对象

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
}

看看运行结果:

在这里插入图片描述

我们知道,delete自定义类型对象时,会调用相应的析构函数,而上面的运行结果显示p1p2走的都是Person类的析构,p2指向的Student对象没有析构,可能导致内存泄漏

不满足多态时,调用函数时看的是指针类型,指针是什么类型就调用谁的。如p1p2都是Person类型,所以调用的都是Person的析构函数

满足多态时,要看指针指向的类型,指向的是什么类型就调用谁的

在上面的情况中,我们期望的是:指向父类,那就调用父类的析构;指向子类,那就调用子类的析构。所以就需要用到多态,需要子类与父类的析构函数构成虚函数重写。这就是为什么析构函数的名字会被改为destructor

class Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Person->全价" << endl;
	}
	// 析构函数重写
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Student->半价" << endl;
	}
	// 析构函数重写
	virtual ~Student()
	{
		// 清理开辟的空间
		cout << "delete[]" << _ptr << endl;
		delete[] _ptr;
		cout << "~Student()" << endl;
	}
protected:
	// 开辟空间
	int* _ptr = new int[10];
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
}

在这里插入图片描述

由上我们可以做出总结:在继承中,析构函数尽量写为虚函数,防止发生内存泄露

3.子类的虚函数可以不加virtual

只要父类的虚函数加了virtual,子类虚函数可以不加virtual,如下

class Person
{
public:
	// 父类虚函数一定加 virtual
	virtual void BuyTickets()
	{
		cout << "Person->全价" << endl;
	}
};

class Student : public Person
{
public:
	// 子类虚函数可以不加 virtual
	void BuyTickets()
	{
		cout << "Student->半价" << endl;
	}
};

在这里插入图片描述
但是实际中为了代码可读性,还是建议加上


C++11 final与override

C++11 提供了两个关于重载的关键字:finaloverride

被final修饰的虚函数,无法被重写

class Car
{
public:
    // 被final修饰,无法被重写
	virtual void drive() final
	{
		cout << "Car->drive()" << endl;
	}
};
class Benz : public Car
{
public:
	virtual void drive()
	{
		cout << "Benz->drive()" << endl;
	}
};

在这里插入图片描述

此外,被final修饰的类,无法被继承

class Car final   // final修饰类
{
public:
	virtual void drive() final
	{
		cout << "Car->drive()" << endl;
	}
};
class Benz : public Car
{
public:
	virtual void drive()
	{
		cout << "Benz->drive()" << endl;
	}
};

在这里插入图片描述

override可以检查子类虚函数是否重写了父类的某个虚函数,没有重写就报错

class Car
{
public:
	virtual void drive()
	{
		cout << "Car->drive()" << endl;
	}
};
class Benz : public Car
{
public:
    // 参数不同,无法构成重写
	virtual void drive(int i) override
	{
		cout << "Benz->drive()" << endl;
	}
};

在这里插入图片描述

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

重载

  1. 两个函数在同一作用域
  2. 函数名相同,参数不同

重写(覆盖)

  1. 一个函数在父类作用域,一个函数在子类作用域
  2. 函数三同:函数名、参数、返回值相同。协变(返回值不同)、析构函数(函数名不同)例外
  3. 必须都是虚函数

重定义(隐藏)

  1. 一个函数在父类作用域,一个函数在子类作用域
  2. 函数名相同
  3. 父类和子类的同名函数不构成重写就是重定义

抽象类

在虚函数的后面加上=0,这个虚函数就变为了纯虚函数,包含纯虚函数的类称为抽象类(接口类)抽象类不能实例化对象。继承了抽象类的子类也不可以实例化对象,只有将父类的纯虚函数重写,子类才可以实例化对象。

也就是说,纯虚函数规定子类必须进行重写,同时也体现了接口继承

class Car  // 抽象类
{
public:
	// 纯虚函数
	virtual void drive() = 0
	{
		cout << "Car->drive()" << endl;
	}
};
int main()
{
	Car c; // 尝试实例化
}

抽象类无法实例化:

在这里插入图片描述

class Benz : public Car  // 继承了抽象类
{
};

int main()
{
	Benz b; // 尝试实例化
}

子类也无法实例化:

在这里插入图片描述

子类将父类的纯虚函数重写

class Benz : public Car
{
public:
	virtual void drive()
	{
		cout << "Benz->drive()" << endl;
	}
};
int main()
{
	Benz b; // 尝试实例化
	b.drive();
}

可以实例化:

在这里插入图片描述

抽象类虽然不可以实例化,但是可以作为指针接收子类对象

在这里插入图片描述

实现继承与接口继承

实现继承

普通函数的继承就是一种实现继承,子类继承了父类的函数,可以使用函数,继承的是父类函数的实现

接口继承

虚函数的继承是一种接口继承,子类继承的是父类函数的接口,对父类函数的实现进行重写,覆盖了父类函数的实现。继承接口的目的是为了实现多态,如果不实现多态,那就不要将函数写成虚函数


多态的原理

虚表

此时有以下类

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

常见笔试问题:sizeof(Base)是多少?

4吗?既然这个题都这样问了,大概率不是4,直接来看结果:

在这里插入图片描述

Base的大小除了int4字节,还多出来4字节,我们自然就联想到这与虚函数有关

我们通过调试来看一下:

在这里插入图片描述

可以看到,在对象的前面,多了一个__vfptr指针,我们称这个指针为虚函数表指针,虚函数表中存的是虚函数的地址,虚函数表简称虚表

在这里插入图片描述

我们再改造一下上面的代码:将虚函数func改名为func1,添加虚函数func2,添加普通函数func3;添加子类Derive继承Base,并重写func1

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1()" << endl;
	}
	virtual void func2()
	{
		cout << "Base::func2()" << endl;
	}
    // 普通函数
	void func3()
	{
		cout << "Base::func3()" << endl;
	}
protected:
	int _b = 1;
};

class Derive : public Base
{
public:
    // 重写
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
protected:
	int _d = 2;
};

在这里插入图片描述

通过上图,func1func2进入虚表,而func3没有进入虚表,可以得知普通函数的地址并不会放入虚表

再来看子类内部的情况:

在这里插入图片描述

在这里插入图片描述

可以得知:

  1. 子类继承了父类的虚函数表,但是这个虚表又和父类的虚表不是同一张虚表,是父类虚表的拷贝。这点从两个__vfptr的值不同可以看出
  2. func1完成了重写,所以d的虚表中存的是重写的Derive::func1,所以虚函数的重写也叫覆盖,覆盖的是虚表中的虚函数。重写是语法层面的的叫法,覆盖是原理层面的叫法
  3. 子类的普通函数和继承父类的普通函数不会进入虚表,例如func3

此外,子类自己的虚函数会按照声明顺序放到虚表的最后

class Derive : public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1()" << endl;
	}
	// 子类自己的虚函数
	virtual void func4()
	{
		cout << "Drive::func4()" << endl;
	}
protected:
	int _d = 2;
};

不过在vs下的监视窗口并不显示func4进入了虚表

在这里插入图片描述

我们可以用内存窗口查看:

在这里插入图片描述

我们知道前两个是func1func2,第三个也不知道是不是func4

虚函数表的本质是一个函数指针数组,一般情况这个数组最后面放了一个nullptr。我们可以将虚表内的指针拿出来调用,这样就可以验证func4

我们先将函数指针typedef一下,要不然写起来有点麻烦

typedef void(*VFPTR)();// 将函数指针重命名为VFPTR

接着写一个打印函数指针数组的函数

void PrintVFPTR(VFPTR* vf)// 传递的是函数指针数组的首地址,即虚表指针
{
	for (int i = 0; i < 3; i++)
	{
		// 打印函数的地址
		printf("%p->", vf[i]);
		// 取出函数地址,调用函数
		VFPTR pvf = vf[i];
		(*pvf)();
	}
}

现在的问题就是,如何取到虚表指针。虚表指针是对象的第一个成员,指针大小是4字节,也就是我们要取到对象的前4字节

在这里插入图片描述

直接将对象强转为int可不可以?

在这里插入图片描述

显然不可以,有关联的类型才可以相互转换。但是,指针可以随便相互转换

我们可以将Derive*转换为int*

VFPTR* ptr = (int*)&d;

再将int*解引用,这样我们就得到了d的4个字节

VFPTR* ptr = *((int*)&d);

还有一点:整型和指针可以相互转换,因为整型表示大小,4个字节;指针表示编号,也是4个字节。将得到的4字节转为我们需要的VFPTR*就搞定了

VFPTR* ptr = (VFPTR*)(*((int*)&d));

下面直接测试:

在这里插入图片描述

可以看到,子类对象d的虚表中确实有自己的虚函数func4

多态的原理

了解了前置知识,我们可以看多态的原理了

既然我们花了很多篇幅来讲解虚表,那就说明虚表是实现多态的关键

依然是PersonStudent买票的例子

class Person
{
public:
	virtual void BuyTickets()
	{
		cout << "Person->全价" << endl;
	}
protected:
	int _p = 1;
};

class Student : public Person
{
public:
	void BuyTickets()
	{
		cout << "Student->半价" << endl;
	}
protected:
	int _s = 2;
};

void func(Person& p)
{
	p.BuyTickets();
}
int main()
{
	Person p;
	func(p);

	Student s;
	func(s);
	return 0;
}

满足多态时,调用函数会根据指针或者引用找到指向的对象,然后到对象的虚表中找对应的虚函数进行调用

核心就是:指向谁,就调用谁的虚函数

指向父类,调用父类的虚函数

在这里插入图片描述

指向子类,调用子类的虚函数

在这里插入图片描述


不满足多态,也就是普通的函数调用,在编译阶段就确定了函数的地址

满足多态的条件时,函数地址就不是在编译阶段确定了,而是在运行阶段。运行时,到对象的虚表找对应虚函数的地址

int main()
{
	Student s;
	// 多态调用
	func(s);
	// 普通函数调用
	s.BuyTickets();
	return 0;
}

在这里插入图片描述

call可以看出,多态和普通函数调用确实是不同的


多继承的虚函数表

涉及到多继承就让人头大,接下来我们看一下多继承中的虚表是什么样的

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

可以预测,Derive对象会继承Base1Base2的虚表,有两张虚表,重写的虚函数会覆盖虚表中的虚函数。那么子类自己的虚函数func3会怎样呢?是在两张表中都放一个,还是只放一张表?可以用打印虚表数组的方式查看

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;
    // 查看d中的Base1
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	// 查看d中的Base2
	Base2* pf = &d;
	VFPTR* vTableb2 = (VFPTR*)(*(int*)pf);
	PrintVTable(vTableb2);
	return 0;
}

结果:

在这里插入图片描述

通过结果可以看出:多继承子类的未重写虚函数会放在第一个继承父类部分的虚表中

本来还有菱形继承和菱形虚拟继承的部分,但是写到这里实在不想写了,这部分太恶心人了。而且实际中不建议也不会用菱形继承和菱形虚拟继承,所以就到这里吧

结束,再见 😄

  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值