多态性学习(上)
什么是多态?
多态是指同样的消息被不同类型的对象接收时导致不同的行为。所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。虽然这看上去好像很高级的样子,事实上我们普通的程序设计中经常用到多态的思想。最简单的例子就是运算符,使用同样的加号“+”,就可以实现整型数之间、浮点数之间、双精度浮点数之间的加法,以及这几种数据类型混合的加法运算。同样的消息--加法,被不同类型的对象—不同数据类型的变量接收后,采用不同的方法进行相加运算。这些就是多态现象。
多态的类型
面向对象的多态性可以分为4类:重载多态、强制多态、包含多态和参数多态。我们对于C++了解的函数的重载就是属于重载多态,上文讲到的运算符重载也是属于重载多态的范畴。包含多态是类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现的。这一次的总结中主要讲解重载多态和包含多态,剩下的两种多态我将在下文继续讲解。
运算符重载
运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时导致不同的行为。运算符重载的实质就是函数重载。C++中预定义的运算符的操作对象只能是基本的数据类型,那么我们有时候需要对自定义的数据类型(比如类)也有类似的数据运算操作。所以,我们的运算符重载的这一多态形式就衍生出来了。
相信看到这里,应该有很多像我这样的大学生并不陌生了吧,在我们钟爱的ACM/ICPC中是不是经常遇到过的啊?没错,特别是在计算几何中我们定义完一个向量结构体之后,需要对“+”“-”实行运算符重载,这样我们就可以直接对向量进行加减乘除了。
运算符重载的规则
- C++中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。C++中类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”和三元运算符“?:”是不能重载的。
- 重载之后运算符的优先级和结合性都不会改变。
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。
运算符重载的实现
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<algorithm> 7 #define inf 0x7fffffff 8 using namespace std; 9 10 class Complex { 11 public: 12 Complex (double r=0.0 , double i=0.0):real(r),imag(i){} 13 Complex operator + (const Complex &c2) const; 14 Complex operator - (const Complex &c2) const; 15 void display() const; 16 private: 17 double real; 18 double imag; 19 }; 20 21 Complex Complex::operator + (const Complex &c2) const { 22 return Complex(real+c2.real , imag+c2.imag); 23 } 24 Complex Complex::operator - (const Complex &c2) const { 25 return Complex(real-c2.real , imag-c2.imag); 26 } 27 void Complex::display() const { 28 cout<<"("<<real<<", "<<imag<<")"<<endl; 29 } 30 31 int main() 32 { 33 Complex c1(5,4),c2(2,10),c3; 34 cout<<"c1= "; 35 c1.display(); 36 cout<<"c2= "; 37 c2.display(); 38 c3=c1+c2; 39 cout<<"c3=c1+c2 :"; 40 c3.display(); 41 c3=c1-c2; 42 cout<<"c3=c1-c2 :"; 43 c3.display(); 44 return 0; 45 }
在本例中,将复数的加减法这样的运算重载为复数类的成员函数,可以看出,除了在函数声明及实现的时候使用了关键字operator之外,运算符重载成员函数与类的普通成员函数没有什么区别。在使用的时候,可以直接通过运算符、操作数的方式来完成函数调用。这时,运算符“+”、“-”原有的功能都不改变,对整型数、浮点数等基本类型数据的运算仍然遵循C++预定义的规则,同时添加了新的针对复数运算的功能。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<algorithm> 7 #define inf 0x7fffffff 8 using namespace std; 9 10 class Clock { 11 public: 12 Clock(int hour=0,int minute=0,int second=0); 13 void showTime() const; 14 Clock& operator ++ (); 15 Clock operator ++ (int); 16 private: 17 int hour,minute,second; 18 }; 19 20 Clock::Clock(int hour,int minute,int second) { 21 if (hour>=0&&hour<24 && minute>=0&&minute<60 && second>=0&&second<60) { 22 this->hour = hour; 23 this->minute = minute; 24 this->second = second; 25 } 26 else { 27 cout<<"Time error!"<<endl; 28 } 29 } 30 void Clock::showTime() const { 31 cout<<hour<<":"<<minute<<":"<<second<<endl; 32 } 33 Clock & Clock::operator ++ () { 34 second ++ ; 35 if (second >= 60) { 36 second -= 60; 37 minute ++ ; 38 if (minute >= 60) { 39 minute -= 60; 40 hour = (hour+1)%24; 41 } 42 } 43 return *this; 44 } 45 Clock Clock::operator ++ (int) { 46 Clock old= *this; 47 ++(*this); 48 return old; 49 } 50 51 int main() 52 { 53 Clock myClock(23,59,59); 54 cout<<"First time output: "; 55 myClock.showTime(); 56 cout<<"show myClock++: "; 57 (myClock++).showTime(); 58 cout<<"show ++myClock: "; 59 (++myClock).showTime(); 60 return 0; 61 }
这个例子中,我们把时间自增前置++和后置++运算重载为时钟类的成员函数,前置单目运算符和后置单目运算符的重载最主要的区别就在于重载函数的形参。
语法规定:前置单目运算符重载为成员函数时没有形参,后置单目运算符重载为成员函数时需要有一个int型形参。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<algorithm> 7 #define inf 0x7fffffff 8 using namespace std; 9 10 class Complex { 11 public: 12 Complex (double r=0.0,double i=0.0):real(r),imag(i){} 13 friend Complex operator + (const Complex &c1,const Complex &c2); 14 friend Complex operator - (const Complex &c1,const Complex &c2); 15 friend ostream & operator << (ostream &out,const Complex &c); 16 private: 17 double real; 18 double imag; 19 }; 20 21 Complex operator + (const Complex &c1,const Complex &c2) { 22 return Complex(c1.real+c2.real , c1.imag+c2.imag); 23 } 24 Complex operator - (const Complex &c1,const Complex &c2) { 25 return Complex(c1.real-c2.real , c1.imag-c2.imag); 26 } 27 ostream & operator << (ostream &out,const Complex &c) { 28 cout<<"("<<c.real<<", "<<c.imag<<")"<<endl; 29 return out; 30 } 31 32 int main() 33 { 34 Complex c1(5,4),c2(2,10),c3; 35 cout<<"c1= "<<c1<<endl; 36 cout<<"c2= "<<c2<<endl; 37 c3=c1+c2; 38 cout<<"c3=c1+c2 :"<<c3<<endl; 39 c3=c1-c2; 40 cout<<"c3=c1-c2 :"<<c3<<endl; 41 return 0; 42 }
这一次我们将运算符重载为类的非成员函数,就必须把操作数全部通过形参的方式传递给运算符重载函数,“<<”操作符的左操作数为ostream类型的引用,ostream是cout类型的一个基类,右操作数是Complex类型的引用,这样在执行cout<<c1时,就会调用operator<<(cout,c1)。
包含多态
刚才就有说到,虚函数是包含多态的主要内容。那么,我们就来看看什么是虚函数。
虚函数是动态绑定的基础。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。
根据赋值兼容规则,可以使用派生类的对象来代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,但是我们访问到的只是从基类继承来的同名成员。解决这一问题的方法是:如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同的行为,从而实现运行过程的多态。
上面这一段文字初次读来有点生拗,希望读者多读两遍,因为这是很重要也是很核心的思想。接下来,我们看看两段代码,体会一下基类中虚函数的作用。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<algorithm> 7 #define inf 0x7fffffff 8 using namespace std; 9 10 class A { 11 public: 12 A() {} 13 virtual void foo() { 14 cout<<"This is A."<<endl; 15 } 16 }; 17 class B:public A { 18 public: 19 B(){} 20 void foo() { 21 cout<<"This is B."<<endl; 22 } 23 }; 24 25 int main() 26 { 27 A *a=new B(); 28 a->foo(); 29 if (a != NULL) delete a; 30 return 0; 31 }
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<algorithm> 7 #define inf 0x7fffffff 8 using namespace std; 9 10 class Base1 { 11 public: 12 virtual void display() const; 13 }; 14 void Base1::display() const { 15 cout<<"Base1::display()"<<endl; 16 } 17 18 class Base2:public Base1 { 19 public: 20 void display() const; 21 }; 22 void Base2::display() const { 23 cout<<"Base2::display()"<<endl; 24 } 25 26 class Derived:public Base2 { 27 public: 28 void display() const; 29 }; 30 void Derived::display() const { 31 cout<<"Derived::display()"<<endl; 32 } 33 34 void fun(Base1 *ptr) { 35 ptr->display(); 36 } 37 38 int main() 39 { 40 Base1 base1; 41 Base2 base2; 42 Derived derived; 43 fun(&base1); 44 fun(&base2); 45 fun(&derived); 46 return 0; 47 }
在后面的一段程序中,派生类并没有显式的给出虚函数的声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是否是虚函数:
- 该函数是否与基类的虚函数有相同的名称
- 该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
- 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值。
虚析构函数
在C++中,不能声明虚构造函数,但是可以声明虚析构函数。如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数。在析构函数设置为虚函数之后,在使用指针引用时可以动态绑定,实现运行时的多态,保证使用基类类型的指针就能够调用适当的析构函数针对不用的对象进行清理工作。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cstdlib> 5 #include<cmath> 6 #include<algorithm> 7 #define inf 0x7fffffff 8 using namespace std; 9 10 class Base { 11 public: 12 ~Base(); 13 }; 14 Base::~Base() { 15 cout<<"Base destructor"<<endl; 16 } 17 18 class Derived:public Base { 19 public: 20 Derived(); 21 ~Derived(); 22 private: 23 int *p; 24 }; 25 Derived::Derived() { 26 p=new int(0); 27 } 28 Derived::~Derived() { 29 cout<<"Derived destructor"<<endl; 30 delete p; 31 } 32 33 void fun(Base *b) { 34 delete b; 35 } 36 37 int main() 38 { 39 Base *b=new Derived(); 40 fun(b); 41 return 0; 42 }
这说明,通过基类指针删除派生类对象时调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有得到释放,造成了内存泄露。
避免上述错误的有效方法就是将析构函数声明为虚函数:
1 class Base { 2 public: 3 virtual ~Base(); 4 };
此时,我们再次运行这一份代码,得到的结果就如下图所示。
这说明派生类的析构函数被调用了,派生类对象中动态申请的内存空间被正确地释放了。这是由于使用了虚析构函数,实现了多态。