c++程序设计(第3版)第十二章笔记

多态性的概念

多态性是面向对象程序设计的一个重要特征,如果一种语言只支持类而不支持多态,是不能够称为面向对象的语言的,只能说是基于对象的,如VB。c++支持多态并且能够实现多态性,利用多态性可以设计和实现一个易于扩展的系统。

在面向对象方法中一般是这样来描述多态的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去相应共同的消息。所谓消息就是调用函数,不同的行为就是不同的实现,即执行不同的函数。

其实我们之前已经接触过了多态性,例如运算符的重载、函数的重载等。

在c++中,多态性的表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。

多态分为两类:静态多态性和动态多态性

静态多态性

静态多态性是根据函数重载实现的。有函数重载和运算符重载形成的多态性就是静态多态性,要求在程序编译时就知道调用函数的全部信息,因此,在程序编译时系统就能决定要调用的时哪儿个函数。静态多态性又称为编译时的多态性。静态多态性的函数调用速度快、效率高,但缺乏灵活性,在程序运行前就已决定了执行的函数和方法。

动态多态性

动态多态性的特点是不在编译时确定调用的是哪儿个函数,而是在程序运行过程中才动态地确定操作所针对的对象。它又称运行时的多态性, 动态多态性是通过虚函数实现的。

重载函数与同名覆盖的区别

重载函数在函数类型和参数个数两方面至少有一个不同

同名覆盖则是在参数个数和函数类型两方面全部相同且一个处于基类一个处于派生类

利用虚函数实现动态多态性 

虚函数的作用

在同一个类当中是不能够定义两个名字相同、 参数个数和类型都相同的函数的,否则就是重复定义。但是在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数,但是要通过虚函数来实现。

c++当中的虚函数就是用来解决动态多态问题的。所谓虚函数,就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义函数。在程序运行期间,用指针指向某一派生类中的对象,这样就能调用指针指向的派生类对象中的函数,而不会调用其他派生类中的函数。

注意:虚函数的作用是在派生类中重新定义与基类同名的函数,并且可以通过基类指针来访问基类和派生类中的同名函数。

#include <iostream>
using namespace std;
class Student
{
	protected: 
		string name;
		int age;
		char sex;
	public:
		Student(string a,int b,char c):name(a),age(b),sex(c){}
		virtual void display()//声明为虚函数 
		{
			cout<<"name:"<<name<<endl;
			cout<<"age:"<<age<<endl;
			cout<<"sex:"<<sex<<endl;
		}
};
class Graduate:public Student
{
	private:
		double wage;
	public:
		Graduate(string a,int b,char c,double d):Student(a,b,c),wage(d){}
		void display()
		{
			cout<<"name:"<<name<<endl;
			cout<<"age:"<<age<<endl;
			cout<<"sex:"<<sex<<endl;
			cout<<"wage:"<<wage<<endl;
		}
};
int main()
{
	Student A("小芳",18,'f');
	Graduate B("土狗",18,'m',888);
	Student *p;
	p=&A;
	p->display();
	p=&B;
	p->display();
	return 0;
}

例如对于上一章中继承的一个例子进行小小的修改(修改部分为注释那行)

我们可以发现指针p指向的display函数能够成功的输出Graudate类对象的数据而不是与之前那样仅仅输出姓名、年龄以及性别

原因在于多态性的实现:在原有的未加virtual的程序当中,本来我们定义的基类指针p是指向基类对象的,如果用它来指向派生类对象,则会自动进行指针类型转换,将基类指针转换为派生类指针,这样基类指针指向的就是派生类对象中的基类部分,也仅仅能对于派生类中从基类继承来的数据进行输出。在程序修改之前是无法通过基类指针去调用派生类对象中的成员函数的

但是虚函数打破了这一限制,在基类中的display被声明为虚函数后,对于派生类中的display函数进行重定义,这时派生类的同名函数就取代了基类中的虚函数。因此在基类指针指向派生类对象后,调用display函数就是调用的派生类的display函数。要注意的是,只有用virtual声明了函数为虚函数后才具有以上作用

虚函数的以上功能具有十分强大的实际意义,在面向对象的程序设计当中,经常会用到基类的继承,目的是保留基类的特性,以减少基类的开发时间。但是,从基类继承来的某些成员函数不完全适应派生类的需要,且如果派生的层次过多还要起不同的名字,若名字相同又会出现同名覆盖的现象

虚函数很好的解决了这个问题。可以看到:当把基类当中某个成员函数声明为虚函数后,允许起派生类中对该函数重新定义赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同的类的对象,从而调用其中的同名函数。

