多态性与虚函数

Posted twc829

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多态性与虚函数相关的知识,希望对你有一定的参考价值。

多态性是面向对象程序设计的基本特征之一。通过虚函数实现多态性。


  • 绑定方式与多态性

一、基本概念

多态性:方法和函数具有相同的名字,但有不同的行为。

绑定:将函数调用与某个函数体对应起来。

早期绑定,又静态绑定:在编译阶段完成绑定。

晚期绑定,又动态绑定:在运行阶段才完成绑定。

编译时多态性:在编译阶段确定名字的含义,在C++语言中通过函数重载和模板机制实现。

运行时多态性:在运行阶段才确定名字的含义,在C++语言中使用虚函数结合继承机制、动态绑定机制实现。

在C++语言中,函数调用的默认绑定方式是静态绑定,只有通过基类类型的引用或指针调用被指定为虚函数的成员函数才进行动态绑定。获得运行时多态性需同时满足以下条件:

a 有一个继承层次;

b 在基类中定义虚函数;

c 在派生类中对基类中定义的虚函数进行重定义;

d 通过基类指针(或基类引用)调用虚函数。

运行时多态性的基础是公有派生类对基类的类型兼容性,即指向基类对象的指针可指向该基类的公有派生类对象(类似地,基类对象的引用也可关联到该基类的公有派生类对象)。声明指针(引用)变量时指定的基类型称为指针(或引用)的静态类型,指针变量实际所指向(或引用变量实际所关联)的对象的类型称为指针(或引用)的动态类型。


二、多态性的作用

多态性使程序员可以使用相同的名字定义多个操作或函数,对语义相似的操作或函数采用同一标识符进行命名;增强程序的可修改性和可扩充性,在进行程序开发时可有效应对需求的变化。

面向对象程序设计方法围绕对象组织程序,即根据实体设计程序,以”对象“概念为核心,围绕对象和类(不是函数)组织代码,封装使得对象数据和操作的实现都被屏蔽起来,不受外界影响。




  • 虚函数

虚函数,指在类定义体中使用保留字virtual声明的成员函数。

多态类,指包含虚函数的类。

在公有继承层次中的一个或多个派生类中对基类中定义的虚函数进行重定义,再通过指向基类的指针(或基类引用)调用虚函数实现运行时多态性。

一、虚函数举例

除构造函数,任意非static成员函数都可根据需要设计为虚函数。

其中,构造函数不能设计为虚函数,是因为构造函数在对象完全构造之前运行,在构造函数运行时,对象的类型还是不完整的;static成员函数不能设计为虚函数,是因为static成员函数由类的所有实例共享,不属于某个对象。

举例1:

/*
	虚函数的动态绑定
*/
#include <iostream>
using namespace std;
class Base{
public:
	virtual void showName()
	{
		cout << "Base class" << endl;
	}
};
class DClass1 :public Base{
public:
	void showName()			// 继承成员的重定义
	{
		cout << "The first derived class" << endl;
	}
};
class DClass2 :public Base{
public:
	void showName()			// 继承成员的重定义
	{
		cout << "The second derived class" << endl;
	}
};
int main()
{
	Base bObj;
	DClass1 d1Obj;
	DClass2 d2Obj;

	Base *ptr;			// 定义指向基类的指针
	ptr = &bObj;
	ptr->showName();
	ptr = &d1Obj;		// 基类指针指向派生类对象
	ptr->showName();
	ptr = &d2Obj;		// 基类指针指向派生类对象
	ptr->showName();

	system("pause");
	return 0;
}

运行结果:

Base class

The first derived class

The second derived class

分析:

动态绑定能以统一的方式使用在不同类中定义的成员函数,即以同样方式(函数名)调用在派生类或基类中定义的虚函数;动态绑定在调用虚函数时无需关心对象的具体类型。

注意:若通过指针调用非虚成员函数,则该调用仅与指针的基类型有关,与该指针当前指向的对象无关。如去掉程序中基类Base中成员函数showName声明中的virtual保留字,则运行结果为:

Base class

Base class

Base class

因为ptr的基类型是Base,对非虚函数的调用采用静态绑定。

举例2:

/*
	通过基类引用实现动态绑定
*/
#include <iostream>
using namespace std;
class Base{
public:
	virtual void showName()
	{
		cout << "Base class" << endl;
	}
};
class DClass1 :public Base{
public:
	void showName()			// 继承成员的重定义
	{
		cout << "The first derived class" << endl;
	}
};
class DClass2 :public Base{
public:
	void showName()			// 继承成员的重定义
	{
		cout << "The second derived class" << endl;
	}
};
void printIdentity(Base& obj)
{
	obj.showName();
}
int main()
{
	Base bObj;
	DClass1 d1Obj;
	DClass2 d2Obj;

	printIdentity(bObj);
	printIdentity(d1Obj);
	printIdentity(d2Obj);

	system("pause");
	return 0;
}

注意:

基类中定义的虚函数,在派生类中仍为虚函数,不管在派生类中是否使用virtual保留字指定。

虚函数的指定只需在类定义体中的成员函数声明上加保留字virtual,在类定义体外部出现的成员函数定义上不能再用virtual,否则将出现编译错误。


二、使用虚函数的特定版本

在一些特定情况下,程序员可能希望覆盖上述默认虚函数调用机制,从而强制函数调用使用虚函数的特定版本。最常见的情况是为了在派生类虚函数调用基类中的相应版本,从而可以重用基类版本完成继承层次中所有类型的公共任务,而每个派生类型只在本类的虚函数版本中添加自己的特殊工作。

举例:

