注意:本文中代码均使用 Qt 开发编译环境
面向对象的多态性可以分为四类:重载多态、强制多态、包含多态和参数多态,前两种统称为专用多态,而后两种也称为通用多态。
多态的实现###
多态从实现的角度可以划分为:编译时多态和运行时的多态。
确定操作的具体对象的过程就是绑定(binding,也叫联编)。绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程;就是把一条消息和一个对象的方法相结合的过程。按照绑定进行的阶段不同,可分为:静态绑定和动态绑定,这两种绑定过程中分别对应着多态的两种实现方式:
1.绑定工作在编译连接阶段完成的情况称为静态绑定。也叫早期绑定或前绑定。
2.绑定工作在程序运行阶段完成的情况称为动态绑定。也叫晚期绑定或后绑定。
运算符重载###
C++ 中预定义的运算符的操作对象只能是基本数据类型。但实际上,对于许多用户自定义类型(例如类),也需要类似的运算操作。这时就必须在C++中重新定义这些运算符,赋予已有运算符新的功能,使它能够用于特定类型执行特定的操作。运算符重载的实质是函数重载,它提供了 C++ 的可扩展性,也是 C++ 最吸引人的特性之一。
运算符重载是通过创建运算符函数实现的,运算符函数定义了重载的运算符将要进行的操作。运算符函数的定义与其他函数的定义类似,惟一的区别是运算符函数的函数名是由关键字 operator 和其后要重载的运算符符号构成的。
运算符函数定义的一般格式如下:
<返回类型说明符> operator <运算符符号> (<参数表>)
{
<函数体>
}
运算符重载时要遵循以下规则:
(1) 除了类属关系运算符 "." 、成员指针运算符 ".*" 、作用域运算符 "::" 、sizeof 运算符和三目运算符 "?:" 以外,C++ 中的所有运算符都可以重载。
(2) 重载运算符限制在 C++ 语言中已有的运算符范围内的允许重载的运算符之中,不能创建新的运算符。
(3) 运算符重载实质上是函数重载,因此编译程序对运算符重载的选择,遵循函数重载的选择原则。
(4) 重载之后的运算符不能改变运算符的优先级和结合性,也不能改变运算符操作数的个数及语法结构。
(5) 运算符重载不能改变该运算符用于内部类型对象的含义。它只能和用户自定义类型的对象一起使用,或者用于用户自定义类型的对象和内部类型的对象混合使用时。
(6) 运算符重载是针对新类型数据的实际需要对原有运算符进行的适当的改造,重载的功能应当与原有功能相类似,避免没有目的地使用重载运算符。
示例1:
#include <QCoreApplication>
#include <QDebug>
class complex{
public:
complex(double r=0.0, double i=0.0) : real(r), imag(i) {}
complex operator +(complex c2);
complex operator -(complex c2);
QString display() const;
private:
double real;
double imag;
};
complex complex::operator + (complex c2){
return complex(real + c2.real, imag + c2.imag);
}
complex complex::operator - (complex c2){
return complex(real - c2.real, imag - c2.imag);
}
QString complex::display() const {
return "(" + QString::number(real) + ", " + QString::number(imag) + ")";
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
complex c1(5, 4), c2(2, 10), c3;
qDebug() << "c1 = " << c1.display();
qDebug() << "c2 = " << c2.display();
c3 = c1 - c2;
qDebug() << "c3 = c1 - c2 = " << c3.display();
c3 = c1 + c2;
qDebug() << "c3 = c1 + c2 = " << c3.display();
return a.exec();
}
运行结果:
运算符函数重载一般有两种形式:重载为类的成员函数和重载为类的非成员函数。非成员函数通常是友元。(可以把一个运算符作为一个非成员、非友元函数重载。但是,这样的运算符函数访问类的私有和保护成员时,必须使用类的公有接口中提供的设置数据和读取数据的函数,调用这些函数时会降低性能。可以内联这些函数以提高性能。)
成员函数运算符###
运算符重载为类的成员函数的一般格式为:
<函数类型> operator <运算符> ( <参数表> ) {
<函数体>
}
当运算符重载为类的成员函数时,函数的参数个数比原来的操作数要少一个(后置单目运算符除外),这是因为成员函数用this指针隐式地访问了类的一个对象,它充当了运算符函数最左边的操作数。因此:
(1) 双目运算符重载为类的成员函数时,函数只显式说明一个参数,该形参是运算符的右操作数。
(2) 前置单目运算符重载为类的成员函数时,不需要显式说明参数,即函数没有形参。
(3) 后置单目运算符重载为类的成员函数时,函数要带有一个整型形参。
调用成员函数运算符的格式如下:
<对象名>.operator <运算符>(<参数>)
它等价于
<对象名><运算符><参数>
例如:a+b等价于a.operator+(b)。一般情况下,我们采用运算符的习惯表达方式。
示例2:
#include <QCoreApplication>
#include <QDebug>
class Clock {
public:
explicit Clock();
Clock(int NewH=0,int NewM=0,int NewS=0);
QString ShowTime() const;
Clock& operator ++(); //前置单目运算符重载
Clock operator ++(int); //后置单目运算符重载
private:
int Hour, Minute, Second;
};
Clock::Clock() : Hour(0), Minute(0), Second(0) {
}
Clock::Clock(int NewH, int NewM, int NewS) {
Hour = qMax(0, qMin(NewH, 23));
Minute = qMax(0, qMin(NewM, 59));
Second = qMax(0, qMin(NewS, 59));
}
QString Clock::ShowTime() const {
return QString::number(Hour) + ":"
+ QString::number(Minute) + ":"
+ QString::number(Second);
}
Clock& Clock::operator ++() {
if (++Second>=60) {
Second = Second - 60;
if (++Minute >= 60) {
Minute = Minute - 60;
Hour = (++Hour) % 24;
}
}
return *this;
}
Clock Clock::operator ++(int) {
Clock old = *this;
++(*this);
return old;
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Clock myClock(23,59,59);
qDebug() << "myClock is: " << myClock.ShowTime();
qDebug() << "myClock++:" << (myClock++).ShowTime();
qDebug() << "myClock is: " << myClock.ShowTime();
qDebug() << "++myClock:" << (++myClock).ShowTime();
qDebug() << "++myClock:" << (++myClock).ShowTime();
return a.exec();
}
运行结果:
友元函数运算符###
运算符重载为类的友元函数的一般格式为:
friend <函数类型> operator <运算符> ( <参数表> ) {
<函数体>
}
当运算符重载为类的友元函数时,由于没有隐含的this指针,因此操作数的个数没有变化,所有的操作数都必须通过函数的形参进行传递,函数的参数与操作数自左至右一一对应。
调用友元函数运算符的格式如下:
operator <运算符>(<参数1>,<参数2>)
它等价于
<参数1><运算符><参数2>
例如:a+b等价于operator+(a,b)。
友元示例:
#include <QCoreApplication>
#include <QDebug>
class complex {
public:
complex(double r=0.0,double i=0.0) : real(r), imag(i) {
}
friend complex operator + (complex c1, complex c2);
friend complex operator - (complex c1, complex c2);
QString display() const;
private:
double real;
double imag;
};
complex operator + (complex c1,complex c2) {
return complex(c1.real + c2.real, c1.imag + c2.imag);
}
complex operator - (complex c1,complex c2) {
return complex(c1.real - c2.real, c1.imag - c2.imag);
}
QString complex::display() const {
return "(" + QString::number(real) + ","
+ QString::number(imag) +")";
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
complex c1(5,4), c2(2,10), c3;
qDebug() << "c1 =" << c1.display();
qDebug() << "c2 =" << c2.display();
c3 = c1 - c2;
qDebug() << "c3 = c1-c2 = " << c3.display();
c3 = c1 + c2;
qDebug() << "c3 = c1+c2 = " << c3.display();
return a.exec();
}
运行结果:
两种重载形式的比较
在多数情况下,将运算符重载为类的成员函数和类的友元函数都是可以的。但成员函数运算符与友元函数运算符也具有各自的一些特点:
(1) 一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
(2) 以下一些双目运算符不能重载为类的友元函数:=、()、[]、->。
(3) 类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。
(4) 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
(5) 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数。
(6) 当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一 个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部 类型的对象,该运算符函数必须作为一个友元函数来实现。
(7) 当需要重载运算符具有可交换性时,选择重载为友元函数。