第8章 多态性和虚函数.docx
《第8章 多态性和虚函数.docx》由会员分享,可在线阅读,更多相关《第8章 多态性和虚函数.docx(17页珍藏版)》请在冰点文库上搜索。
第8章多态性和虚函数
第8章多态性和虚函数
封装性基础
面向对象系统三特征继承性关键
多态性补充
多态性是指发出同样的消息被不同类型的对象接收时导致完全不同的行为。
这里所说的消息主要是指对类的成员函数的调用,而不同的行为是指不同的实现。
利用多态性,用户只需发送一般形式的消息,而将所有的实现留给接收消息的对象。
多态的类型:
简单的多态性是函数重载和运算符重载。
重要的多态性是建立在虚函数的概念和方法上的。
8.1函数重载
8.2运算符重载
运算符重载就是赋予已有的运算符多重含义,使它能够用对类的对象进行特定的操作。
8.2.1运算符重载的几个问题
1.C++中不能重载的运算符是:
.,.*,:
:
?
:
2.运算符重载不改变原运算符的优先级和结合性。
3.编译程序对运算符重载的选择,遵循函数重载的原则。
4.重载运算符的限制:
(1)不可臆造新的运算符。
(2)重载运算符坚持4个“不能改变”。
·不能改变运算符操作数的个数;
·不能改变运算符原有的优先级;
·不能改变运算符原有的结合性;
·不能改变运算符原有的语法结构。
5.运算符重载时必须遵循哪些原则:
(1)重载运算符含义必须清楚。
(2)重载运算符不能有二义性。
8.2.2运算符重载的两种形似
运算符重载的两种形式:
成员函数形式和友元函数形式。
1.重载为类的成员函数
例8.2复数类四则运算重载
#include
classcomplex
{public:
complex(){real=imag=0;}
complex(doubler,doublei){real=r,imag=i;}
complexoperator+(constcomplex&c);
complexoperator-(constcomplex&c);
complexoperator*(constcomplex&c);
complexoperator/(constcomplex&c);
friendvoidprint(constcomplex&c);
private:
doublereal,imag;
};
inlinecomplexcomplex:
:
operator+(constcomplex&c)
{ returncomplex(real+c.real,imag+c.imag);}
inlinecomplexcomplex:
:
operator-(constcomplex&c)
{ returncomplex(real-c.real,imag-c.imag);}
inlinecomplexcomplex:
:
operator*(constcomplex&c)
{ returncomplex(real*c.real-imag*c.imag,real*c.imag+imag*c.real);}
inlinecomplexcomplex:
:
operator/(constcomplex&c)
{ returncomplex((real*c.real+imag+c.imag)/(c.real*c.real+c.imag*c.imag),
(imag*c.real-real*c.imag)/(c.real*c.real+c.imag*c.imag));}
voidprint(constcomplex&c)
{ if(c.imag<0)
cout<else
cout<}
voidmain()
{ complexc1(2.0,3.0),c2(4.0,-2.0),c3;
c3=c1+c2;
cout<<"\nc1+c2=";
print(c3);
c3=c1-c2;
cout<<"\nc1-c2=";
print(c3);
c3=c1*c2;
cout<<"\nc1*c2=";
print(c3);
c3=c1/c2;
cout<<"\nc1/c2=";
print(c3);
c3=(c1+c2)*(c1-c2)*c2/c1;
cout<<"\n(c1+c2)*(c1-c2)*c2/c1=";
print(c3);
cout<}
该程序的运行结果为:
c1+c2=6+1i
c1-c2=-2+5i
c1*c2=14+8i
c1/c2=0.45+0.8i
(c1+c2)*(c1-c2)*c2/c1=9.61538+25.2308i
小结:
1.在程序中,定义了4个成员函数作为运算符重载函数。
将运算符重载函数说明为类的成员函数格式如下:
类名operator运算符(参数表)
2.程序中出现的表达式:
c1+c2编译程序将给解释为:
c1.operator+(c2)
其中,c1和c2是complex类的对象。
operator+()是运算+的重载函数。
3.该运算符重载函数仅有一个参数c2。
可见,当重载为成员函数时,双目运算符仅有一个参数。
对单目运算符,重载为成员函数时,不能再显式说明参数。
重载为成员函数时,总时隐含了一个参数,该参数是this指针。
this指针是指向调用该成员函数对象的指针。
例8.2时间类
#include
classTime
{public:
Time(){hours=minutes=seconds=0;}
Time(inth,intm,ints)
{hours=h;minutes=m;seconds=s;}
Timeoperator+(Time&c);//+运算符重载
Timeoperator++();//前置单目运算符重载
Timeoperator++(int);//后置单目运算符重载
voidShowTime();
private:
inthours,minutes,seconds;
};
TimeTime:
:
operator+(Time&c)//+运算符重载
{hours=hours+c.hours;
minutes=minutes+c.minutes;
seconds=seconds+c.seconds;
returnTime(*this);}
TimeTime:
:
operator++()//前置单目运算符重载
{++seconds;
returnTime(*this);
}
TimeTime:
:
operator++(int)//后置单目运算符重载
{seconds++;//注意形参表中的参数
returnTime(*this);
}
voidTime:
:
ShowTime()
{if(seconds>=60)
{seconds-=60;minutes++;}
if(minutes>=60)
{minutes-=60;hours++;}
if(hours>=24)
hours-=24;
cout<’<’<}
voidmain()
{Timet1(2,23,45),t2(12,34,46),t3,t4;
t3=t1+t2;
t3.ShowTime();
t1++;
t1.ShowTime();
++t4;
t4.ShowTime();
}
输出:
14:
58:
31
14:
58:
32
0:
0:
1
在本例中,我们把时间自增前置“++”和后置“++”运算重载为时间类的成员函数,前置单目运算符和后置单目运算符重载的最主要的区别就在于重载函数的形参。
语法规定,前置单目运算符重载为成员函数时没有形参,而后置单日运算符重载为成员函数时需要有一个int型形参。
这个int型参数在函数体中并不使用,纯粹是用来区别前置与后置。
因此参数表中可以只给出类型名,没有参数名。
2.重载为友元函数
当重载友元函数时,将没有隐含的参数this指针。
这样,对双目运算符,友元函数有2个参数,对单目运算符,友元函数有一个参数。
但是,有些运行符不能重载为友元函数,它们是:
=,(),[]和->。
重载为友元函数的运算符重载函数的定义格式如下:
friend类型说明符operator运算符(参数表)
{……}
例8.4用友元函数代替成员函数,重编例8.2。
#include
classcomplex
{public:
complex(){real=imag=0;}
complex(doubler,doublei){real=r,imag=i;}
friendcomplexoperator+(constcomplex&c1,
constcomplex&c2);
friendcomplexoperator-(constcomplex&c1,
constcomplex&c2);
friendcomplexoperator*(constcomplex&c1,
constcomplex&c2);
friendcomplexoperator/(constcomplex&c1,
constcomplex&c2);
friendvoidprint(constcomplex&c);
private:
doublereal,imag;
};
complexoperator+(constcomplex&c1,constcomplex&c2)
{ returncomplex(c1.real+c2.real,c1.imag+c2.imag);}
complexoperator-(constcomplex&c1,constcomplex&c2)
{returncomplex(c1.real-c2.real,c1.imag-c2.imag);}
complexoperator*(constcomplex&c1,constcomplex&c2)
{returncomplex(c1.real*c2.real-c1.imag*c2.imag,
c1.real*c2.imag+c1.imag*c2.real);}
complexoperator/(constcomplex&c1,constcomplex&c2)
{returncomplex((c1.real*c2.real+c1.imag+c2.imag)/(c2.real*c2.real+c2.imag*c2.imag),(c1.imag*c2.real-c1.real*c2.imag)/(c2.real*c2.real+c2.imag*c2.imag));
}
voidprint(constcomplex&c)
{ if(c.imag<0)
cout<else
cout<}
voidmain()
{complexc1(2.0,3.0),c2(4.0,-2.0),c3;
c3=c1+c2;
cout<<"\nc1+c2=";
print(c3);
c3=c1-c2;
cout<<"\nc1-c2=";
print(c3);
c3=c1*c2;
cout<<"\nc1*c2=";
print(c3);
c3=c1/c2;
cout<<"\nc1/c2=";
print(c3);
c3=(c1+c2)*(c1-c2)*c2/c1;
cout<<"\n(c1+c2)*(c1-c2)*c2/c1=";
print(c3);
cout<}
注意:
1、该程序的运行结果与上例相同。
2、程序中出现的c1+c2编译程序解释为:
operator+(c1,c2)调用如下函数,进行求值,
complexoperator+(constcomplex&c1,constcomplex&c2)
3.两种重载形式的比较
一般情况下,单目运算符最好重载为成员函数;双目运算符最好重载为友元函数。
8.3静态联编和动态联编
联编是指一个计算机程序自身彼此关联的过程。
按照联编所进行的阶段不同,可分为:
静态联编和动态联编。
8.3.1静态联编
静态联编是指联编工作出现在编译连接阶段,又称早期联编,因为这种联编过程是在程序开始运行之前完成的。
在编译时所进行的这种联编又称静态束定。
例8.4下面举一个静态联编的例子。
#include
classPoint
{public:
Point(doublei,doublej){x=i;y=j;}
doubleArea()const{return0.0;}
private:
doublex,y;
};
classRectangle:
publicPoint
{public:
Rectangle(doublei,doublej,doublek,doublel)
:
Point(i,j)
{ w=k;h=l;}
doubleArea()const{returnw*h;}
private:
doublew,h;
};
voidfun(Point&s)
{ cout<voidmain()
{ Rectanglerec(3.0,5.2,15.0,25.0);
fun(rec);
}
运行结果为:
0
输出结果表明在fun()函数中,s所引用的对象执行的Area()操作被关联到Point:
:
Area()的实现代码上。
这是因为静态联编的结果。
在程序编译阶段,对s所引用的对象所执行的Area()操作只能束定到Point类的函数上。
因此,导致程序输出了所不期望的结果。
因为我们期望的是s引用的对象所执行的Area()操作应该束定到Rectangle类的Area()函数上。
这是静态联编所达不到的。
8.3.2动态联编
从对静态联编的上述分析中可以知道,编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进行联编工作被称为动态联编,或称动态束定,又叫晚期联编。
动态联编实际上是进行动态识别。
C++规定动态联编是在虚函数的支持下实现的。
可见,静态联编和动态联编也都是属于多态性的,它们是不同阶段对不同实现进行不同的选择。
8.7虚函数
虚函数是动态联编的基础。
虚函数的定义方法:
virtual类型说明符函数名(参数表)
虚函数在派生类中可能有不同的实现。
动态联编只能通过指针或引用标识对象来操作虚函数。
如果采用一般类型的标识对象来操作虚函数,则将采用静态联编的方式调用虚函数。
例8.9
#include
classPoint
{public:
Point(doublei,doublej){x=i;y=j;}
virtualdoubleArea()const{return0.0;}//虚函数
private:
doublex,y;
};
classRectangle:
publicPoint
{public:
Rectangle(doublei,doublej,doublek,doublel)
:
Point(i,j)
{w=k;h=l;}
virtualdoubleArea()const{returnw*h;}//虚函数
private:
doublew,h;
};
voidfun(Point&s)
{cout<}
voidmain()
{Rectanglerec(3.0,5.2,15.0,25.0);
fun(rec);
}
输出:
375
派生类中对基类的虚函数进行替换时,要求派生类中说明的虚函数与基类中被替换的虚函数之间应满足:
(1)与基类的虚函数的参数个数相同。
(2)与基类的虚函数的参数类型一致。
(3)其返回值或者与基类虚函数的相同,或者都返回指针或引用。
满足上述条件的派生类的成员函数,自然是虚函数,不必加virtual说明。
例8.10
#include
classA
{public:
virtualvoidact1()//虚函数
{cout<<”A:
:
act1()called.”<voidact2()
{act1();}//成员函数调用虚函数
};
classB:
publicA
{public:
voidact1()//省略virtual
{cout<<”B:
:
act1()called.”<};
voidmain()
{Bb;
b.act2();
}
输出:
B:
:
act1()called.
如果:
voidact2()
{this->act1();}
输出:
B:
:
act1()called.
如果:
voidact2()
{A:
:
act1();}
输出:
A:
:
act1()called.
总结动态联编的实现条件:
(1)要有说明的虚函数;
(2)调用虚函数操作的是指向对象的指针或者对象引用;或者是由成员函数调用虚函数。
(3)子类型关系的建立
构造函数中调用虚函数时,采用动态联编即构造函数调用的虚函数是自己类中实现的虚函数,如果自己类中没有实现这个虚函数,则调用基类中的虚函数,而不是任何派生类中实现的函数。
析构造函数中调用虚函数时与构造函数相同。
例8.11
#include
classA
{public:
A(){}
virtualvoidf()//虚函数
{cout<<”A:
:
f()called.”<};
classB:
publicA
{public:
B(){f();}
voidg(){f();}
};
classC:
publicB
{public:
C(){}
virtualvoidf()
{cout<<”C:
:
f()called.”<};
voidmain()
{Cc;
c.g();
}
输出:
A:
:
f()called.
C:
:
f()called.
说明:
执行Cc;语句时,系统调用构造函数给c初始化,先调C(){},然后调B(){f();}。
在该类中没有f()虚函数的实现,因此调用它的基类的虚函数,所以出现A:
:
f()called.
例8.12
#include
classA
{public:
virtualvoidprint(intx,inty)//虚函数
{cout<};
classB:
publicA
{public:
virtualvoidprint(intx,floaty)//虚函数
{cout<};
voidshow(A&a)
{a.print(3,8);
a.print(6,5.9);
}
voidmain()
{Bb;
show(b);
}
编译时出现一个警告错。
输出:
3,8
6,5
说明:
一般要求基类中说明了虚函数后,派生类中说明的虚函数应该与基类中虚函数的个数相等,对应的参数的类型相同。
如果不相同,则派生类中虚函数的参数类型强制转换为基类中虚函数的类型。
8.5纯虚函数和抽象类
8.5.1纯虚函数
在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它说明为纯虚函数,它的实现放到派生类去做。
纯虚函数定义格式如下:
virtual类型函数名(参数表)=0;
例8.14
#include
classpoint
{public:
point(inti=0,intj=0){x0=i;y0=j;}
virtualvoidset()=0;//纯虚函数
virtualvoiddraw()=0;//纯虚函数
protected:
intx0,y0;
};
classline:
publicpoint
{public:
line(inti=0,intj=0,intm=0,intn=0):
point(i,j){x1=m;y1=n;}
voidset(){cout<<”line:
:
set()called.\n”;}
voiddraw(){cout<<”line:
:
draw()called.\n”;}
protected:
intx1,y1;
};
classellipse:
publicpoint
{public:
ellipse(inti=0,intj=0,intp=0,intq=0):
point(i,j)
{x2=p;y2=q;}
voidset(){cout<<”ellipse:
:
set()called.\n”;}
voiddraw(){cout<<”ellipse:
:
draw()called.\n”;}
protected:
intx2,y2;
};
voiddrawobj(point*p)
{p->draw();}
voidsetobj(point*p)
{p->set();}
voidmain()
{line*lineobj=newline;
ellipse*elliobj=newellipse;
drawobj(lin