注意:由虚函数实现的动态多态性就是:同一类族中不同类的对象对同一函数调用作出不同的相应

虚函数的使用方法是

1.在基类当中用virtual来声明成员函数为虚函数。在类外定义虚函数时,不必再加virtual

2.在派生类中重新定义此函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体

当一个函数被声明为虚函数时,其派生类当中的同名函数都自动改为虚函数。因此在派生类重新声明该函数时,可以加virtual,也可以不加,但习惯上一般在每一层声明该函数时都加virtual,使程序更加清晰

3.定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象

4.通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数

通过虚函数与指向基类对象的指针变量的配合使用,就能实现动态的多态性。如果想调用同一类族中不同类的同名函数,只要先用基类指针指向该类对象即可。如果指针先后指向同一类族当中不同类的对象,就能不断地调用这些对象中的同名函数

需要说明,有时在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数,如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,这并不是多态性行为,没有用到虚函数的功能

我们可以发现,函数重载处理的是同一层次上同名函数的问题,虚函数处理的是不同派生层次上的同名函数问题,前者是横向重载,后者是纵向重载,但虚函数与函数重载不同的是:同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或参数类型不同)

静态关联与动态关联 

在编译系统当中,编译系统要根据已有的信息对于同名函数的调用作出判断。比如函数重载就是根据函数的参数或参数的类型不同来判断究竟调用哪儿个函数。对于调用同一类族当中的虚函数,我们应带在调用的时候用一定的方式告诉编译系统要调用的是哪儿一个类当中的函数

确定调用的具体对象的过程称为关联,一般来说关联指把一个标识符和一个存储地址联系起来

前面提到的函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于哪儿一个类,其过程称为静态关联,由于是在运行前进行关联的,因此又称为早期关联,函数的重载属于静态关联

从上面那块代码程序我们可以看出,通过指针p指向的display函数属于对虚函数的调用,且我们在调用的时候并没有为其指定对象名,且在编译过程中通过指针p对于display函数的调用是合乎语法符合编译的,但是我们发现在编译阶段无法确定调用的是哪儿个display函数。在这种情况下,编译系统把它放在运行阶段处理,在运行阶段,基类指针变量先指向某一个类对象,然后通过此指针变量调用该对象中的函数。此时调用哪儿一个对象无疑是确定的。由于是在运行阶段把虚函数和类对象“绑定”在一起的,因此,此过程称为动态关联。这种多态性是动态的多态性,即运行阶段的多态性

由于在运行阶段,指针可以先后指向不同的类对象,从而在调用同一类族中的不同类的虚函数。由于动态关联是在编译以后的运行阶段进行的,因此也称为滞后关联

在什么情况下应该声明虚函数

使用虚函数时要注意两点:

1.只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数,虚函数仅仅能够用于类的继承层次结构当中,因为虚函数的作用是允许在派生类中对基类虚函数重新定义的

2.一个成员函数被声明为虚函数后,在同一类族当中的类就不能在定义一个非虚与该虚函数具有相同参数(个数和类型)和函数返回值相同的同名函数

使用虚函数的情况

1.首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望更改功能,一般将它声明为虚函数

 2.如果成员函数在类被继承后功能无需修改,或派生类用不到该函数,则不要把它声明为虚函数。不要仅仅考虑到其作为基类而把基类所有成员函数都声明为虚函数

3.应考虑对成员函数的调用是通过基类指针还是引用去访问,若是通过基类指针或引用去访问则应当声明为虚函数

4.有时在定义虚函数时,其为空,后面的功能仅仅留给派生类中去重新定义实现添加

注意:

使用虚函数系统要有一定的空间开销。当一个类带有虚函数时,编译一同会为该类构造一个虚函数表,他是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联的时间开销是很少的,因此多态性是十分高效的

虚析构函数

虚析构函数的使用也是多态性的一种体现,前面我们了解到派生类可以对于基类的构造函数进行继承,因此在派生类对象的建立过程中首先调用基类的构造函数,而后才调用派生类的构造函数,在此对象被清理的时候先调用派生类的析构函数再调用基类的析构函数

然而若是我们new一个派生类的对象,在对其进行delete时我们会发现其仅仅执行了基类的析构函数,对于动态建立的派生类的析构函数并无丝毫的调用,这就容易造成内存泄漏

解决这个情况的方式就是对于基类的析构函数进行虚函数处理,与普通的虚函数不同,在基类进行析构函数的虚函数处理之后,其派生类中的虚构函数无论名字相不相同则全部转化为虚函数

