导图社区 cpp专题知识框架----多态性与虚函数
c 知识点框架整理,此部分为多态性与虚函数,多态性指具有不同功能的函数可以用同一个函数名;指向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为;
编辑于2023-06-03 00:43:46多态性与虚函数
多态性
抽象定义
不同对象收到相同的消息时,产生不同的动作
具体定义
指具有不同功能的函数可以用同一个函数名;
指向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为;
分类
实现角度
静态多态性
函数重载
运算符重载
动态多态性
继承
虚函数
应用角度
通用多态
参数多态
函数模板
类模板
类属函数和类属类相关联
包含多态
虚函数
定义于不同类中的同名成员函数的多态行为
专用多态
重载多态
函数重载
运算符重载
同类中的同名函数
强制多态
强制转化
将一个变元的类型加以变化,以符合一个函数或操作的要求
联编
多态实现的角度分为的静态多态(编译时多态)与动态多态(运行时多态) 本质上是联编所引起的。
定义
把函数名与函数体的程序代码连接(联系)在一起的过程。
分类
静态联编
在编译阶段完成的联编
静态联编时,系统用实参和形参进行匹配,对于同名的重载函数便根据参数上的差异进行区分,然后进行联编。从而实现了多态性。
函数重载和运算符重载
C++从C继承过来的
动态联编
运行阶段完成的联编
当程序调用到某一函数名时,才去寻找和连接其程序代码,对面向对象的程序设计而言,就是当对象接收到某一消息时,才去寻找和连接相应的方法。
虚函数
C++完善多态性新增的
例子
运算符重载
#include <iostream> //声明类Point class Point { public: Point(float x=0,float y=0);//有默认参数的构造函数 void setPoint(float,float); //设置坐标值 float getX( ) const {return x;} //读x坐标 float getY( ) const {return y;} //读y坐标 friend ostream & operator<<(ostream &,const Point &);//重载运算符“<<” protected: //受保护成员 float x,y; }; //下面定义Point类的成员函数 //Point的构造函数 Point::Point(float a,float b) //对x,y初始化 {x=a;y=b;} //设置x和y的坐标值 void Point::setPoint(float a,float b) //为x,y赋新值 {x=a;y=b;} //重载运算符“<<”,使之能输出点的坐标 ostream & operator<<(ostream &output,const Point &p) {output<<″[″<<p.x<<″,″<<p.y<<″]″<<endl; return output; } int main( ) {Point p(3.5,6.4);//建立Point类对象p cout<<″x=″<<p.getX( )<<″,y=″<<p.getY( )<<endl;//输出p的坐标值 p.setPoint(8.5,6.8); //重新设置p的坐标值 cout<<″p(new):″<<p<<endl; //用重载运算符“<<”输出p点坐标 } 运行结果为 x=3.5,y=6.4 p(new):[8.5,6.8]
不同类的同名函数
class Circle:public Point//circle是Point类的公用派生类 {public: Circle(float x=0,float y=0,float r=0); //构造函数 void setRadius(float); //设置半径值 float getRadius( ) const; //读取半径值 float area ( ) const; //计算圆面积 friend ostream &operator<<(ostream &,const Circle &);//重载运算符“<<” private: float radius; }; //定义构造函数,对圆心坐标和半径初始化 Circle::Circle(float a,float b,float r):Point(a,b),radius(r){ } //设置半径值 void Circle::setRadius(float r) {radius=r;} //读取半径值 float Circle::getRadius( ) const {return radius;} //计算圆面积 float Circle::area( ) const {return 3.14159*radius*radius;} //重载运算符“<<”,使之按规定的形式输出圆的信息 ostream &operator<<(ostream &output,const Circle &c) {output<<″Center=[″<<c.x<<″,″<<c.y<<″],r=″<<c.radius<<″,area=″<<c.area( )<<endl; return output; } int main( ) {Circle c(3.5,6.4,5.2);//建立Circle类对象c,并给定圆心坐标和半径 cout<<″original circle:\\nx=″<<c.getX()<<″, y=″<<c.getY()<<″, r=″<<c.getRadius( ) <<″, area=″<<c.area( )<<endl; //输出圆心坐标、半径和面积 c.setRadius(7.5); //设置半径值 c.setPoint(5,5); //设置圆心坐标值x,y cout<<″new circle:\\n″<<c; //用重载运算符“<<”输出圆对象的信息 Point &pRef=c; //pRef是Point类的引用变量,被c初始化 cout<<″pRef:″<<pRef; //输出pRef的信息 return 0; } 程序编译通过,运行结果为original circle:(输出原来的圆的数据) x=3.5, y=6.4, r=5.2, area=84.9486 new circle: (输出修改后的圆的数据) Center=[5,5], r=7.5, area=176.714 pRef:[5,5] (输出圆的圆心“点”的数据)
静态多态
虚函数
定义
虚函数允许函数调用与函数体之间的联系在运行时才建立,也就是在运行时才决定如何动作,即所谓的动态联编。 虚函数是成员函数,而且是非static的成员函数。
声明格式
virtual<类型说明符><函数名>(<参数表>)
基类定义了一个virtual函数,派生类继承了基类后重载了该函数,但未加上virtual关键字,此派生类再被一个派生类继承后,系统遵循如下规则来判定
该函数与基类的虚函数有相同的名称。
该函数与基类的虚函数有相同的参数个数及相同的对应参数类型。
该函数与基类的虚函数有相同的返回类型或者满足赋值兼容规则的指针、引用型的返回类型。
满足三者 则自动判定 虚函数
实现格式
virtual 函数类型 函数名(形参表) { // 函数体 }
派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与其基类中的原型完全相同。
说明
如果某类中的一个成员函数被说明为虚函数,这就意味着该成员函数在派生类中可能有不同的实现。当使用这个成员函数操作指针或引用所标识对象时,对该成员函数调用采取动态联编方式,即在运行时进行关联或束定。
动态联编只能通过指针或引用标识对象来操作虚函数。如果采用一般类型的标识对象来操作虚函数,则将采用静态联编方式调用虚函数。
派生类中对基类的虚函数 替换的前提
与基类的虚函数有相同的参数个数;
其参数的类型与基类的虚函数的对应参数类型相同;
其返回值或者与基类虚函数的相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中被替换的虚函数所返回的指针或引用的基类型的子类型。
在构造函数中调用虚函数
采用静态联编
造函数调用的虚函数是自己类中实现的虚函数
如果自己类中没有实现这个虚函数,则调用基类中的虚函数
不会调用派生类的虚函数
例子
#include<iostream.h> class A{ public: void show(){ cout<<"A"; } }; class B:public A { public: void show(){ cout<<"B"; } }; main() { A a,*pc; B b; pc=&a; pc->show(); pc=&b; pc->show(); return 0; } 结果 AA
#include<iostream.h> class Base{ public: Base(int x,int y) { a=x; b=y; } void show() { cout<<"Base----------\n"; cout<<a<<" "<<b<<endl; } private: int a,b; }; class Derived :public Base { public: Derived(int x,int y,int z):Base(x,y) { c=z; } void show() { cout<< "Derived---------\n"; cout<<"c="<<c<<endl; } private: int c; }; void main() { Base mb(60,60),*pc; Derived mc(10,20,30); pc=&mb; pc->show(); pc=&mc; pc->show(); } 结果 Base---------------- 60 60 Base---------------- 10 20
静态联编 导致的 基类指针 只调用基类 成员函数
#include<iostream.h> class Base { public: Base(int x,int y) { a=x; b=y; } virtual void show() //定义虚函数show() { cout<<"Base----------\n"; cout<<a<<" "<<b<<endl;} private: int a,b; }; class Derived : public Base { public: Derived(int x,int y,int z):Base(x,y){c=z; } void show() //重新定义虚函数show() { cout<< "Derived---------\n"; cout<<c<<endl;} private: int c; }; void main() { Base mb(60,60),*pc; Derived mc(10,20,30); pc=&mb; pc->show(); //调用基类Base的show()版本 pc=&mc; pc->show(); //调用派生类Derived的show()版本 } 程序运行结果如下: Base--------- 60 60 Derived-------- 30
动态联编 根据指针或引用类型 决定调用基类还是派生类 的成员函数
注意事项
通过定义虚函数来使用C++提供的多态机制时,派生类应该从它的基类公有派生。赋值兼容规则成立的前提条件是派生类从其基类公有派生。
必须首先在基类中定义虚函数。在实际应用中,应该在类等级内需要具有动态多态性的几个层次中的最高层类内首先声明虚函数。
在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。
使用对象名和点运算符的方式也可以调用虚函数,但是这种调用在编译时进行的是静态联编,它没有充分利用虚函数的特性。只有通过基类指针访问虚函数时才能获得运行时的多态性。
一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。但是虚函数可以在另一个类中被声明为友元函数。
内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看作是非内联的。
构造函数不能是虚函数。因为虚函数作为运行过程中多态的基础,主要是针对对象的,而构造函数是在对象产生之前运行的,因此虚构造函数是没有意义的。
析构函数可以是虚函数,而且通常说明为虚函数。
虚析构
解决的问题
在程序用带指针参数的delete运算符撤销对象时,会发生一个情况: 系统会只执行基类的析构函数,而不执行派生类的析构函数。
解决方案
析构函数设置为虚函数后,在使用指针引用时可以动态联编,实现运行时的多态,保证使用基类类型的指针能够调用适当的析构函数针对不同的对象进行清理工作。
说明
如果一个类的析构函数是虚函数,那么,由它派生而来的所有派生类的析构函数也是虚析构函数,不管它是否使用了关键字virtual进行说明。
定义格式
virtual~类名()
例子
#include<iostream.h> class Grandam{ public: Grandam() { } virtual ~Grandam() { cout<<"This is Grandam∷~Grandam()."<<endl; } }; class Mother:public Grandam{ public: Mother() { } ~Mother() { cout<<"This is Mother∷~Mother()."<<endl; } }; void main() { Grandam *f; f=new Mother; delete f; } 运行结果为: This is Mother∷~Mother(). This is Grandam∷~Grandam().
虚函数与重载函数的区别
普通的函数重载时,其函数的参数个数或参数类型必须有所不同,函数的返回类型也可以不同。
当重载一个虚函数时,也就是说在派生类中重新定义虚函数时,要求函数名、返回类型、参数个数、参数的类型和顺序与基类中的虚函数原型完全相同。
如果仅仅返回类型不同,其余均相同,系统会给出错误信息;
若仅仅函数名相同,而参数的个数、类型或顺序不同,系统将它作为普通的函数重载,这时将丢失虚函数的特性。
虚函数的限制
只有成员函数才能声明为虚函数。因为虚函数仅适用于有继 承关系的类对象,所以普通函数不能声明为虚函数。
虚函数必须是非静态成员函数。这是因为静态成员函数不受 限于某个对象。
内联函数不能声明为虚函数。因为内联函数不能在运行中动态确定其位置。
构造函数不能声明为虚函数。多态是指不同的对象对同一消息有不同的行为特性。虚函数作为运行过程中
多态的基础,主要是针对对象的,而构造函数是在对 象产生之前运行的,因此,虚构造函数是没有意义的。
析构函数可以声明为虚函数。析构函数的功能是在该类对象消亡之前进行一些必要的清理工作,析构函数没有类型,也没有参数,和普通成员函数相比,虚析构函数情况略为简单些。
多继承与虚函数
例子
#include<iostream.h> class Base1{ public: virtual void fun() //定义fun()是虚函数 { cout<<"--Base1--\n"; } }; class Base2{ public: void fun() //定义fun()为普通的成员函数 { cout<<"--Base2--\n"; } }; class Derived:public Base1,public Base2{ public: void fun() { cout<<"--Derived--\n"; } }; void main() { Base1 obj1,*ptr1; Base2 obj2,*ptr2; Derived obj3; ptr1=&obj1; ptr1->fun(); ptr2=&obj2; ptr2->fun(); ptr1=&obj3; ptr1->fun(); ptr2=&obj3; ptr2->fun(); } 结果 --Base1-- --Base2-- --Derived-- --Base2—
纯虚函数
定义
纯虚函数是一个在基类中说明的虚函数,它在该基类中没有定义,但要求在它的派生类中必须定义自己的版本,或重新说明为纯虚函数。
声明与实现
virtual <函数类型> <函数名> ( 参数表 ) = 0;
说明
纯虚函数没有函数体;
最后面的“=0”并不表示函数返回值为0; 这是一个声明语句,最后应有分号。
纯虚函数只有函数的名字而不具备函数的功能,不能被调用。在派生类中对此函数提供定义后,它才能具备函数的功能,可被调用。
如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数定义,则该虚函数在派生类中仍然为纯虚函数。
例子
#include<iostream.h> class Circle { public: void setr(int x){ r=x; } virtual void show()=0; // 纯虚函数 protected: int r; }; class Area:public Circle{ public: void show(){ cout<<"Area is "<<3.14*r*r<<endl;} }; // 重定义虚函数show( ) class Perimeter:public Circle{ public: void show(){cout<<"Perimeter is "<<2*3.14*r<<endl;} }; // 重定义虚函数show( ) void main() { Circle *ptr; Area ob1; Perimeter ob2; ob1.setr(10); ob2.setr(10); ptr=&ob1; ptr->show(); ptr=&ob2; ptr->show(); } 结果 Area is 314 Perimeter is 62.8
#include <iostream> using namespace std; // Base class class Shape { public: // pure virtual function providing interface framework. virtual int getArea() = 0; void setWidth(int w) { width = w; } void setHeight(int h) { height = h; } protected: int width; int height; }; // Derived classes class Rectangle: public Shape { public: int getArea() { return (width * height); } }; class Triangle: public Shape { public: int getArea() { return (width * height)/2; } }; int main(void) { Rectangle Rect; Triangle Tri; Rect.setWidth(5); Rect.setHeight(7); // Print the area of the object. cout << "Total Rectangle area: " << Rect.getArea() << endl; Tri.setWidth(5); Tri.setHeight(7); // Print the area of the object. cout << "Total Triangle area: " << Tri.getArea() << endl; return 0; } 结果 Total Rectangle area: 35 Total Triangle area: 17
抽象类
定义
一个具有纯虚函数的类称为抽象类。一个类至少有一个纯虚函数,那么就称该类为抽象类。
特点
抽象类不能建立抽象类对象
其纯虚函数的实现由派生类给出。
派生类中必须重载基类中的纯虚函数,否则它仍将被看作一个抽象类。
注意事项
由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类的对象,纯虚函数的实现由派生类给出。
不允许从具体类派生出抽象类。
抽象类不能用作参数类型、函数返回类型或显式转换的类型。 但是抽象类的指针和引用,可以。
可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。
抽象类的析构函数可以被声明为纯虚函数,这时,应该至少提供该析构函数的一个实现。
如果派生类中没有重定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不是抽象类了,它是一个可以建立对象的具体类。
在抽象类中也可以定义普通成员函数或虚函数,虽然不能为抽象类声明对象,但仍然可以通过派生类对象来调用这些不是纯虚函数的函数。
可以通过抽象类的指针和引用调用抽象类中的非虚成员函数,但这就不体现多态性了。
说明
一个基类如果包含一个或一个以上纯虚函数,就是抽象基类。抽象基类不能也不必要定义对象。
抽象基类与普通基类不同,它一般并不是现实存在的对象的抽象(例如圆形(Circle)就是千千万万个实际的圆的抽象),它可以没有任何物理上的或其他实际意义方面的含义。
在类的层次结构中,顶层或最上面的几层可以是抽象基类。抽象基类体现了本类族中各类的共性,把各类中共有的成员函数集中在抽象基类中声明。
抽象基类是本类族的公共接口。或者说,从同一基类派生出的多个类有同一接口。
如果在基类声明了虚函数,则在派生类中凡是与该函数有相同的函数名、函数类型、参数个数和类型的函数,均为虚函数(不论在派生类中是否用virtual声明)。
使用虚函数提高了程序的可扩充性。 把类的声明与类的使用分离。这对于设计类库的软件开发商来说尤为重要。开发商设计了各种各样的类,但不向用户提供源代码,用户可以不知道类是怎样声明的,但是可以使用这些类来派生出自己的类。