假设在一个大学学籍管理程序中需要对一般学生和研究生进行管理,为此可以定义一个学生类Student和一个研究生类GraduateStudent。因为研究生是一种学生,因此可通过继承学生类Student而定义研究生类GraduateStudent。

学生一般具有姓名、学号、专业、年级等基本信息,研究生除了具有学生的基本信息外,一般还具有导师、类别等信息。

假设类Student提供虚函数displayInfo显示学生对象的基本信息,类GraduateStudent重定义该displayInfo函数,以显示研究生对象的基本信息。

定义这两个函数:

class Student{
public:
	virtual void displayInfo()
	{
		cout << stuID << endl;
		cout << name << endl;
		cout << sex << endl;
		cout << major << endl;
	}
	// 省略其他成员的定义
};
class GraduateStudent: public Student{
public:
	void displayInfo()
	{
		Student::displayInfo();		// 调用基类的displayInfo函数输出基本信息
		cout << type << endl;		// 输出研究生特有的信息
		cout << advisor << endl;
	}
	// 省略其他成员的定义
};

注意:

在派生类虚函数中调用基类版本时,必须使用作用域分辨操作符。若缺少作用域分辨操作符,则函数调用会在运行时确定并将是一个自身调用,从而导致无穷递归。


三、虚析构函数

一般,继承层次的根类中最好要定义虚析构函数。因为在对指向动态分配对象的指针进行delete操作时,也就是对动态创建的对象进行撤销时,需要调用适当的析构函数以清除对象。

当被撤销的是某个继承层次中的对象时,指针的静态类型可能与实际上被撤销对象的类型不同,对这样的指针进行delete操作时,若基类中的析构函数不是虚函数,将会只调用基类的析构函数,而不调用派生类的析构函数,这可能导致不正确的对象撤销操作:派生类的析构函数没有被调用。

举例:

/*
	非虚析构函数的调用
*/
#include <iostream>
using namespace std;
class Base{
public:
	~Base()
	{
		cout << "Base destructor" << endl;
	}
};
class DClass :public Base{
public:
	~DClass()
	{
		cout << "Derived class destructor" << endl;
	}
};
int main()
{
	Base *ptr;			// 定义指向基类的指针
	ptr = new DClass;	// 动态创建派生类对象
	// 省略对ptr的使用
	delete ptr;			// 动态撤销派生类对象

	system("pause");
	return 0;
}

执行结果:

Base destructor

分析:

尽管指针ptr实际指向的是派生类DClass的对象,但实际执行的是撤销基类对象的操作,因此被调用的是基类Base的析构函数,而派生类DClass的析构函数并没有被调用。

若派生类的析构函数中没有什么实质性操作,不会有什么问题;但派生类的析构函数中有操作(如当派生类中含有指针成员时,析构函数中通常会对成员指针进行delete操作,以释放指针成员所指向的内存),则会因为对基类指针进行delete操作没有调用派生类的析构函数导致问题(如派生类对象的指针成员所指向的内存将得不到释放)。

若在基类中定义需析构函数,将避免出现上述问题,保证执行适当的析构函数。如在程序中将Base类的析构函数指定为虚函数,则运行结果:

Derived class destructor

Base destructor

注意:

派生类必须对想要重定义的每个继承成员进行声明。在派生类中对基类中定义的虚函数进行声明时,一般要函数的原型(包括函数名和形参列表)完全相同。

但这一规则有两个例外:一个是派生类的析构函数与基类的析构函数并不同名;另一个是若基类中虚函数返回的是某个类型X的指针(或引用),则派生类中的相应虚函数可以返回类型X的派生类的指针(或引用)。




  • 纯虚函数和抽象类

一、纯虚函数

继承机制表现的是事物(实体)类别之间的共性与个性的关系。因此,在一个继承层次中,基类表示了所有派生类所具有的共性,而每个派生类分别表示了本类所特有的个性。基类中的需函数标识的是某个共性操作,派生类中对虚函数的重定义体验了在派生类中该操作所具有的特殊性。若派生类对基类中的虚函数没有进行重定义,则使用基类中定义的版本。

但在许多情况下,对于基类中的虚函数我们无法给出明确定义,则使用纯虚函数指明在基类中无法给出某个虚函数的定义。

纯虚函数是一个在基类中声明的虚函数,但在基类中没有定义函数体,要求任何派生类都必须定义自己的版本。

声明纯虚函数的一般情形如下:

virtual 返回值类型 函数名(形参列表)=0;

即在一般虚函数的声明上加一个=0,以指明该函数是一个纯虚函数。


二、抽象类

1 定义

抽象类指包含纯虚函数的类。

2 特性

只能用作其他类的基类;

不能用于直接创建对象实例;

不能用作函数的形参类型、返回值类型;

不能用于强制类型转换;

可声明抽象类的指针和引用;

举例:

class Account{
public:
	virtual bool deposit(int account) = 0;
	virtual bool withdraw(int account) = 0;
	// 类中其他成员的定义省略
};
Account *ptr;				// 声明抽象类Account的指针
Account& fun3(Account& a);	// 抽象类的引用做函数的形参和返回值类型

// Account x;					// 声明抽象类Account的对象(创建抽象类的对象实例)
// Account fun1(int);			// 抽象类作为函数的返回值类型
// void fun2(Account a);		// 抽象类作为函数的形参类型

注意:

若派生类继承某个抽象类,但派生类并没有对抽象基类中的全部纯虚函数进行重定义,则该派生类也是一个抽象类。


以上是关于多态性与虚函数的主要内容,如果未能解决你的问题,请参考以下文章

C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表

C++ 多态 : 虚函数静态绑定动态绑定单/多继承下的虚函数表

多态性与虚函数

C++之多态性与虚函数

多态性与虚函数

C++多态性与虚函数