多态是指同样的消息被不同类型的对象接收时导致不同的行为。
面向对象的多态可以分为4种:
(1)重载多态:包括C++中的函数重载以及运算符重载(实际上也是函数重载);
(2)强制多态:例如浮点数与整型数相加时,先把整型数转换为浮点数再相加;
(3)包含多态:指类族中定义于不同类中的同名成员函数的多态行为,主要通过虚函数实现;
(4)参数多态:指通过类模板、函数模板生成的多态。
多态也可以从实现的角度分:
(1)编译时多态。是指在编译的过程中就确定了同名操作的具体操作对象;
(2)运行时多态。是指在运行的时候才动态的确定操作所针对的对象。
下面我们介绍一下用的比较多的C++的多态形式。
1、运算符重载
运算符重载是指让现在已有的运算符有多重含义,使得同一个运算符在处理不同的形式的数据时,能够导致不同的行为。实际上,运算符重载也属于函数重载的一种。
下面我们举一个例子还解释运算符重载的含义。我们都知道两个int类型的变量是可以使用“+”进行求和操作的,这很容易理解。那么,如果我们定义一个复数类Complex,它的两个对象可以通过“+”操作符实现操作吗?很明显这样是不行的,因为“+”符号不知道如何对这种类型的数据进行处理。我们的运算符重载的功能就是为诸如Complex等自定义的类定义运算符应有的操作。
运算符的重载规则:
(1)C++中除了少数几个运算符不能够重载之外,剩下的全部可以重载,而且只能重载C++中已经定义的运算符(不能新创运算符啊~)。不能够重载的运算符有:
类属关系运算符“.”、成员指针运算符“.*”、作用域运算符“::”和三目运算符“ ? : ”。
(2)运算符重载以后,不会改变优先级和结合性。
(3)重载的运算符应当与原有的功能相类似,
不能改变原运算符的操作数的个数,同时
至少要保证一个操作对象是你
自定义的类型。
运算符的重载方式有两种:
当作类的成员函数、
重载为类的非成员函数(因为很有可能用到类的私有成员,这种情况下一般定义为类的
友元函数)
定义的语法格式为:
返回类型 operator 操作符名称(形参表)
{
函数体
}
1.1、重载为类的成员函数
重载为类的成员函数时,操作符的形参表中的形参个数一般比该操作符的操作数的个数少1,比如“+”操作符的操作对象有2个,但是真正写作形参表中时,只有一个操作对象。这是因为重载为类的成员函数时,默认的左操作对象为该类的一个对象,右操作对象才为形参表中的对象。即,实际上当重载结束后,使用oprd1 B oprd2时,可以理解为oprd1.operator B(oprd2)。
下面给出一个例子,来具体讲解一下常用的操作符重载的方式,以Complex复数类为例。
#include <iostream>
using namespace std;
class Complex
{
double real;
double img;
public:
Complex() :real(0.0), img(0.0){}
Complex(double r, double i) :real(r), img(i){}
Complex operator+(const Complex& c);
Complex& operator++();//前置++
Complex operator++(int);//后置++
void display();
};
Complex Complex::operator+(const Complex& c)
{
return Complex(real + c.real, img + c.img);
}
Complex& Complex::operator++()
{
real++;
return *this;
}
Complex Complex::operator++(int)
{
Complex t = *this;
real++;
return t;
}
void Complex::display()
{
cout << real << "," << img << endl;;
}
int main()
{
Complex a(2.5, -2);
Complex b(1, 4);
Complex c = a + b;
cout << "c = a+b: " << endl;
c.display();
cout << "前置++:" << endl;
(++c).display();
cout << "后置++:" << endl;
(c++).display();
cout << "后置++以后:" << endl;
c.display();
}
这里以“+”、“前置++”、“后置++”为例写了重载操作符的几个例子。
可以看到“+”操作符与我们前面说的一样,形参表中只有一个操作符。
这里注意前置++和后置++的区别,后置++的形参表中有一个int,这个int其实在运算中不起任何作用,只是用来区分前置和后置。
细心的话可以返现,前置++返回的是一个引用,而后置++返回的则是一个普通的类型,这是因为前置++返回的运算结果是自身,而后置++返回的则是一个新的变量,而返回引用比返回一个类型对象开销要小,因此前置返回引用。而后置不能返回引用,所以返回的是一个变量。
运行结果如下:
这里需要注明一下,需要返回引用的重载操作符有:前置++、前置--、“=”操作符、[ ]操作符以及输入输出<<和>>等。
1.2、重载为非成员函数
为什么我在之前的例子中输出选择了display()函数,而不是使用更加方便的"<<"操作符呢?这是因为输入输出操作符是不能够重载为类的成员函数的。不能重载为成员函数的情况有两种,下面我们依次介绍:
(1)要重载的操作符的第一个操作数不是可以更改的类型时不能重载为成员函数;
(2)以非成员函数形式重载,能够支持更灵活的类型转换。如5.0 + c1,其中c1为Complex类型对象,如果以成员函数进行重载,那么左操作数必须为Complex类型,所以这样写就是错误的。而如果重载为非成员函数的话,Complex类的构造函数可以使实数隐含转换为Complex类型。
下面是前面的例子,我们把他们写成非成员函数。
#include <iostream>
using namespace std;
class Complex
{
double real;
double img;
public:
Complex() :real(0.0), img(0.0){}
Complex(double r, double i) :real(r), img(i){}
friend Complex operator+(const Complex& c1,const Complex& c2);
friend Complex& operator++(Complex& c);
friend Complex operator++(Complex& c,int);
//void display();
friend ostream& operator<<(ostream& o, const Complex& c);
};
Complex operator+(const Complex& c1,const Complex& c2)
{
return Complex(c1.real + c2.real, c1.img + c2.img);
}
Complex& operator++(Complex& c)
{
c.real++;
return c;
}
Complex operator++(Complex& c,int)
{
Complex t = c;
c.real++;
return t;
}
ostream& operator<<(ostream& o, const Complex& c)
{
o << c.real << " " << c.img;
return o;
}
int main()
{
Complex a(2.5, -2);
Complex b(1, 4);
Complex c = a + b;
cout << "c = a+b: " << endl;
cout << c << endl;
cout << "前置++:" << endl;
cout << (++c) << endl;
cout << "后置++:" << endl;
cout << (c++) << endl;
cout << "后置++以后:" << endl;
cout << c << endl;
}
结果与上面的运行结果相同,如下图所示:
2、虚函数
根据兼容性规则,可以使用派生类的对象来代替基类对象。如果用基类类型的指针指向派生类的对象,就可以通过该指针访问该对象,但是访问到的只是从基类继承来的同名成员。那如果想通过基类的指针实现不同的派生类中同名的成员函数的不同功能时,该怎么办呢?解决这个问题的方法是,如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名的函数声明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行时的多态。
声明虚函数的格式如下:
virtual 函数返回类型 函数名(形参表);
基类的成员函数声明为虚函数以后,其派生类的同名函数如果满足一下规则,那么也是虚函数:
(1)该函数是否与基类的虚函数有相同的参数个数以及相同的对应参数类型;
(2)该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针或引用型的返回值。
派生类的覆盖基类的成员函数时,可以使用virtual,也可以不使用,但最好加上,这样可以明确的指出,该函数为虚函数。
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void display();
void display2();
};
void Base1::display()
{
cout << "Base1 display" << endl;
}
void Base1::display2()
{
cout << "Base1 display2" << endl;
}
class Base2 :public Base1
{
public:
virtual void display();
void display2();
};
void Base2::display()
{
cout << "Base2 display" << endl;
}
void Base2::display2()
{
cout << "Base2 display2" << endl;
}
class Base3 :public Base1
{
public:
virtual void display();
void display2();
};
void Base3::display()
{
cout << "Base3 display" << endl;
}
void Base3::display2()
{
cout << "Base3 display2" << endl;
}
void fun(Base1* ptr)
{
ptr->display();
}
void fun2(Base1* ptr)
{
ptr->display2();
}
int main()
{
Base1 b1;
Base2 b2;
Base3 b3;
cout << "虚函数组:" << endl;
fun(&b1);
fun(&b2);
fun(&b3);
cout << "非虚函数组:" << endl;
fun2(&b1);
fun2(&b2);
fun2(&b3);
return 0;
}
结果如图:
这里有一点需要多提一下,对于构造函数是不能声明为虚函数的,但是对于析构函数是可以声明为虚函数的,而且 基类的析构函数最好声明为虚函数,这是因为如果有可能通过基类的指针调用派生类的析构函数时,为了防止对象切片,当基类的析构函数声明为虚函数以后,那么用基类指针执行派生类的析构函数时,会正确的执行派生类的析构函数,而非单单只有基类的析构函数。(这里比较特殊,因为析构函数的名称明显不同啊)
参考:《C++程序语言设计(第四版)》郑莉等著