C++进阶:多态(二)

390 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

💦 虚函数

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
  • 虚函数就是被 virtual 修饰的类成员函数,它跟虚继承共用了一个关键字 virtual。
  • 注意虚继承和虚函数中的 virtual,并没有关联关系,就像取地址和引用没有半毛钱关系,并不是天下姓王的都是亲戚。

💦 虚函数的重写

//重写(覆盖)
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "正常排队-全价买票" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "正常排队-半价买票" << endl;
	}
};

//隐藏(重定义)
class A
{
public:
	void fun()
	{
		cout << "fun()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		cout << "fun(int i)" << endl;
	}
};
  • 构成多态的条件之一是虚函数的重写,而虚函数也有自己的规则,虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数,即派生类虚函数和基类虚函数的返回值类型、函数名、参数列表完全相同,就称子类的虚函数重写了基类的虚函数。

    注意区分隐藏的概念,隐藏是只要基类函数名和派生类函数名相同即是隐藏或重定义。

  • 虚函数要求三同,但是这三同有些例外,这就恶心了,具体例外看测试用例三四五。

✔ 测试用例一:

#include<iostream>
using namespace std;

//class A {};//AB为无关联的类
//class B {};
class A {};//AB为关联的父子类
class B : public A {};

class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "正常排队-全价买票" << endl;
		return new A;
	}
protected:
	int _age;
	string _name;
};
class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "正常排队-半价买票" << endl;
		return new B;
	}
protected:
	//...
};
void Func(Person& ptr)
{
	ptr.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}
  • 协变 (基类与派生类虚函数返回值类型不同),即重写的虚函数可以不同,但是返回值必须是父子类型指针或引用。

    如果返回值是普通没有关联的类,那么它既不满足三同、也不满足协变,会编译报错。

    在这里插入图片描述

    如果返回值是有关联的父子类,那么虽然它不满足三同,但是它满足协变这个例外,所以能构成多态。

    在这里插入图片描述

✔ 测试用例二:

#include<iostream>
using namespace std;

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

int main()
{
	//普通场景
	Person p;
	Student s;

	//new对象的特殊场景 
	Person* p1 = new Person;
	Person* p2 = new Student;
	
	delete p1;//p1->destructor() + operator delete(p1)
	delete p2;//p2->destructor() + operator delete(p2)

	return 0;
}
  • 析构函数的重写 (基类与派生类析构函数的名字不同)

    如果是普通的析构, 程序运行没有问题,这里生命周期结束,s 后定义,s 先析构,s 中分为为两个部分,先调用自己的析构,再去调用继承的父类的析构,随后再去调用 p 的析构;如果是虚函数的析构,可以看到结果同普通的析构。

    在这里插入图片描述

    虚函数的析构有什么意义 ❓

      普通场景下,虚函数是否重写都是 ok 的;new 对象的特殊场景下,Person 的指针 p1 指向 Person 的对象、Person 的指针 p2 指向 Student 的对象、delete Person 的对象、delete Student 的对象。这里 new Person 调用 Person 的构造函数、new Student 调用 Studnet 的构造函数 + Person 的构造函数都没有问题;这里 delete p1 期望的是 delete 调用 Person 的析构函数、delete p2 调用 Student 的析构函数 + Person 的析构函数,但是在继承中我们说过,在子类中要去显示的调用父类的析构函数,需要指定作用域,因为所有类的析构函数名都被处理成了 destructor(),所以子类和父类的析构函数构成隐藏关系。为什么它要对析构函数名作单独处理呢,因为如果这里不构成多态,调用时看的是指针的类型,那么这里 p1 和 p2 调用的都是 Person 的析构函数,此时就不对了。p1 没问题,但是 p2 指向的是一个子类对象,子类对象应该先调用子类的析构函数,再去调用父类的析构函数,万一子类对象中又去 delete,那么 Student 的析构函数没调到就有可能会出现资源泄漏。

    在这里插入图片描述

      所以这里 delete p1/p2 是想达到多态的场景,Person* 指向父类调父类,指向子类调用子类,上面已经满足多态的条件之一,通过基类的指针或者引用调用虚函数;但是并没有满足多态的条件之二,被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;要完成虚函数的重写有两个条件:它们必须是虚函数以及三同,析构函数没有返回值,也就不考虑协变了。这里的两个析构函数没有返回值、参数,函数名不相同,因为在这种场景下需要多态,所以编译器对它们进行了特殊处理,统一成 destructor(),所以这里我们对于这种场景是需要加上 virtual 的,所以 delete p1 指向父类,调用父类的虚函数,delete p2 指向子类,调用子类的虚函数,子类析构函数结束后,再调用父类的虚函数。

    在这里插入图片描述

✔ 测试用例三:

#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "正常排队-全价买票" << endl;
	}
protected:
	int _age;
	string _name;
};
class Student : public Person
{
public:
	void BuyTicket()//可以不加virtual
	{
		cout << "正常排队-半价买票" << endl;
	}
protected:
	//...
};
void Func(Person* ptr)
{
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(&ps);
	Func(&st);

	return 0;
}
  • 其实严格来说这里还有一个例外,子类中的重写函数可以不加 virtual,但是通常不建议这样做。

    在这里插入图片描述

    为什么子类重写时可以不加 virtual ❓

    在这里插入图片描述

      因为它的理解是认为你是先继承下来的,我是在重写你,继承后你都有虚函数属性了,我去重写你,加与不加都无所谓。主要的实用场景还是测试用例四中的问题, 如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名不同,看起来违背了重写的规则,其实编译器对析构函数名统一处理成了 destructor()。也就是说如果支持子类不加虚函数也构成重写的话,那么只要父类中析构函数是虚函数,析构函数就一定构成重写,之后的问题就不存在了。

      这种例外,无疑是让语法变的更重了,C++ 经常爱搞这种东西,已经见怪不怪了。

💦 静态多态和动态多态

有些书籍会把多态进行细分:

  • 静态多态是函数重载,调用一个函数,传不同的参数,就有不同的行为。
  • 动态的多态是调用一个虚函数,不同的对象去调用,就有不同的行为。