多态性
- 多态的概念和类型
- 多态的类型
- 多态的实现
- 运算符重载
- 运算符重载的概念和规则
- 概念
- 规则
- 运算符重载为类的成员函数
- 双目运算符
- 单目运算符
- 运算符重载为类的友元函数
- 双目运算符重载
- 单目运算符重载
多态的概念和类型
消息:消息在C++编程中指的是对类的成员函数的调用。
多态就是指相同的消息被不同类型的对象接收会引起不同的操作,直接点讲,就是在不同的情况下调用同名函数时,可能实际调用的并不是同一个函数。
多态的类型
多态性有四种类型:
- 重载多态(专用多态)。普通函数的重载和类的成员函数的重载,它们都属于重载多态。
- 强制多态(专用多态)。整型变量和浮点型变量相加时,需要先把整型变量强制转换为浮点型再进行加法运算,这就是强制多态。从概念上讲,强制多态就是将一个变量的类型进行转换,以满足一个函数运算的要求。
- 参数多态(通用多态)。类模板将类型参数化,设定了确定的类型才可以实例化。由类模板实例化得到的所有类都有相同的操作,但是被操作对象的类型不同,这就是参数多态
- 包含多态(通用多态)。包含多态是指类族中不同类的同名成员函数实现的操作不相同。包含多态一般通过虚函数来实现。
多态的实现
从多态实现的阶段不同来分类可以分为:
- 编译时的多态
- 运行时的多态
编译时的多态是指在编译的过程中就确定了具体调用同名函数中的哪个函数,而运行时的多态则是在程序运行过程中才动态的确定调用的具体函数。这种确定调用同名函数的哪个函数的过程就叫做联编或者绑定。
绑定实际上就是确定某个标识符对应的存储地址的过程。按照绑定发生的阶段的不同可以分为:静态绑定和动态绑定。静态绑定就对应着编译时的多态,动态绑定对应运行时的多态。
- 如果绑定过程发生在编译链接阶段,则称为静态绑定。在编译链接过程中,编译器根据类型匹配等特征确定某个同名标识究竟调用哪一段程序代码,也就是确定通过某个同名函数到底调用哪个函数体。1中的四种多态中有三种需要静态绑定:重载多态、强制多态和参数多态。
- 而如果绑定过程发生在程序运行阶段,则成为动态绑定。在编译链接过程中无法确定调用的具体函数,就要等到程序运行时动态确定。包含多态就需要使用动态绑定实现。
运算符重载
运算符重载的概念和规则
概念
运算符重载就是为预定义的一些运算符增加新的意义,使其因操作数类型的不同而产生不同的操作。
运算符重载实际上属于函数重载,因为在运算符重载中,不是运算符表达式而是调用运算符函数,操作数变成了运算符函数的参数,运算符函数的参数不同时调用不同的函数。这些与函数重载如出一辙。
为什么我们需要运算符重载?
因为自定义数据类型有时也需要使用运算符进行某些运算,比如加法运算,但是预定义的运算符的操作数只能是基本数据类型,所以自定义数据类型的运算需要进行运算符重载。
class Date
{
public:
Date(int nYear, int nMonth, int nDay) { m_nYear=nYear; m_nMonth=nMonth; m_nDay=nDay; } // 构造函数
void show(); // 显示日期
private:
int m_nYear;
int m_nMonth;
int m_nDay;
};
- 假设我们声明了两个Date类的对象:Date date1(2011, 11, 1), date2(2012, 1, 6);。然后需要计算date1和date2所表示日期差多少天,也就是进行减法运算,最简单的就是用运算符“-”,但是如果直接写date2-date1,编译器会报错,因为编译器不知道怎样进行此减法运算。这就需要我们自己写程序来说明在对Date类对象进行“-”运算时,具体做哪些处理,也就是需要进行运算符重载。
规则
运算符重载的使用有如下规则:
- 运算符重载是为了让自定义数据类型能够使用预定义运算符,对预定义运算符进行重定义,但一般重定义的功能与原运算符的功能相似,运算符重载的参数个数与原运算符的操作数个数相同,而且至少有一个参数属于自定义数据类型。
- 运算符重载后其优先级和结合性都与原运算符相同。
- 除了类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”、sizeof运算符和条件运算符“?:”这五种运算符外,其余C++运算符都能重载,而且只有C++中已有的运算符可以重载。
运算符重载为类的成员函数时的声明形式为:
函数类型 operator 运算符(参数表)
{
函数体;
}
- 函数类型是运算符重载的返回值类型。operator是声明和定义运算符重载时的关键字。运算符就是需要重载的运算符,比如“+”或“-”,但不能是“.”、“.*”、“::”、sizeof或“?:”。参数表列出重载运算符的参数及类型,这里当重载运算符不是后置“++”或“–”时,参数的个数比原运算符的操作数个数少一个,因为类的对象调用运算符重载成员函数时,自己的数据可以直接访问,不需要在参数表中传递,所以参数表中就不必列出该对象本身了。
运算符重载为类的友元函数时的声明形式为:
friend 函数类型 operator 运算符(参数表)
{
函数体;
}
- 与上面运算符重载为类的成员函数时不同的是,在函数类型前需要加关键字friend。另外,运算符重载友元函数访问类的对象的数据时,必须通过类的对象名访问,所以此友元函数的所有参数都需要进行传递,参数个数与原运算符的操作数个数相同。
在软件开发中用了运算符重载后,会体会到复杂类型数据也能进行加减运算的方便的。这让我们的程序书写更简单,可读性更高,更易维护,最终提高软件开发效率。
运算符重载为类的成员函数
运算符重载为类的成员函数后就可以像其他成员函数一样访问本类的数据成员了。在类的外部通过类的对象,可以像原运算符的使用方式那样使用重载的运算符,比如,“+”运算符被重载为类A的成员函数后,A的对象a和其他对象b就可以这样进行加法运算:a+b。
重载的运算符可能是双目运算符也可能是单目运算符。
双目运算符
如果是双目运算符,比如“+”和“-”,则一个操作数是使用此运算符的对象本身,另一个操作数使用运算符重载函数传递进来的对象。假设有双目运算符U,a为类A的对象,另有某类也可以是A类的对象b,我们想实现a U b这样的运算,就可以把U重载为类A的成员函数,此函数只有一个形参,形参的类型为对象b的类型。这样进行a U b的运算就相当于函数调用:a.operator U(b)。
#include <iostream>
using namespace std;
class CTimeSpan
{
public:
CTimeSpan(int nHours = 0, int nMins = 0); // 构造函数
CTimeSpan operator +(CTimeSpan ts); // 运算符“+”重载为成员函数
int GetHours() { return m_nHours; } // 获取小时数
int GetMins() { return m_nMins; } // 获取分钟数
void Show(); // 显示时间值
private:
int m_nHours; // 小时数
int m_nMins; // 分钟数
};
CTimeSpan::CTimeSpan(int nHours, int nMins) // 构造函数的实现
{
nHours += nMins / 60;
nMins %= 60;
m_nHours = nHours;
m_nMins = nMins;
}
CTimeSpan CTimeSpan::operator +(CTimeSpan ts) // 重载运算符函数实现
{
int nNewHours;
int nNewMins;
nNewHours = m_nHours + ts.GetHours();
nNewMins = m_nMins + ts.GetMins();
nNewHours += nNewMins / 60;
nNewMins %= 60;
return CTimeSpan(nNewHours, nNewMins);
}
void CTimeSpan::Show()
{
cout << m_nHours << "小时" << m_nMins << "分钟" << endl;
}
int main()
{
CTimeSpan timeSpan1(2, 50);
CTimeSpan timeSpan2(3, 30);
CTimeSpan timeSum;
timeSum = timeSpan1 + timeSpan2;
cout << "timeSpan1: ";
timeSpan1.Show();
cout << "timeSpan2: ";
timeSpan2.Show();
timeSum = timeSpan1 + timeSpan2;
cout << "timeSum=timeSpan1+timeSpan2: ";
timeSum.Show();
return 0;
}
- 运算符重载成员函数跟一般的成员函数类似,只是使用了关键字operator。使用重载运算符的方式与原运算符相同。运算符作用于整型、浮点型和CTimeSpan等不同的对象会发生不同的操作行为,这就是多态性。
单目运算符
如果是单目运算符,比如“++”和“–”,操作数就是此对象本身,重载函数不需要传递参数,只是后置单目运算符语法上规定有一个形式上的参数,以区别于前置单目运算符。
- 假设有前置单目运算符U,如前置“++”,a为类A的对象,我们想实现U a这样的运算,也可以把U重载为类A的成员函数,此函数没有形参。这样U a表达式就相当于函数调用:a.operator U()。
- 假设有后置单目运算符U,如后置“–”,a为类A的对象,我们想实现a U这样的运算,同样可以把U重载为类A的成员函数,但此函数需要有一个整型的形参。重载后a U表达式就相当于函数调用:a.operator U(0)。
前置单目运算符重载和后置单目运算符重载在语法形式上的区别就是前者重载函数没有形参,而后者重载函数有一个整型形参,此形参对函数体没有任何影响,这只是语法上的规定,仅仅是为了区分前置和后置。
#include <iostream>
using namespace std;
class Clock //时钟类声明
{
public: //外部接口
Clock(int NewH = 0, int NewM = 0, int NewS = 0);
void ShowTime();
Clock& operator ++(); //前置单目运算符重载
Clock operator ++(int); //后置单目运算符重载
private: //私有数据成员
int Hour, Minute, Second;
};
Clock::Clock(int NewH, int NewM, int NewS)
{
if (0 <= NewH && NewH < 24 && 0 <= NewM && NewM < 60 && 0 <= NewS && NewS < 60)
{
Hour = NewH;
Minute = NewM;
Second = NewS;
}
else
cout << "错误的时间!" << endl;
}
void Clock::ShowTime()
{
cout << Hour << ":" << Minute << ":" << Second << endl;
}
Clock& Clock::operator ++() //前置单目运算符重载函数
{
Second++;
if (Second >= 60)
{
Second = Second - 60;
Minute++;
if (Minute >= 60)
{
Minute = Minute - 60;
Hour++;
Hour = Hour % 24;
}
}
return *this;
}
//后置单目运算符重载
Clock Clock::operator ++(int) //注意形参表中的整型参数
{
Clock old = *this; //此处返回的为old的指针,将没有参与前置运算的指针返回
++(*this);
return old;
}
int main()
{
Clock myClock(23, 59, 59);
cout << "初始时间myClock:";
myClock.ShowTime();
cout << "myClock++:";
(myClock++).ShowTime();
myClock.ShowTime();
cout << "++myClock:";
(++myClock).ShowTime();
return 0;
}
- 此处区分前置运算和后置运算的区别:前置在显示前处理,后置在显示后处理(返回操作之前的指针即可)。
- 因为后置单目运算符重载函数中的整型形参没有实际意义,只是为了区分前置和后置,所以参数表中只给出类型就行了,参数名写不写都可以。
运算符重载为类的友元函数
友元函数通过类的对象可以访问类的公有、保护和私有成员,也就是类的所有成员友元函数都能访问到。所以运算符重载为类的友元函数以后也可以访问类的所有成员。
与运算符重载为成员函数时不同的是,重载的友元函数不属于任何类,运算符的操作数都需要通过函数的形参表传递。操作数在形参表中从左到右出现的顺序就是用运算符写表达式时操作数的顺序。
双目运算符重载
如果有双目运算符U,它的其中一个操作数是类A的对象a,那么运算符U就可以重载为类A的友元函数,此友元函数的两个参数中,一个是类A的对象,另一个是其他对象,也可以是类A的对象。这样双目运算符重载为类的友元函数后,假设运算符的两一个操作数是对象b,则表达式a U b就相当于调用函数operator U(a, b)。
#include <iostream>
using namespace std;
class CTimeSpan
{
public:
CTimeSpan(int nHours = 0, int nMins = 0); // 构造函数
friend CTimeSpan operator +(CTimeSpan ts1, CTimeSpan ts2); // 运算符“+”重载为成员函数
int GetHours() { return m_nHours; } // 获取小时数
int GetMins() { return m_nMins; } // 获取分钟数
void Show(); // 显示时间值
private:
int m_nHours; // 小时数
int m_nMins; // 分钟数
};
CTimeSpan::CTimeSpan(int nHours, int nMins) // 构造函数的实现
{
nHours += nMins / 60;
nMins %= 60;
m_nHours = nHours;
m_nMins = nMins;
}
void CTimeSpan::Show()
{
cout << m_nHours << "小时" << m_nMins << "分钟" << endl;
}
CTimeSpan operator +(CTimeSpan ts1, CTimeSpan ts2) // 重载运算符函数实现
{
int nNewHours;
int nNewMins;
nNewHours = ts1.m_nHours + ts2.m_nHours;
nNewMins = ts1.m_nMins + ts2.m_nMins;
nNewHours += nNewMins / 60;
nNewMins %= 60;
return CTimeSpan(nNewHours, nNewMins);
}
int main()
{
CTimeSpan timeSpan1(2, 50);
CTimeSpan timeSpan2(3, 30);
CTimeSpan timeSum;
timeSum = timeSpan1 + timeSpan2;
cout << "timeSpan1: ";
timeSpan1.Show();
cout << "timeSpan2: ";
timeSpan2.Show();
timeSum = timeSpan1 + timeSpan2;
cout << "timeSum=timeSpan1+timeSpan2: ";
timeSum.Show();
return 0;
}
- 加法运算符重载为CTimeSpan类的友元函数而不是成员函数,我们看到运算符重载函数有两个形参ts1和ts2,通过这两个参数将需要进行运算的操作数传递进去,而在此函数中也能够访问类CTimeSpan的私有成员m_nHours和m_nMins。
- 此处为+的运算符重载,还有其他运算符的重载。不同的运算符的操作方式不同。
单目运算符重载
如果有前置单目运算符U,比如前置“–”,a为类A的对象,我们想实现U a这样的运算,就可以把U重载为类A的友元函数,此友元函数只有一个形参,为类A的对象,重载后表达式U a相当于调用函数operator U(a)。如果是后置单目运算符U,如后置“++”,a还是类A的对象,那么要实现a U这样的运算,也可以把U重载为类A的友元函数,此时友元函数就需要有两个形参,一个是类A的对象,另一个是整型形参,此整型形参没有实际意义,与上一节后置单目运算符重载为成员函数时的整型形参一样,只是为了区分前置运算符和后置运算符的重载。重载后表达式a U就相当于调用函数operator U(a, 0)。