目录标题
1 多态性
多态是指同样的消息被不同类型的对象接收时导致不同的行为,所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现。
1-1 多态的类型
面向对象的多态性可以分为4种,重载多态、强制多态、包含多态和参数多态。前面两种统称为专用多态,而后面两种称为通用多态。
- 重载多态:普通函数及类的成员函数的重载,运算符重载。
- 强制多态:指将一个变量的类型加以变化,以符合一个函数或者操作的要求。
- 包含多态:类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现。
- 参数多态:将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象。
本节笔记讲重载多态的运算符重载和包含多态的虚函数,强制多态就是变量类型强制转换或隐式转换,不讲,而参数多态是跟模板有关,在下一节笔记中。
1-2 多态的实现
多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。
前者是在编译的过程中确定了同名操作的具体操作对象,而后者是在程序运行过程中才动态地确定操作所针对的具体对象。
这种确定操作的具体对象的过程就是绑定。绑定是指计算机程序自身彼此关联的过程,就是把一条消息和一个对象的方法相结合的过程。按照绑定进行的阶段不同,可以分为两种不同的绑定方法:静态绑定和动态绑定,分别对应着多态的两种实现方式。
绑定工作在编译连接阶段完成的情况称为静态绑定,比如重载、强制和参数多态。
绑定工作在程序运行阶段完成的情况称为动态绑定,比如包含多态。
2 运算符重载(重载多态)
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。
2-1 运算符重载的规则
- 运算符重载的规则如下:
- C++语言中的运算符除了少数几个外,全部可以重载,而且只能重载C++中已经存在的运算符。
- 重载之后运算符的优先级和结合性都不会改变。
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来说,重载的功能应当与原有功能相类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。
- 不能重载的运算符:类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”和三目运算符“?:”。原因如下:
- 前面两个保证了C++语言中访问成员功能的含义不被改变。
- 作用域分辨符的操作数是类型,而不是普通的表达式,也不具备重载的特征。
- 运算符的重载形式有两种,重载为类的非静态成员函数和重载为非成员函数。
返回类型指定了重载运算符的返回值类型,也就是运算结果类型。
operator是定义运算符重载函数的关键字。
运算符即是要重载的运算符名称,必须是C++中可重载的运算符。
形参表中给出重载运算符所需要的参数和类型。
运算符重载为类的成员函数的一般语法形式为:
返回类型 类名::operator 运算符(形参表)
{
函数体
}
运算符重载为类的非静态成员函数的一般语法形式为:
返回类型 operator 运算符(形参表)
{
函数体
}
- 两种重载形式的区别:当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个(后置“++”“–”除外),因为第一个操作数会被作为函数调用的目的对象,无须出现在参数表中。当运算符重载为非成员函数时,函数的参数个数与原操作个数相同,运算符的所有操作数必须显式通过参数传递。
2-2 运算符重载为成员函数
运算符重载可以分为双目运算符重载和前后置单目运算符重载。
- 如果是双目运算符,左操作数是对象本身的数据,由this指针指出,右操作数则需要通过运算符重载函数的参数表来传递。
对于双目运算符A,如果要重载为类的成员函数,使之能够实现表达式oprd1 A oprd2
,其中oprd1为B类的对象,则应当把A重载为B类的成员函数,该函数只有一个形参,形参的类型是oprd2所属的类型。经过重载后表达式oprd1 A oprd2
相当于oprd1.operator A(oprd2)
- 如果是单目运算符,操作数由对象的this指针给出,就不再需要任何参数。对于前置单目运算*U,如“-”(负号)等,如果要重载为类的成员函数,用来实现表达式
U oprd
,其中oprd为A类的对象,则U应当重载为A类的成员函数,函数没有形参。经过重载后表达式U oprd
相当于oprd.operator U()
; - 对于后置运算符“++”和“–”,如果要将它们重载为类的成员函数,用来实现表达式
oprd++
和oprd--
,其中oprd为A类的对象,那么运算符就应当重载为A类的成员函数,这时函数要带有一个整型(int)形参。
双目运算符举例:
class Complex{
public:
Complex(double r=0.0,double i=0.0):real(r),imag(i){}
Complex operator+(const Complex &c2) const;
Complex operator-(const Complex &c2) const;
void display() const;
private:
double real;
double imag;
};
Complex Complex::operator+(const Complex &c2) const{
return Complex(real+c2.real,imag+c2.imag);
}
Complex Complex::operator-(const Complex &c2) const{
return Complex(real-c2.real,imag-c2.imag);
}
void Complex::display() const{
cout<<"("<<real<<","<<imag<<")"<<endl;
}
int main(){
Complex c1(5,4),c2(2,10);
(c1+c2).display();
(c1-c2).display();
return 0;
}
单目运算符举例:
class Clock{
public:
Clock(int hour=0,int minute=0,int second=0);
void showTime() const;
Clock& operator++();
Clock operator++(int);
private:
int hour,minute,second;
}
Clock::Clock(int hour,int minute,int second){
if(0<=hour&&hour<24&&0<=minute&&minute<60&&0<=second&&second<60){
this->hour=hour;
this->minute=minute;
this->second=second;
}else
cout<<"Time error!"<<endl;
}
void Clock::showTime() const{
cout<<hour<<":"<<minute<<":"<<second<<endl;
}
Clock &Clock::operator++(){
second++;
if(second>=60){
second-=60;
minute++;
if(minute>=60){
minute-=60;
hour=(hour+1)%24;
}
}
return *this;
}
Clock Clock::operator++(int){
Clock old=*this;
++(*this);
return old;
}
int main(){
Clock myClock(23,59,59);
myClock.showTime();
(myClock++).showTime();
(++myClock).showTime();
return 0;
}
2-3 运算符重载为非成员函数
- 对于双目运算符B,如果要实现
oprd1 B oprd2
,其中oprd1和oprd2中只要有一个具有自定义类型,就可以将B重载为非成员函数,函数的形参为oprd1和oprd2。经过重载后表达式oprd1 B oprd2
相当于operator B(oprd1,oprd2)
- 对于前置单目运算符U,如“-”(负号)等,如果要实现表达式
U oprd
,其中oprd具有自定义类型,就可以将U重载为非成员函数,函数的形参为oprd。经过重载后表达式& oprd
相当于operator U(oprd)
- 对于后置单目运算符++和–,如果要实现表达式
oprd++
和oprd--
,其中oprd具有自定义类型,就可以将运算符重载为非成员函数,这里函数的形参有两个,一个是oprd,另一个是int类型形参。经过重载后表达式& oprd
相当于operator U(oprd)
举例:
class Complex{
public:
Complex(double r=0.0,double i=0.0):real(r),imag(i){}
friend Complex operator+(const Complex &c1,const Complex &c2);
friend Complex operator-(const Complex &c1,const Complex &c2);
friend Complex &operator<<(ostream &out,const Complex &c);
private:
double real;
double imag;
};
Complex Complex::operator+(const Complex &c1,const Complex &c2){
return Complex(c1.real+c2.real,c1.imag+c2.imag);
}
Complex Complex::operator-(const Complex &c1,const Complex &c2){
return Complex(c1.real-c2.real,c1.imag-c2.imag);
}
ostream & operator<<(ostream &out, const Complex &c){
out<<"<"<<c.real<<","<<c.imag<<")";
return out;
}
int main(){
Complex c1(5,4),c2(2,10),c3;
c3=c1+c2;
cout<<c3;
c3=c1-c2;
cout<<c3;
return 0;
}
3 虚函数(包含多态)
根据赋值兼容规则,可以使用派生类的对象代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,问题是访问到的只是从基类继承来的同名成员。解决这一问题的办法是:如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现了运行过程的多态。
3-1 一般虚函数成员
一般虚函数成员的声明语法是:virtual 函数类型 函数名(形参表);
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
运行过程中的多态需要满足3个条件:
- 类之间满足赋值兼容规则
- 要声明虚函数
- 要由成员函数来调用或者是通过指针、引用来访问虚函数。如果使用对象名来访问虚函数,则绑定在编译过程中就可以进行,无须在运行过程中进行。
注意:虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而内联函数的处理是静态的,所以虚函数一般不能以内联函数处理。但将虚函数声明为内联函数也不会引起错误。
class Base1{
public:
virtual void display() const;
};
void Base1::display() const{
cout<<"Base1::display()"<<endl;
}
class Base2:public Base1{
public:
virtual void display() const;
};
void Base2::display() const{
cout<<"Base2::display()"<<endl;
}
class Derived:public Base2{
public:
virtual void display() const;
};
void Derived::display() const{
cout<<"Derived::display()"<<endl;
}
void fun(Base1 *ptr){
ptr->display();
}
int main(){
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);fun(&base2);fun(&derived);
return 0;
}
//运行结果为
Base1::display()
Base2::display()
Derived::display()
在本例子中,派生类并没有显式地给出虚函数声明,这里系统就会遵循以下规则来判断派生类的一个函数成员是不是虚函数:
- 该函数是否与基类的虚函数有相同的名称。
- 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型。
- 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。
如果派生类的函数满足了上述条件,就会自动确定为虚函数。这里,派生类的虚函数便覆盖了基类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有其他重载形式。
3-2 final和override说明符
派生类中如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同,这仍然是合法的,但是该函数与虚函数是相互独立的,派生类中的函数并没有覆盖掉基类的版本,就实际的编程习惯而言,这往往意味着错误的发生,因为我们本希望派生类能覆盖掉基类的虚函数,但是因为弄错了参数列表而没有达到预期目标。
想要调试并发现这样的错误往往非常困难,在C++11标准中可以使用override关键字来说明派生类中的虚函数,这么做的好处是通过标记使得编译器能够发现一些错误。如果使用override 标记了某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错:
class Base{
public:
virtual void f1(int) const;
virtual void f2();
void f3();
};
class Derived:public Base{
public:
void f1(int) const override;//正确,f1与基类的f1匹配
void f2(int) const override;//错误,基类中没有形如f2(int)的函数
void f3() override;//错误,f3不是虚函数
void f4() override;//错误,基类中没有名为f4的函数
};
相应还能把某个函数指定为final,意味着该函数不能被覆盖,任何试图覆盖该函数的操作都将引发错误:
class Derived2:public Base{
public:
void f1(int) const fianl;//不允许后续的其他类覆盖f1(int)
};
class Derived3:public Derived2{
public:
void f1(int) const;//错误,Derived2的f1已经声明为final
void f2();//正确,覆盖从Base间接继承来的f2
};
3-3 虚析构函数
在 C++中,不能声明虚构造函数,但是可以声明虚析构函数。析构函数没有类型,也没有参数,和普通成员函数相比,虚析构函数情况略为简单些。虚析构函数的声明语法为:virtual ~类名();
如果一个类的析构函数是虚函数,那么,由它派生而来的所有子类的析构函数也是虚函数。析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不同的对象进行清理工作。
简单来说,如果有可能通过基类指针调用对象的析构函数(通过 delete),就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。
class Base{
public:
virtual ~Base();
};
Base::~Basse(){
cout<<"Base destructor"<<endl;
}
class Derived:public Base{
public:
Derived();
~Derived();
private:
int *p;
};
Derived::Derived(){
p=new int(0);
}
Derived::~Derived(){
cout<<"Derived destructor"<<endl;
delete p;
}
void fun(Base *b){
delete b;
}
int main(){
Base *b=new Derived();
fun(b);
return 0;
}
//运行结果为:
Derived destructor
Base destructor
3-4 纯虚函数与抽象类
抽象类是一种特殊的类,它为一个类族提供统一的操作界面。抽象类是带有纯虚函数的类。
3-4-1 纯虚函数
纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:virtual 函数类型 函数名(参数表)=0;
实际上,它与一般虚函数成员的原型在书写格式上的不同就在于后面加了“=0”。声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。纯虚函数的函数体由派生类给出。
3-4-2 抽象类
带有纯虚函数的类是抽象类。抽象类的主要作用是通过它为一个类族建立一个公共的接口,使它们能够更有效地发挥多态特性。抽象类声明了一族派生类的共同接口,而接口的完整实现,即纯虚函数的函数体,要由派生类自己定义。
抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的实现,这时的派生类仍然是一个抽象类。
抽象类不能实例化,即不能定义一个抽象类的对象,但是,我们可以定义一个抽象类的指针和引用。通过指针和引用,就可以指向并访问派生类对象,进而访问派生类的成员,这种访问是具有多态特征的。