• 增加了面向对象软件系统的灵活性
• 减少了冗余信息
• 提高了软件的可重用性和可扩充性
多态性是面向对象的一个重要特征。什么是多态性?
以下为结构化编程中的一个例子:
void fuite_eat(int objFruit)
{
switch(objFruit)
case 0: //apple
apple_eat();
//printf("apple eat");
break;
case 1: //orange
orange_eat();
//printf("orange eat");
break;
case 2: //peal
peal_eat();
//printf("peal eat");
break;
}
上面的例子中以水果为例,不同的水果的吃法可能各不相同,这样要根据不同的水果类型来判断调用特定的水果的吃的方法。
在程序编译时系统预先绑定了各种水果吃的方法,这就是“前期绑定”。
但当增加一种水果时,不得不更改上述程序,新增加一个入口,并调用新水果的吃法。
面向对象技术中的多态性运用“后期绑定”技术解决此问题。
可以在基类中定义一个虚函数,然后在派生类中覆盖它,当调用此方法时,系统会根据对象的类型而决定调用哪一个对象的方法(即:不同对象收到相同消息时,产生不同的动作 )。
如:
class fruit
{ virtual void eat()=0; }
class apple:public fruit
{
void eat() { printf("apple eat"); }
}
class orange:public fruit
{
void eat() { printf("orange eat"); }
}
void fruite_eat(fruit * f)
{
f->eat();
}
• 编译器发现一个类中有被声明为virtual的函数,就会为它建一个虚函数表,即 VTABLE。
• VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。
•派生类有自己的VTABLE,但是,派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。
•在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。
•上例中:系统在得到对象的指针后会查找VTABLE,找到方法的函数指针,然后调用该方法。如果此方法未被派生类实现,则系统会调用其基类的方法。
•函数地址的后期绑定在面向对象的编程语言中是非常重要的,在java中对类中所有函数都进行了后期绑定,但C++,则是让程序开发者决定对哪一个函数进行后期绑定。
•为多态性行为提供后期绑定是要付出代价的,因为在类的实现时VTABLE要进行初始化且在调用虚函数时必须在运行时查找虚函数表。
1 多态性概述
•多态性含义:指不同对象收到相同的消息时,产生不同的动作。
•体现在程序中为:多态性是指用一个名字定义不同的函数,这些函数执行不同但又类似的操作,从而可以使用相同的调用方式来调用这些具有不同功能的同名函数。
1.1 多态的分类
C++中的多态性可以分为四类:
•参数多态:由类模板实例化各个类都有的具有相同的操作,而操作对象的类型却各不相同
•包含多态:主要通过虚函数来实现。强调不同类中的同名成员函数的多态行为
•强制多态:即,将一个变元的类型实例化,比如加法运算时候浮点数与整数的强制转换,即运算符重新定义(重载)。
•重载多态
前面两种统称为通用多态,而后面两种统称为专用多态。
1.2 多态的实现
多态从实现的角度分为两类:
–编译时的多态:
是通过静态联编来实现的。静态联编就是在编译阶段完成的联编。编译时多态性主要是通过函数重载和运算符重载实现的。
–运行时的多态
是用动态联编实现的。动态联编是运行阶段完成的联编。运行时多态性主要是通过虚函数来实现的。
2 运算符重载
什么是运算符重载?
•重载,即重新赋予新的含义。
•函数重载就是对一个已有的函数赋予新的含义,使之实现新功能。
•运算符也可以重载,对C++已提供的运算符进行重载,赋予它们新的含义,使之一名多用。譬如,用“+”号如何进行两个复数的相加?C++中不能直接用运算符“+”对复数进行相加运算。
•可以通过定义一个专门的函数来实现复数相加。
通过函数来实现复数相加
#include<iostream>
using namespace std;
class Complex
{
public:
Complex()
{
real = 0;
imag = 0;
}
Complex(double r,double i)
{
real = r;
imag = i;
}
Complex complex_add(Complex &c2);
void display();
private:
double real;
double imag;
};
Complex Complex::complex_add(Complex &c2)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
void Complex::display()
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main(int argc,char *argv[])
{
Complex c1(3,4),c2(5,-10),c3;
c3 = c1.complex_add(c2);
cout<<"c1=";
c1.display();
cout<<"c2=";
c2.display();
cout<<"c1+c2=";
c3.display();
return 0;
}
•结果是正确的,但调用方式不直观、太烦琐,不方便。能否也和整数的加法运算一样,直接用加号“+”来实现复数运算呢?如c3=c1+c2;
•如果能做到,就为对象的运算提供了很大的方便。这就需要对运算符“+”进行重载。
2.1 运算符重载的方法
•运算符重载是通过定义函数实现的:定义一个重载运算符的函数,在需要执行被重载的运算符时,系统就自动调用该函数,以实现相应的运算。重载运算符的函数一般格式如下:
函数类型 operator 运算符名称 (形参表列)
{ 对运算符的重载处理 }
•例如,想将“+”用于Complex类(复数)的加法运算,函数的原型:
Complexoperator+ (Complex& c1,Complex& c2);
函数operator+重载了运算符+。
• 用函数的方法理解运算符:
在运算符重载后,执行表达式就是调用函数的过程。 可以把两个整数相加也想像为调用函数:
int operator + (int a,int b)
{ return (a+b); }
–如果有表达式5+8,就调用此函数,将5和8作为调用函数时的实参,函数的返回值为13。
重载运算符“+”,使之能用于两个复数相加。
#include<iostream>
using namespace std;
class Complex
{
public:
Complex()
{
real = 0;
imag = 0;
}
Complex(double r,double i)
{
real = r;
imag = i;
}
Complex operator+(Complex &c2);
void display();
private:
double real;
double imag;
};
Complex Complex::operator+(Complex &c2)
{
Complex c;
c.real = real + c2.real;
c.imag = imag + c2.imag;
return c;
}
void Complex::display()
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main(int argc,char *argv[])
{
Complex c1(3,4),c2(5,-10),c3;
c3 = c1 + c2;
cout<<"c1=";
c1.display();
cout<<"c2=";
c2.display();
cout<<"c1+c2=";
c3.display();
return 0;
}
•比较例1和例2,只有两处不同:
(1) 在例2中以operator+函数取代了例1中的complex_add函数,而且只是函数名不同,函数体和函数返回值的类型都是相同的。
(2) 在main函数中,以“c3=c1+c2;”取代了例1中的“c3=c1.complex_add(c2);”。在将运算符+重载为类的成员函数后,C++编译系统将程序中的表达式c1+c2解释为:
c1.operator+(c2)//其中c1和c2是Complex类的对象
–即以c2为实参调用c1的运算符重载函数
operator+(Complex &c2),进行求值,得到两个复数之和。
•对上面的运算符重载函数operator+还可以
改写得更简练一些:
Complex Complex∷operator + (Complex &c2)
{ return Complex(real+c2.real, imag+c2.imag); }
需要说明的是: 运算符被重载后,其原有的功能仍然保留,没有丧失或改变。
•通过运算符重载,扩大了C++已有运算符的作用范围,使之能用于类对象。运算符重载使C++具有更强大的功能、更好的可扩充性和适应性,这是C++最吸引人的特点之一。
2.2 重载运算符的规则
(1) C++不允许用户自己定义新的运算符,只能对已有的C++运算符进行重载。
(2) C++中绝大部分的运算符允许重载。不能重载的运算符只有5个:
•. (成员访问运算符)
•.* (成员指针访问运算符)
•∷ (域运算符)
•sizeof (长度运算符)
•? : (条件运算符)
前两个运算符不能重载是为了保证访问成员的功能不能被改变,域运算符和sizeof运算符的运算对象是类型而不是变量或一般表达式,不具重载的特征。
(3) 重载不能改变运算符运算对象(即操作数)的个数。
(4) 重载不能改变运算符的优先级别。
(5) 重载不能改变运算符的结合性。
(6) 重载运算符的函数不能有默认的参数,否则就改变了运算符参数的个数,与前面第(3)点矛盾。
(7) 重载的运算符必须和用户定义的自定义类型的对象一起使用,其参数至少应有一个是类对象(或类对象的引用)。也就是说,参数不能全部是C++的标准类型,以防止用户修改用于标准类型数据的运算符的性质。
(8) 用于类对象的运算符一般必须重载,但有两个例外,运算符“=”和“&”不必用户重载。
① 赋值运算符(=)可以用于每一个类对象,可以利用它在同类对象之间相互赋值。
② 地址运算符&也不必重载,它能返回类对象在内存中的起始地址。
(9) 应当使重载运算符的功能类似于该运算符作用于标准类型数据时所实现的功能。
(10) 运算符重载函数可以是类的成员函数(如例2),也可以是类的友元函数,还可以是既非类的成员函数也不是友元函数的普通函数。
2.3 运算符重载函数作为类成员函数和友元函数
例2对“+”进行了重载,使之能用于两个复数的相加。其中运算符重载函数operator+作为Complex类中的成员函数。
“+”是双目运算符,为什么在例7.2程序中的重载函数中只有一个参数呢?实际上,运算符重载函数有两个参数,由于重载函数是Complex类中的成员函数,有一个参数是隐含的,运算符函数是用this指针隐式地访问类对象的成员。
•重载函数operator+访问了两个对象中的成员,一个是this指针指向的对象中的成员,一个是形参对象中的成员。如this>real+c2.real,this->real就是c1.real。
•前面已说明,在将运算符函数重载为成员函数后,如果出现含该运算符的表达式,如
c1+c2,编译系统把它解释为:
c1.operator+(c2)
•运算符重载函数的返回值是Complex类型,返回值是复数c1和c2之和(Complex(c1.real +c2.real,c1.imag+c2.imag))。
•运算符重载函数除了可以作为类的成员函数外,还可以是非成员函数。
•将运算符“+”重载为适用于复数加法,重载函数不作为成员函数,而放在类外,作为Complex类的友元函数。
#include<iostream>
using namespace std;
class Complex
{
public:
Complex()
{
real = 0;
imag = 0;
}
Complex(double r,double i)
{
real = r;
imag = i;
}
friend Complex operator+(Complex &c1,Complex &c2);
void display();
private:
double real;
double imag;
};
Complex operator+(Complex &c1,Complex &c2)
{
return Complex(c1.real+c2.real,c1.imag+c2.imag);
}
void Complex::display()
{
cout<<"("<<real<<","<<imag<<"i)"<<endl;
}
int main(int argc,char *argv[])
{
Complex c1(3,4),c2(5,-10),c3;
c3 = c1 + c2;
cout<<"c1=";
c1.display();
cout<<"c2=";
c2.display();
cout<<"c1+c2=";
c3.display();
return 0;
}
•将运算符“+”重载为非成员函数后,C++编译系统将程序中的表达式c1+c2解释为:
operator+(c1,c2)
•即执行c1+c2相当于调用以下函数:
Complex operator + (Complex &c1,Complex &c2)
{return Complex(c1.real+c2.real, c1.imag+c2.imag);}
• 为什么把运算符函数作为友元函数呢?
因为运算符函数要访问Complex类对象中的成员。如果运算符函数不是Complex类的友元函数,而是一个普通的函数,它是没有权利访问Complex类的私有成员的。