一、多态的概念

多态,通俗来说就是多种形态。多态可分为编译时多态(静态多态)和运行时多态(动态多态)。

编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模版,它们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。

运行时多态,具体来说就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达成多种形态。

二、多态的定义和实现

2.1 多态的构成条件

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

实现多态有两个必须条件:

·必须是基类的指针或引用调用虚函数

·被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖

2.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。

注意:非成员函数不能加virtual修饰。

2.3 虚函数的重写/覆盖

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型函数名参数列表完全相同),称派生类的虚函数重写了基类的虚函数。

注:在重写基类虚函数时,派生类的虚函数不加virtual关键字仍然可以构成重写,但是该写法不是很规范,不建议这样使用。

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

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

void Func(Person* ptr)
{
	ptr->buyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(&p);
	Func(&s);

	return 0;
}
运行结果

可以看到,虽然都是Person指针ptr在调用buyTicket函数,但是和ptr没有关系,而是由ptr指向的对象决定的。

2.4 多态场景的一个选择题

下面程序输出的结果是什么()

A、A->0       B、B->1      C、A->1     D、B->0       E、编译出错        F、以上都不正确

#include <iostream>

using namespace std;

class A
{
public:
	virtual void func(int val = 1)
	{
		cout << "A->" << val << endl;
	}
	
	virtual void test()
	{
		func();
	}
};

class B
{
public:
	virtual void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};


int main()
{
	B* p = new B;
	p->test();
	
	return 0;
}

答案是B

首先要思考的问题是,是否构成多态?我们可以看到,是一个B*类型的指针p去调用test函数,那么,test函数参数部分隐藏的this指针是A*还是B*呢?有人可能会这么想,B类继承了A类,所以是B*类型的。这种想法是错误的,继承不是把基类成员拷贝一份到派生类当中,在编译时,派生类对象调用成员函数是先在派生类中查找,再去基类中查找。所以这里的this指针是A*类型的,也就是基类的指针。其次我们发现,虚函数完成了重写。两个条件都满足,所以构成多态,也就是说,这里调用的是B::func()。

那么看到这里,有人会想,那不应该是选D吗,怎么会选B呢?注意,构成多态时,重写的是虚函数的实现,所以这里val的值才会是1而不是0(如下图)

2.5 虚函数重写的一些其他问题

2.5.1 协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。协变的实际意义不大,了解即可。

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class Person
{
public:
	virtual A* buyTicket()
	{
		cout << "全价买票" << endl;
		return nullptr;
	}
};

class Student : public Person
{
public:
	virtual B* buyTicket()
	{
		cout << "半价买票" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->buyTicket();
}

int main()
{
	Person p;
	Student s;
	Func(&p);
	Func(&s);

	return 0;
}

2.5.2 析构函数的重写

我们先来看下面这段代码:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class B :public A
{
public:
	~B()
	{
		cout << "~B()->delete" << _p << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};


int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1;
	delete p2;

	return 0;
}

运行结果如下图所示:

我们发现,只析构了A类,却没有析构B类,这不就造成内存泄漏了吗?那为什么没有调用到B的析构呢?首先,我们知道delete操作符本质是先调用对应的析构函数,再调用operator delete(p1)。我们希望指向谁就调用谁的析构函数,也就是多态调用。而编译器对析构函数的名称统一处理成了destructor,所以基类的析构函数加了virtual关键字,派生类的析构函数就构成了重写。

#define  _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

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

class B :public A
{
public:
	virtual ~B()
	{
		cout << "~B()->delete" << _p << endl;
		delete[] _p;
	}
protected:
	int* _p = new int[10];
};


int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1;
	delete p2;

	return 0;
}

三、纯虚函数和抽象类

在虚函数的后面加上 =0,则这个函数为纯虚函数。纯虚函数不需要定义实现,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