专业人员一般都习惯声明虚析构函数,即使基类并不需要定义虚析构函数,也显式的定义一个函数体为空的虚析构函数,以保证再撤销动态分配空间时能够得到正确的处理。

构造函数不能声明为虚函数。这是因为在执行构造函数当中类对象还未完成建立的过程,当然谈不上把函数与类对象绑定

#include <iostream>
using namespace std;
class Student
{
	protected: 
		string name;
		int age;
		char sex;
	public:
		Student(){}
		virtual ~Student(){cout<<"完成基类的清理"<<endl;} 
};
class Graduate:public Student
{
	private:
		double wage;
	public:
		Graduate(){}
		~Graduate(){cout<<"完成派生类的清理"<<endl;} 
};
int main()
{
	Student *p=new Graduate;
	delete p;
	return 0;
}

运行结果

纯虚函数与抽象类

纯虚函数 

有的情况下,虚函数为空且仅仅为了派生类当中能够对其功能重新定义,并无实际意义,我们把这样的函数叫做纯虚函数,有个简便的写法

virtual 函数类型 函数名(参数列表)=0;

注意: 

1.纯虚函数没有函数体

2.最后的=0并不代表函数返回值为0他只是起一个形式上的作用告诉编译系统这时纯虚函数

3.这是一个声明函数,末尾加分号

纯虚函数仅仅是一个函数名字不具备函数的功能,不能被调用,只是个空壳而已。它只是通知编译系统:“在这定义了一个虚函数留给后面派生类中定义”。纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在基类中没有保留函数名字,则无法实现多态性。如果在一个类中声明了纯虚函数而在派生类当中没有对该函数进行定义,则该虚函数在派生类中仍然为纯虚函数

抽象类

有了空壳函数作为基础来供派生类中的函数进行重定义,自然也有类似于空壳类的存在。也就是抽象类。如果声明了一个类,一般可以用它定义对象。但是在面向对象的程序设计过程中往往有一些类它们不用来生成对象。定义这些类的唯一目的是用它作为基类去建立派生类。它们作为一种基本类型提供给用户,用户在这个基础上根据自己的需要定义出功能各异的派生类,用这些派生类去建立对象

这种不用来定义对象而只作为一种基本类型用作继承的类,称为抽象类,由于它常用作基类,通常称为抽象基类。饭时包含纯虚函数的类都是抽象类。因为纯虚函数是无法调用的,包含纯虚函数的类是无法建立对象的。

抽象类的作用是作为一个类族的共同基类,或者说,为一个类族提供一个公共接口

如果在抽象类所派生出来的新类当中对于基类的所有纯虚函数进行了定义,那么这些函数就被赋予了新的功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类,如果在派生类当中没有对于所有纯虚函数进行定义,此派生类仍然是抽象类,不能用来建立对象

虽然抽象类不可以用来建立对象,但是可以定义指向抽象类数据的指针变量,当派生类成为具体类之后。就可以用这种指针指向派生类对象,然后通过该指针调用虚函数实现多态性的操作

本章结论

1.一个基类如果包含一个或一个以上的纯虚函数时就是抽象基类,抽象基类是不能也不必要定义对象

2.抽象基类于普通基类不同,它一般不是现实存在的对象的抽象,它可以是没有任何物理上的或者其他实际意义方面的含义

3.在类的层次结构当中,顶层或者最上面的几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中公有成员函数集中在抽象基类中声明。

4.抽象基类是本类族的公共接口,或者说从同一基类派生出的多个类由同一接口,因此能响应同一形式的消息,但是相应的方式因对象不同而异。在通过虚函数实现动态多态性时,可以不必考虑对象是哪儿一个类的,都用同一种方式调用

5.如果能够通过对象名在编译阶段确定调用的是哪儿个类的虚函数这是静态关联,如果是通过基类的指针p调用虚函数,在编译阶段无法确定调用哪儿一个类的虚函数,只有在运行时p指针指向某一类对象时才确定,那么此为动态关联

6.如果在基类中声明了虚函数,那么在派生类中凡是与该函数有相同的函数名、函数类型、参数个数和参数类型的函数均为虚函数。同一虚函数在不同的类当中可以有着不同的定义。纯虚函数是在抽象基类中声明的,只是在抽象基类中才称为纯虚函数,在其派生类中虽然继承了该函数,但除非再次用=0把它声明为纯虚函数,否则不能称为纯虚函数

7.使用虚函数提高了程序的可扩充性

多态性即为开启继承功能的钥匙

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

计科土狗

谢谢家人们

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

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

打赏作者

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

抵扣说明:

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

余额充值