面向对象技术三大特性:继承、类的封装、多态。
- 7.1多态的描述
7.1.1多态的含义
多态是指具有继承关系的类,拥有相同的接口(函数名、形参和返回值),并允许有各自不同的实现,且一个对象实例只有在调用共同接口时,才能确定调用是何种实现,即“一个接口,多种实现”。
多态是指不同对象接受相同消息后产生的不同动作。函数重载是一种多态,相同的函数名,对应多个不同函数体。在调用函数时,根据参数的不同执行了不同的函数体,产生了不同的动作。同理,运算符重载也是多态的典型应用,相同的运算符,可以用作不同类型数据进行运算,实质时调用了不同运算符重载函数
7.1.2多态的分类
面对象的多态可分为4类:重载多态、强制多态、包含多态和类型参数化多态。前两者统称为特殊性多态,用来刻化语义上无关联的类型间的关系。后两者称为一般性多态,用来系统地刻画语义上相关的一种类型。
函数重载和运算符重载都属于重载多态。强制多态是指强制类型转换。在派生类中声明与基类同名的成员函数(参数必须相同,否则会被视为函数重载),并在基类中把该函数定义为虚函数,实现“一个接口,多种方式”,这就是包含多态。多态函数(或类)必须带有类型,在使用时赋予实际的类型加以实例化,该类型参数确定函数(或类)在每次执行时操作数的类型,由于类模板实例化的各个类都具有相同的操作,而操作对象的类型却各不相同,此种被称为类型参数化多态。
7.1.3多态实现的方式
从实现角度分为编译时多态和运行时多态。前者是在程序运行过程中才能动态地确定操作的具体对象,后者是在编译的过程中确定了同名操作的具体操作对象。这种确定操作对象的过程称为联编或绑定。
静态联编对应于编译时多态,动态联编对应于运行时多态。静态联编是指在程序执行前将函数实现和函数调用关联起来,也叫早绑定或前绑定。动态联编是指在函数运行时才将函数实现和函数调用相关联,也叫运行时绑、晚绑定或后绑定。
动态联编灵活但速度相对较慢,而静态联编与之相反。 - 7.2运算符重载
运算符重载提供了一种机制,可以重新定义运算符,使之可以应用于用户自定义类型。
7.2.1 运算符重载的定义
运算符重载是通过定义运算符重载函数来实现的。运算符重载函数的定义和一个普通函数定义类似。运算符重载函数采用两种形式:成员函数和友元函数 。
1.重载为友元函数,函数定义格式如下:
<类型> operator <运算符>(<参数列表>)
{
<函数体>
}
然后在相应类中将该函数声明为友元,声明形式为:
friend <类型> operator <运算符>(<参数列表>);
2.重载为成员函数,函数定义格式如下:
<类型> <类名>:: operator <运算符>(<参数列表>)
{
<参数体>
}
在以上两种重载格式中,<参数列表>中参数的个数不仅取决于运算符的操作数个数,还取决于重载的形式,即重载为成员函数还是友元函数。
当重载为成员函数,参数个数比运算符的操作个数少一个,因为调用该成员函数本身也作为一个操作数参与运算,因此双目运算符的参数个数是一个,单目运算符没有参数。当重载为友元函数,参数个数与运算符的操作数个数相同。后缀++和后缀–为了与前缀分开,会增加一个int型参数。
注:
(1) 只能重载C++预定义的运算符,不能创造新的运算符。但不能重载 “ . ” 、“.* ”
、“ ::” 、“?:” 和“sizeof” 。
(2)重载后的运算符不能改变原有的优先级、结合性以及操作数的个数。
(3)运算符重载函数的参数中至少有一个是自定义的类型。只用于简单类型的运算符不能重载;也不能为简单类型定义新的运算符。
(4)大多数运算符既可以重载为友元函数也可以重载为成员函数,一般将双目运算符重载为友元函数,而将单目运算符重载为成员函数。
(5)对运算符重载时,尽量不要改变其内置的原有定义,如果改变了原来的含义,不如另起一个函数名字。
7.2.2 双目运算符重载
双目运算符即可以重载为类的成员函数,也可以重载为类的友元函数。两者的区别是,对于同一个运算符,用成员函数比友元函数实现少一个参数。
1.双目运算符重载为成员函数
例 7.1 “==”和“!=” 重载为成员函数。
#include <iostream> using namespace std; class Point { public: Point () {X=0; Y=0;} Point (double x, double y){X=x;Y=y; } bool operator==(const Point &); bool operator!=(const Point &); private: double X,Y; }; bool Point ::operator ==(const Point &p) //定义双目关系运算符" =="重载运算符 { return((X==p.X)&&(Y==p.Y)); } bool Point :: opertaor !=(const Point &p ) //定义双目关系运算符" != "重载函数 { return !((*this)==p); //调用运算符"=="重载函数 } int main () { Point p1,p2(2,3.4); if(p1==p2) cout<<"点p1和点p2相同"<<endl; if(p1!=p2) cout <<"点p1与点p2不相同"<<endl; } 说明:(1)需要判断两个类对象是否相等,重载关系运算符"==",比另外定义一个函数更符合人们的习惯。且一个运算符可以调用另一个已重载的运算符。 (2)程序中声明一个点类Point,并对运算符"=="进行了重载定义,用来判断两个对象是否相等。如果两个的横纵标相等,表示两个点相同,对运算符"!="进行重载函数时调用了运算符"=="的重载函数。
讯享网
例 7.2算术运算符" + “、”-" 重载为成员函数。
讯享网#include<iostream> using namespace std; class Point { public: Point (){ X=0;Y=0;} Point (double x,double y){ X=x;Y=y;} Point operator + (const Point & ); //算术运算符"+"重载为成员函数 Point operator - (const Point &); //算术运算符"-"重载为成员函数 void Dispoint() { cout <<"点的位置是:("<<X<<","<<Y<<")"<<endl;} private: double X,Y; }; Point Point ::operator + (const Point &p) //定义算术运算符 " + " 重载函数 { Point t (X+p.X,Y+p.Y); return t; } Point Point ::operator - (const Point &p) //定义运算符"-"重载函数 { Point t (X-p.X,Y-p.Y); return t; } int main() { Point p1,p2(2,3.4),p3(1.2,6); p1. Dispoint; p1=p2+p3; p1. Dispoint; p1=p2-p3; p1. Dispoint; return 0; } 说明:(1)两个点类进行+-运算时,不能直接使用+ -,因此要重载+ -。 (2)由于计算时不能改变实参的值,所以形参类型采用常引用。 (3)双目运算符重载为成员函数时,只有一个参数,另一个参数是this指针指向的的对象本身,this指针可以省略。
#include<iostream> using namespace std; class Point { public: Point (){ X=0;Y=0;} Point (double x,double y){ X=x;Y=y;} friend Point operator + (const Point &,const Point & ); //算术运算符"+"重载为成员函数 friend Point operator - (const Point &,const Point &); //算术运算符"-"重载为成员函数 friend bool operator==(const Point &,const Point &); friend bool operator!=(const Point &,const Point &); void Dispoint() //输出点坐标 { cout <<"点的位置是:("<<X<<","<<Y<<")"<<endl;} private: double X,Y; }; Point operator + (const Point &p1,const Point &p2) //定义运算符“+”重载函数 { Point t(p1.X+p2.X,p1.Y+p1.Y); return t; } Point operator - (const Point &p1,const Point &p2) //定义运算符“-”重载函数 { Point t(p1.X-p2.X,p1.Y-p1.Y); return t; } bool operator==(const Point &p1,const Point &p2) //定义关系运算符“==”重载函数 { return ((p1.X==p2.X)&&(p1.Y==p2.Y)); } bool operator!=(const Point &p1,const Point &p2) //定义关系运算符“!=”重载函数 { return !(p1==p2); } main() { Point p1,p2(2,3.4),p3(1.2,6); if(p2==p3) cout<<"p2=p3"<<endl; if(p2!=p3) cout<<"p2!=p3"<<endl; cout<<"p1的初始位置"; p1.Dispoint() ; p1=p2+p3; cout<<"p1的第一次移动位置"; p1.Dispoint() ; p1=p2-p3; cout<<"p1的第二次移动位置"; p1.Dispoint() ; return 0; } 说明:(1)双目运算符重载为友元函数,运算符两个操作数的参数都要出现在参数列表中。 (2)双目运算符重载为友元函数,可以进行该类对象和其他类型数据的混合运算。 例:增加友元函数 Data operator + ( const int &); Data operator +(const double &); 5+p1; 3.4+p2 加法第一个操作数是整型或浮点型,是内置简单类型,简单类型的运算符不能被重新定义,必须用友元函数来实现重载。
例7.4赋值运算符重载
讯享网#include<iostream> using namespace std; class Point { public: Point (){ X=0;Y=0;} Point (double x,double y){ X=x;Y=y;} Point& operator = (const Point &); //赋值运算符重载函数 Point& operator = (const int &); //赋值运算符重载函数 Point& operator = (const double &); //赋值运算符重载函数 void Dispoint() //输出点坐标 { cout <<"点的位置是:("<<X<<","<<Y<<")"<<endl;} private: double X,Y; }; Point& Point::operator = (const Point &d); //右值是Point类型的的赋值运算定义 { if(this==&d) return *this ;//如果本对象给自己赋值,直接返回本对象 X=d.X; Y=d.Y; return *this; } Point& Point::operator = (const int &i); //右值是整型数据 { X=i; Y=0; return *this; } Point& Point::operator = (const double &f); //右值是浮点型数据 { X=f; Y=0; return *this; } int main() { Point p1,p2(2,3.4); p1=p2; cout<<"p1的位置"; p1.Dispoint(); p1=5; cout<<"p1的位置"; p1.Dispoint(); p1=3.6; cout<<"p1的位置"; p1.Dispoint(); return 0; } 说明:(1)Point类定义了多个赋值运算重载符,满足了在同类对象之间,也可以整型数据和浮点数据对Point类对象进行赋值。根据赋值运算符右操作数类型执行不同的函数。 (2)无论参数为何种类型,赋值运算符都必须重载为成员,并且因为返回的是左值,所以返回值得类型必须是该类的引用。
4.复合赋值运算符重载
复合赋值运算符的编译效率高,编程应尽量使用复合赋值运算。
例7.5 复合赋值运算符"+="重载
#include<iostream> using namespace std; class Point { public: Point (){ X=0;Y=0;} Point (double x,double y){ X=x;Y=y;} Point& operator += (const Point &); //复合赋值运算符重载函数 void Dispoint() //输出点坐标 { cout <<"点的位置是:("<<X<<","<<Y<<")"<<endl;} private: double X,Y; } Point & Point :: operate +=(const Point &d) { X+=d.X; Y+=d.X; return (*this); } int main() { Point p1,p2(2,3.4),p3(3,5.6); cout<<"运算前p1;" p1.Dispoint(); p1+=p2; cout<<"运算后p1;" p1.Dispoint(); return 0; } 说明:复合赋值运算符也应返回左值,所以返回值是一个引用。
7.2.3 单目运算符重载
内置的自增与自减各有前缀和后缀形式。首先要解决前缀和后缀,故在重载时的后缀操作数额外增加一个int型形参,编译时形参被赋值为0,该参数只负责区分前缀和后缀。
例7.6自增自减运算符重载
讯享网#include<iostream> using namespace std; class Increase { public : Increase( int a=0; int b=0); Increase& operator ++ (); //前缀++ Increase operator ++ (int); //后缀++ Increase& operator -- (); //前缀-- Increase operator -- (int); //后缀-- private: X,Y; }; Increase :: Increase(int a, int b) { X=a; Y=b; } Increase& Increase::operator ++ () //定义前缀“++”运算 { X++; Y++; return *this; } Increase& Increase::operator ++ (int) //定义后缀“++”运算 { increase t(X,Y); X++; Y++; return t; } Increase& Increase::operator -- () //定义前缀“--”运算 { X--; Y--; return *this; } Increase& Increase::operator -- (int) //定义后缀“--”运算 { increase t(X,Y); X--; Y--; return t; } void Increase::Display () const { cout<<"("<<X<<","<<Y<<")"<<endl; } int main() { Increase object (1,2),temp; temp=object; cout<<"temp 初值:"; temp.Display(); temp=++object; cout<<"temp=++object后temp的值"; temp.Display(); ... //包含另外三个函数调用 return 0; } 说明: (1)在定义后缀运算时,不使用形参int,后缀运算返回的是原值,因此要先复制原值再改变,并且只需返回值,不需返回引用。 (2) 后缀运算符定义也可以调用前缀运算符,后缀“++”定义可改写为: Increase Increase :: operator ++ (int) { Increase t(X,Y); ++(*this); return t; } (3) 为了和内置类型的定义保持一致,前缀运算符返回左值,所以返回值类型的引用。 一般将自增自减运算符重载定义为成员函数。
#include<iostream> using namespace std; clas Baseclass { public: Baseclass(int a){X=a;} void Disp() { cout<<"x="<<X<<endl; } private: int X; }; class Derivedclass:public Baseclass { public: Derivedclass(int i,int j):Baseclass(i),Y(j) { } //可以在成员初始化列表中初始化数据成员 void Disp() { Baseclass:: Disp(); cout<<"y="<<Y<<endl; } private: int Y; }; int main() { Baseclass base_obj(1); cout<<"输出基类对象的成员:"<<endl; base_obj.Disp(); Derivedclass derived_obj(2,3); cout<<"输出派生类对象的成员:"<<endl; derived_obj.Disp(); return 0; } 说明:(1)基类和派生类的函数同名类型和参数列表相同,但不会产生二义性,而是同名覆盖。 (2)通过派生类对象调用成员Disp()时,调用的是派生类中重写的Disp()成员函数。 (3)当通过基类指针访问派生类对象时,调用成员函数Disp()是继承来自基类的成员而非派生类中重写的成员,与预期不符。C++提供了一种机制,只要将基类中的成员函数声明为虚函数,当通过基类指针访问派生类对象时,调用成员函数Disp()是派生类中重写的函数了。
7.3.1 虚成员函数
将成员函数声明为虚函数,只要在函数声明时在函数返回类型前加上关键字virtual即可,格式如下:
virtual <类型> <函数名> (<参数表>);
例7.9虚成员函数的使用。
讯享网#include<iostream> using namespace std; clas Baseclass { public: Baseclass(int a){X=a;} virtual void Disp() { cout<<"x="<<X<<endl; } private: int X; }; class Derivedclass:public Baseclass { public: Derivedclass(int i,int j):Baseclass(i),Y(j) { } //可以在成员初始化列表中初始化数据成员 void Disp() { Baseclass:: Disp(); cout<<"y="<<Y<<endl; } private: int Y; }; int main() { Baseclass base_obj(1); Derivedclass derived_obj(2,3); Baseclass *basej_p=&base_obj; cout<<"指针指向基类的对象:"<<endl; basej_p->Disp(); basej_p=&derived_obj; cout<<"指针指向派生类对象:"<<endl; derived_obj->Disp(); return 0; } 说明: basej_p->Disp()同一条语句执行了不同的程序代码,对该函数的联编是在运行阶段,即动态联编,根据指针所指向对象的类型决定调用那个函数,实现了运行多态。
虚函数使用注意事项:
(1)基类中的虚函数与派生类中重写的同名函数不但要求名字相同,而且返回值和参数表也要相同。否则看出函数重载,即使加上virtual关键字,也不会动态绑定。如果基类中的虚函数返回一个基类的指针或引用,派生类的虚函数也返回一个基类的指针或引用,C++仍可将其视为虚函数进行动态绑定。
(2)基类中的虚函数在派生类中仍然是虚函数,并且可以通过派生一直将这个虚函数继承下去,且不需要加virtual。
(3)虚函数的声明只能出现在类定义时的函数声明中。
(4)虚函数只能是类的成员函数,不能是友元函数,因为虚函数只适应于有继承关系的类的对象。
不能是静态成员函数,因为基类定义了静态成员,那么整个类继承层次中只能有一个这样的成员。
不能是内联函数,因为内联函数的代码替换是在编译时完成的。
(5) 只能通过指针或引用来操作虚函数实现多态。
(6)设计派生类时,如果不为了实现多态,最好避免与基类使用相同的成员名,避免发生同名覆原则。
7.3.2 虚析构函数
不能声明虚构造函数,但可以声明虚析构函数。如果派生类中存在动态内存分配,并且内存释放工作是在析构函数中实现的,这时必须将析构函数声明为虚函数。
例7.10 非虚构函数
#include<iostream> using namespace std; clas Baseclass { public: Baseclass() { cout<<"调用基类构造函数"<<endl; } virtual void Disp() const { cout<<"输出基类成员"<<endl; } ~Baseclass() { cout<<"调用基类析构函数"<<endl; } }; class Derivedclass :public Baseclass { public: Derivedclass() { cout<<"调用派生类构造函数"<<endl; } void Disp() const { cout<<"输出派生类成员"<<endl; } ~Derivedclass() { cout<<"调用派生类析构函数"<<endl; } } int main() { Baseclass *base_p=new Derivedclass(); *base_p->Disp(); delete base_p; //手动释放派生类剩余的成员(成员函数) return 0; }
程序的运行结果:
讯享网调用基类构造函数 调用派生类构造函数 输出派生类成员 调用基类析构函数
例7.10 虚构函数
#include<iostream> using namespace std; clas Baseclass { public: Baseclass() { cout<<"调用基类构造函数"<<endl; } virtual void Disp() const { cout<<"输出基类成员"<<endl; } virtual ~Baseclass() { cout<<"调用基类析构函数"<<endl; } }; class Derivedclass :public Baseclass { public: Derivedclass() { cout<<"调用派生类构造函数"<<endl; } void Disp() const { cout<<"输出派生类成员"<<endl; } ~Derivedclass() { cout<<"调用派生类析构函数"<<endl; } }
程序的运行结果:
讯享网调用基类构造函数 调用派生类构造函数 输出派生类成员 调用派生类析构函数 调用基类析构函数
说明:(1)把基类析构函数声明为虚函数后,通过基类指针释放其所指的派生类对象时是按照对派生类对象的释放顺序进行的。
(2)如果一个类的析构函数是虚函数,那么它派生类所有的析构函数也是虚函数如果派生类类继续派生下去,这个性质也将一直被继承。
(3)即使处于继承最顶层的根基类的析构函数没有实际工作做,也应该定义一个虚构函数。像其他虚函数一样,析构函数的虚函数形状将被继承,无论派生类显示定义还是使用默认析构函数,派生类析构函数都是虚函数。
- 7.4抽象类
是一种特殊的类,是为了抽象的目的而建立的,它所描述的是所有派生类的共性,那些高度抽象、无法具体化的共性由纯虚函数来描述。含有纯虚函数的类称为抽象类,一个抽象类至少有一个纯虚函数。
7.4.1 纯虚函数
设计基类时,遇到了其成员函数不能被全部实现的情况。例如:不知道具体的图形,所以计算面积的成员函数难以实现。但为了能够多态的使用该函数,在基类中还需要定义该成员函数,这时把它定义为纯虚函数。定义格式如下:
virtual <类型> <函数名>(<参数表>)=0;
纯虚函数不需要定义函数体,其值为0。纯虚函数的函数体有"=0 "替代。与空函数不同空函数有函数体,由一对花括号阔起来,含有空函数的类可以创建对象。含有纯虚函数的类不能创建对象,纯虚函数为派生类提供了一个空位置,为所有派生类提供一个公共接口。派生类中必须对纯虚函数进行重写,才能创建对象。
7.4.2抽象类与具体类
含有纯虚函数的是抽象类,抽象类不能创建对象,只能用作基类,其存在是为了实现运行时多态。定义抽象类是为了建立一个类的通用框架,用于引导建立一系列结构类似的完整派生类,为整个类组提供统一的接口形式。
抽象类虽然不能创建对象,但是可以定义指针和引用。但它们只能指向派生类对象,用来实现运行时多态。
例7.12抽象类的应用—图形类
#include<iostream> using namespace std; const double PI=3.14; class Shape //图形类-抽象类 { public: virtual double Area()=0; virtual void Disp()=0; }; class Circle:public Shape //圆类-具体类 { public: Circle(double r){ R=r;} double Area(){return PI*R*R;} void Disp(){cout<<"圆半径:"<<R;} privae: double R; }; class Square:public Shape //正方形类-具体类 { public: Square(double a){ A=a; } //纯虚函数 double Area(){return A*A;} //纯虚函数 void Disp(){cout<<"正方形边长:"<<A;} private: double A; }; class Rectangle:public Shape //长方形类-具体类 { public: Rectangle(int a,int b){ A=a;B=b; } double Area(){return A*B;} void Disp(){cout<<"长方形边长:"<<A<<"宽:"<<B;} private: double A,B; }; int main() { Shape *p; Circle Circle(1); Square Square(2); Rectangle Rectangle(3,4); p=&Circle; p->Disp(); cout<<"\t\t 面积是:"<<p->Area()<<endl; p=□ p->Disp(); cout<<"\t\t 面积是:"<<p->Area()<<endl; p=&Rectangle; p->Disp(); cout<<"\t\t 面积是:"<<p->Area()<<endl; return 0; } 说明:(1)在程序中,Shape为抽象类,不能创建对象,但可以定义指针。 (2)Area()和Disp()为纯虚函数,为整个图形类提供了一个统一的操作界面。在派生类中分别重写,实现各自的功能。 (3)在运行的不同阶段基类指针指向了不同的派生类对象,因此同样的语句p->Disp()和p->Area()执行了不同的代码,实现了运行时多态。
抽象类具有如下特点:
(1)抽象类只能做其他类的基类,不能实例化,纯虚函数由派生类实现。
(2)只有派生类给出基类所以纯虚函数的实现,这派生类为具体类,可以创建对象。否则仍然为抽象类。
(3)可以定义抽象类指针和引用,但必须使其指向派生类对象,以便多态的使用它们。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/31933.html