关闭

C++深入体验之旅十二:类(下)

标签: C++C++指针面向对象体验
1306人阅读 评论(0) 收藏 举报
分类:

12.友元类

在编写链表类的时候我们有着这样的困惑:链表类和链表结点类都是我们编写的,我们能保证链表类对链表结点类的操作都是安全的。但由于类的封装性,我们不得不编写一些成员函数,以便于链表类访问链表结点类的私有成员数据。好在链表结点类的成员数据并不是很多,否则岂不是需要一大堆成员函数来供别的类访问?对于这种情况,我们能否告诉链表结点类:“链表类是安全的,让它访问你的私有成员吧”?
在C++中,可以用友元来解决这种尴尬的问题。所谓友元,就是作为一个类的“朋友”,可以例外地访问它的私有成员数据或私有成员函数。

友元类

类似于链表类和链表结点类的问题,我们可以用友元类来解决。即链表类是链表结点类的“朋友”,可以直接访问链表结点类的私有成员数据或私有成员函数。显然,要做链表结点类的“朋友”,必须要得到链表结点类的认可。所以我们必须在链表结点类的声明中告诉电脑,链表类是它认可的“朋友”,可以访问它的私有成员。声明友元类的语句格式为:
friend class 类名;
下面我们来看一下,友元是如何让我们更方便地设计程序的:(程序16.2.1)

//node.h 
class Node//声明一个链表结点类 
{ 
   friend class Linklist;//在Node类中声明友元类Linklist 
   public: 
   Node(); 
   Node(Node &n); 
   Node(int i,char c='0'); 
   Node(int i,char c,Node *p,Node *n); 
   ~Node(); 
   static int allocation(); 
   private: 
   int idata; 
   char cdata; 
   Node *prior; 
   Node *next; 
   static int count; 
}; 

//node.cpp 
#include "node.h" 
#include <iostream> 
using namespace std; 
int Node::count=0; 
Node::Node() 
{ 
   cout <<"Node constructor is running..." <<endl; 
   count++; 
   idata=0; 
   cdata='0'; 
   prior=NULL; 
   next=NULL; 
} 
Node::Node(int i,char c) 
{ 
   cout <<"Node constructor is running..." <<endl; 
   count++; 
   idata=i; 
   cdata=c; 
   prior=NULL; 
   next=NULL; 
} 
Node::Node(int i,char c,Node *p,Node *n) 
{ 
   cout <<"Node constructor is running..." <<endl; 
   count++; 
   idata=i; 
   cdata=c; 
   prior=p; 
   next=n; 
} 
Node::Node(Node &n) 
{ 
   count++; 
   idata=n.idata; 
   cdata=n.cdata; 
   prior=n.prior; 
   next=n.next; 
} 
Node::~Node() 
{ 
   count--; 
   cout <<"Node destructor is running..." <<endl; 
} 
int Node::allocation() 
{ 
   return count; 
} 

//linklist.h 
#include "node.h" 
#include <iostream> 
using namespace std; 
class Linklist//定义一个链表类 
{ 
   public: 
   Linklist(int i,char c); 
   Linklist(Linklist &l); 
   ~Linklist(); 
   bool Locate(int i); 
   bool Locate(char c); 
   bool Insert(int i=0,char c='0'); 
   bool Delete(); 
   void Show(); 
   void Destroy(); 
   private: 
   Node head; 
   Node * pcurrent; 
}; 
Linklist::Linklist(int i,char c):head(i,c)//链表的构造函数 
{ 
   cout<<"Linklist constructor is running..."<<endl; 
   pcurrent=&head; 
} 
Linklist::Linklist(Linklist &l):head(l.head) 
{ 
   cout<<"Linklist Deep cloner running..." <<endl; 
   pcurrent=&head; 
   Node * ptemp1=l.head.next;//直接访问私有成员数据 
   while(ptemp1!=NULL) 
   { 
      Node * ptemp2=new Node(ptemp1->idata,ptemp1->cdata,pcurrent,NULL); 
      pcurrent->next=ptemp2; 
      pcurrent=pcurrent->next; 
      ptemp1=ptemp1->next; 
} 
} 
Linklist::~Linklist() 
{ 
   cout<<"Linklist destructor is running..."<<endl; 
   Destroy(); 
} 
bool Linklist::Locate(int i) 
{ 
   Node * ptemp=&head; 
   while(ptemp!=NULL) 
   { 
      if(ptemp->idata==i) 
      { 
         pcurrent=ptemp; 
         return true; 
      } 
      ptemp=ptemp->next; 
   } 
   return false; 
} 
bool Linklist::Locate(char c) 
{ 
   Node * ptemp=&head; 
   while(ptemp!=NULL) 
   { 
      if(ptemp->cdata==c) 
      { 
         pcurrent=ptemp; 
         return true; 
      } 
      ptemp=ptemp->next; 
   } 
   return false; 
} 
bool Linklist::Insert(int i,char c) 
{ 
   if(pcurrent!=NULL) 
   { 
      Node * temp=new Node(i,c,pcurrent,pcurrent->next); 
      if (pcurrent->next!=NULL) 
      { 
         pcurrent->next->prior=temp; 
      } 
      pcurrent->next=temp; 
      return true; 
   } 
   else 
   { 
      return false; 
   } 
} 
bool Linklist::Delete() 
{ 
   if(pcurrent!=NULL && pcurrent!=&head) 
   { 
      Node * temp=pcurrent; 
      if (temp->next!=NULL) 
      { 
         temp->next->prior=pcurrent->prior; 
      } 
      temp->prior->next=pcurrent->next; 
      pcurrent=temp->prior; 
      delete temp; 
      return true; 
   } 
   else 
   { 
      return false; 
   } 
} 
void Linklist::Show() 
{ 
   Node * ptemp=&head; 
   while (ptemp!=NULL) 
   { 
      cout <<ptemp->idata <<'\t' <<ptemp->cdata <<endl; 
      ptemp=ptemp->next; 
   } 
} 
void Linklist::Destroy() 
{ 
   Node * ptemp1=head.next; 
   while (ptemp1!=NULL) 
   { 
      Node * ptemp2=ptemp1->next; 
      delete ptemp1; 
      ptemp1=ptemp2; 
   } 
   head.next=NULL; 
} 

//main.cpp同程序16.1
运行结果:
请输入一个整数和一个字符: 
3 F 
Node constructor is running... 
Linklist constructor is running... 
Node constructor is running... 
Node constructor is running... 
After Insert 
3 F 
2 B 
1 C 
Node Allocation:3 
Node constructor is running... 
An independent node created 
Node Allocation:4 
Node destructor is running... 
Linklist destructor is running... 
Node destructor is running... 
Node destructor is running... 
Node destructor is running... 
可以看到,程序的运行结果和程序16.1的结果一样,但是链表结点类没有程序16.1中那么繁琐。并且在链表类中完全都是直接访问链表结点类的成员数据,大大减少了调用函数产生的开销,这样执行程序的效率也就得以提高了。

13.友元函数

私有成员数据除了可能被别的类访问之外,也可能被别的函数或别的类的部分成员函数访问。为了保证类的封装性,我们可以以函数作为单位,“对外开放”类的私有成员。与声明友元类类似,如果我们想用函数访问链表结点类的私有成员数据,则那些函数必须得到链表结点类的认可。声明友元函数的语句格式为:
    friend 返回值类型函数名(参数表);
如果该函数是某个类的成员函数,则语句格式为:
    friend 返回值类型类名::函数名(参数表);
需要注意的是,在声明友元成员函数时,可能会牵扯出一系列的类的声明顺序问题。当类的结构本身就比较复杂时,友元的使用可能会使得这个问题愈加突出。
下面我们就用友元函数来输出一个结点的信息:(程序16.2.2)

//node.h 
class Node 
{ 
   friend class Linklist; //在Node类中声明友元类Linklist 
   friend void ShowNode(Node &n);//声明友元函数ShowNode 
   public: 
   Node(); 
   Node(Node &n); 
   Node(int i,char c='0'); 
   Node(int i,char c,Node *p,Node *n); 
   ~Node(); 
   static int allocation(); 
   private: 
   int idata; 
   char cdata; 
   Node *prior; 
   Node *next; 
   static int count; 
}; 
//node.cpp 
//其余部分同程序16.2.1 
void ShowNode(Node &n) 
{ 
   cout <<n.idata <<'\t' <<n.cdata <<endl;//友元函数可以访问私有成员数据 
} 

//linklist.h同程序16.2.1 
//main.cpp 
#include <iostream> 
#include "Linklist.h" 
using namespace std; 
int main() 
{ 
   int tempi; 
   char tempc; 
   cout <<"请输入一个整数和一个字符:" <<endl; 
   cin >>tempi >>tempc; 
   Linklist a(tempi,tempc); 
   a.Locate(tempi); 
   a.Insert(1,'C'); 
   a.Insert(2,'B'); 
   cout <<"After Insert" <<endl; 
   a.Show(); 
   Node b(4,'F'); 
   cout <<"An independent node created" <<endl; 
   cout <<"Use friend function to show node" <<endl; 
   ShowNode(b);//用友元函数输出b结点的内容 
   return 0; 
} 
运行结果:
请输入一个整数和一个字符: 
3 F 
Node constructor is running... 
Linklist constructor is running... 
Node constructor is running... 
Node constructor is running... 
After Insert 
3 F 
2 B 
1 C 
Node constructor is running... 
An independent node created 
Use friend function to show node 
4 G 
Node destructor is running... 
Linklist destructor is running... 
Node destructor is running... 
Node destructor is running... 
Node destructor is running... 
我们看到函数ShowNode成功地访问了链表结点b的私有成员数据。所以当一个函数要访问一个或多个对象的私有成员时,我们可以用友元来解决这个问题。

友元的利与弊

我们使用了友元之后,发现在设计程序的时候方便了很多。原先的那些私有成员都能轻松地被访问了。于是我们不用去写那些繁琐的成员函数,程序执行的时候也减少了函数的调用次数,提高了运行效率。
一个“好朋友”带来的是效率和方便,而一个“坏朋友”却能带来更多的麻烦。友元的存在,破坏了类的封装性。一个类出现问题,就不仅仅是由这个类本身负责了,还可能和它众多的友元有关。这无疑使得检查调试的范围突然扩大了许多,难度也陡然增加。
所以,我们在使用友元的时候,权衡使用友元的利弊,使程序达到最佳的效果。

14.操作符重载

在表达式中,我们常会用到各种操作符(运算符),例如1+3和4*2。然而,这些操作符只能用于C++内置的一些基本数据类型。如果我们自己编写一个复数类,它也会有加减法的操作,那么它能否摆脱一串冗长的函数名,而享用加号呢?
在第六章我们学到过,函数是可以重载的,即同名函数针对不同数据类型的参数实现类似的功能。在C++中,操作符也是可以重载的,同一操作符对于不同的自定义数据类型可以进行不同的操作。

作为成员函数

在我们学习操作符重载前,我们先看看原先这个复数类是如何定义的:(程序16.3.1)

//complex.h 
#include <iostream> 
using namespace std; 
class Complex//声明一个复数类 
{ 
   public: 
   Complex(Complex &a);//拷贝构造函数 
   Complex(double r=0,double i=0); 
   void display();//输出复数的值 
   void set(Complex &a); 
   Complex plus(Complex a);//复数的加法 
   Complex minus(Complex a); //复数的减法 
   Complex plus(double r); //复数与实数相加 
   Complex minus(double r); //复数与实数相减 
   private: 
   double real;//复数实部 
   double img;//复数虚部 
}; 
Complex::Complex(Complex &a) 
{ 
   real=a.real; 
   img=a.img; 
} 
Complex::Complex(double r,double i) 
{ 
   real=r; 
   img=i; 
} 
void Complex::display() 
{ 
   cout <<real <<(img>=0?"+":"") <<img <<"i";//适合显示1-3i等虚部为负值的复数 
} 
void Complex::set(Complex &a) 
{ 
   real=a.real; 
   img=a.img; 
} 
Complex Complex::plus(Complex a) 
{ 
   Complex temp(a.real+real,a.img+img); 
   return temp; 
} 
Complex Complex::minus(Complex a) 
{ 
   Complex temp(real-a.real,img-a.img); 
   return temp; 
} 
Complex Complex::plus(double r) 
{ 
   Complex temp(real+r,img); 
   return temp; 
} 
Complex Complex::minus(double r) 
{ 
   Complex temp(real-r,img); 
   return temp; 
} 
//main.cpp 
#include "complex.h" 
#include <iostream> 
using namespace std; 
int main() 
{ 
   Complex a(3,2),b(5,4),temp; 
   temp.set(a.plus(b));//temp=a+b 
   temp.display(); 
   cout <<endl; 
   temp.set(a.minus(b));//temp=a-b 
   temp.display(); 
   cout <<endl; 
   return 0; 
} 
运行结果:
8+6i 
-2-2i 
虽然程序16.3.1已经实现了复数的加减法,但是其表达形式极为麻烦,如果有复数a、b、c和d,要计算a+b-(c+d)将会变得非常复杂。如果不是调用函数,而是使用操作符的话,就会直观得多了。
声明一个操作符重载的语句格式为:
返回值类型operator 操作符(参数表);
事实上,在声明和定义操作符重载的时候,我们可以将其看作函数了,只不过这个函数名是一些操作符。在声明和定义操作符重载时需要注意以下几点:

  1. 操作符只能是C++中存在的一些操作符,自己编造的操作符是不能参与操作符重载的。另外,“::”(域解析操作符)、“.”(成员操作符)、“……?……:……”(条件操作符)和sizeof等操作符不允许重载。
  2. 参数表中罗列的是操作符的各个操作数。重载后操作数的个数应该与原来相同。不过如果操作符作为成员函数,则调用者本身是一个操作数,故而参数表中会减少一个操作数。(请对比程序16.3.2与程序16.3.3)
  3. 各个操作数至少要有一个是自定义类型的数据,如结构或类。
  4. 尽量不要混乱操作符的含义。如果把加号用在减法上,会使程序的可读性大大下降。

下面我们把操作符作为成员函数,来实现复数的加减法:(程序16.3.2)

//complex.h 
#include <iostream> 
using namespace std; 
class Complex//声明一个复数类 
{ 
   public: 
   Complex(Complex &a); 
   Complex(double r=0,double i=0); 
   void display(); 
   void operator =(Complex a);//赋值操作 
   Complex operator +(Complex a);//加法操作 
   Complex operator -(Complex a);//减法操作 
   Complex operator +(double r);//加法操作 
   Complex operator -(double r);//减法操作 
   private: 
   double real; 
   double img; 
}; 
//未定义的函数与程序16.3.1相同 
void Complex::operator =(Complex a) 
{ 
   real=a.real; 
   img=a.img; 
} 
Complex Complex::operator +(Complex a) 
{ 
   Complex temp(a.real+real,a.img+img); 
   return temp; 
} 
Complex Complex::operator -(Complex a) 
{ 
   Complex temp(real-a.real,img-a.img); 
   return temp; 
} 
Complex Complex::operator +(double r) 
{ 
   Complex temp(real+r,img); 
   return temp; 
} 
Complex Complex::operator -(double r) 
{ 
   Complex temp(real-r,img); 
   return temp; 
} 

//main.cpp 
#include "complex.h" 
#include <iostream> 
using namespace std; 
int main() 
{ 
   Complex a(3,2),b(5,4),c(1,1),d(4,2),temp; 
   temp=a+b;//这样的复数加法看上去很直观 
   temp.display(); 
   cout <<endl; 
   temp=a-b; 
   temp.display(); 
   cout <<endl; 
   temp=a+b-(c+d);//可以和括号一起使用了 
   temp.display(); 
   cout <<endl; 
   return 0; 
} 
运行结果:
8+6i 
-2-2i 
3+3i 
以上程序的main.cpp中,复数的加法表达得非常简洁易懂,与程序16.3.1相比有了很大的进步。并且,我们发现使用了括号以后,可以更方便地描述各种复杂的运算。操作符在重载之后,结合性和优先级是不会发生变化的,符合用户本来的使用习惯。

作为友元

前面我们把操作符作为成员函数,实现了复数的加减法。如果我们把操作符作为普通的函数重载,则需要将其声明为友元。这时,参数表中的操作数个数应该与操作符原来要求的操作数个数相同。
下面我们来看一下,用友元和操作符重载来实现复数的加减法:(程序16.3.3)

//complex.h 
#include <iostream.h>//由于VC编译器存在问题,这里使用标准的写法居然无法通过编译 
class Complex 
{ 
   public: 
   Complex(Complex &a); 
   Complex(double r=0,double i=0); 
   void display(); 
   friend Complex operator +(Complex a,Complex b);//作为友元 
   friend Complex operator -(Complex a,Complex b); 
   friend Complex operator +(Complex a,double r); 
   friend Complex operator -(Complex a,double r); 
   private: 
   double real; 
   double img; 
}; 
//未定义的函数与程序16.3.1相同 
Complex operator +(Complex a,Complex b) 
{ 
   Complex temp(a.real+b.real,a.img+b.img); 
   return temp; 
} 
Complex operator -(Complex a,Complex b) 
{ 
   Complex temp(a.real-b.real,a.img-b.img); 
   return temp; 
} 
Complex operator +(Complex a,double r) 
{ 
   Complex temp(a.real+r,a.img); 
   return temp; 
} 
Complex operator -(Complex a,double r) 
{ 
   Complex temp(a.real-r,a.img); 
   return temp; 
} 
//main.cpp 
#include "complex.h" 
#include <iostream.h>//由于VC编译器存在问题,这里使用标准的写法无法通过编译 
int main() 
{ 
   Complex a(3,2),b(5,4),c(1,1),d(4,2),temp; 
   temp=a+b; 
   temp.display(); 
   cout <<endl; 
   temp=a-b; 
   temp.display(); 
   cout <<endl; 
   temp=a+b-(c+d); 
   temp.display(); 
   cout <<endl; 
   return 0; 
} 
运行结果:
8+6i 
-2-2i 
3+3i 
在上面这个程序中,加号和减号操作符由成员函数变成了友元函数。细心的读者可能注意到了,那个赋值操作符的定义跑哪儿去了?
事实上,赋值操作符有点类似于默认拷贝构造函数,也具有默认的对象赋值功能。所以即使没有对它进行重载,也能使用它对对象作赋值操作。但是如果要对赋值操作符进行重载,则必须将其作为一个成员函数,否则程序将无法通过编译。
在操作符重载中,友元的优势尽显无遗。特别是当操作数为几个不同类的对象时,友元不失为一种良好的解决办法。

又见加加和减减

在第五章我们学习了增减量操作符,并且知道它们有前后之分。那么增减量操作符是如何重载的呢?同样是一个操作数,它又是如何区分前增量和后增量的呢?
前增量操作符是“先增后赋”,在操作符重载中我们理解为先做自增,然后把操作数本身返回。后增量操作符是“先赋后增”,在这里我们理解为先把操作数的值返回,然后操作数自增。所以,前增量操作返回的是操作数本身,而后增量操作返回的只是一个临时的值。
在C++中,为了区分前增量操作符和后增量操作符的重载,规定后增量操作符多一个整型参数。这个参数仅仅是用于区分前增量和后增量操作符,不参与到实际运算中去。
下面我们就来看看,如何重载增量操作符:(程序16.3.4)

//complex.h 
#include <iostream.h>//由于VC编译器存在问题,这里使用标准的写法无法通过编译 
class Complex 
{ 
   public: 
   Complex(Complex &a); 
   Complex(double r=0,double i=0); 
   void display(); 
   friend Complex operator +(Complex a,Complex b); 
   friend Complex operator -(Complex a,Complex b); 
   friend Complex operator +(Complex a,double r); 
   friend Complex operator -(Complex a,double r); 
   friend Complex& operator ++(Complex &a);//前增量操作符重载 
   friend Complex operator ++(Complex &a,int); //后增量操作符重载 
   private: 
   double real; 
   double img; 
}; 
//未定义的函数与程序16.3.3相同 
Complex& operator ++(Complex &a) 
{ 
   a.img++; 
   a.real++; 
   return a;//返回类型为Complex的引用,即返回操作数a本身 
} 
Complex operator ++(Complex &a,int)//第二个整型参数表示这是后增量操作符 
{ 
   Complex temp(a); 
   a.img++; 
   a.real++; 
   return temp;//返回一个临时的值 
} 
//main.cpp 
#include "complex.h"  
#include <iostream.h>//由于VC编译器存在问题,这里使用标准的写法无法通过编译 
int main() 
{ 
   Complex a(2,2),b(2,4),temp; 
   temp=(a++)+b; 
   temp.display(); 
   cout <<endl; 
   temp=b-(++a); 
   temp.display(); 
   cout <<endl; 
   a.display(); 
   cout <<endl; 
   return 0; 
} 
运行结果:
4+6i 
-2+0i 
4+4i 
根据运行结果,可以看到a++和++a被区分开来了。而调用后增量操作符的时候,操作数仍然只有一个,与那个用于区分的整型参数无关。
至此,我们已经学完了类的一些基本特性和要素。在接下来的章节中,我们要更深入地发掘面向对象程序设计的优势。

15.类的继承

在前面的章节,我们学会了如何编写一个完整的类。然而,面向对象的优势还没有被完全体现出来。特别是在编写一些相似的类时,可能会造成很多的浪费。本章就将以一个文字游戏为例,向大家介绍类的继承问题。

剑士·弓箭手·法师的困惑

在一个角色扮演类游戏(RPG)中,可能有各种不同职业的玩家,比如剑士、弓箭手和法师。虽然他们的职业不同,却有着一些相似之处:他们都具有生命值(Health Point——HP)、魔法值(Magic Point——MP)、攻击力(Attack Point——AP)、防御力(Defense Point——DP)、经验值(Experience——EXP)和等级(Level——LV)。虽然他们有着相似之处,但又不完全相同:剑士和弓箭手都具有普通攻击的技能,只不过剑士用的是剑,而弓箭手用的是弓箭。
这样看来,我们有麻烦了。如果只用一个类来描述三种不同职业的玩家,肯定无法描述清楚。毕竟这三种职业不是完全相同的。如果用三个类来描述这三种职业,那么三者的共同点和内在联系就无法体现出来,并且还造成了相同属性和功能的重复开发。
我们需要有一种好的方法,既能把剑士、弓箭手和法师的特点描述清楚,又能减少重复的开发和冗余的代码。在C++中,有一种称为继承的方法,使我们可以用一种已经编写好的类来扩写成一个新的类。新的类具有原有类的所有属性和操作,也可以在原有类的基础上作一些修改和增补。继承实质上是源于人们对事物的认知过程:从抽象概念到具体事物。下面我们就来看看剑士、弓箭手和法师的逻辑关系:


在上图中玩家是一个抽象的概念,剑士、弓箭手和法师是具体的事物。任何一个玩家都具有生命值、魔法值等属性,具有发动普通攻击和特殊攻击的能力。不同职业的玩家在发动普通攻击和特殊攻击时,有着不同的效果。
如果你不太玩游戏,或者对剑士、弓箭手没有什么概念,那么我们再来看看学生这个例子。学生是一个抽象的概念,具体的有本科生、中学生、小学生等。任何一个学生都具有姓名、身高、体重、性别等属性,具有学习的能力。不同阶段的学生在学习时,内容会有所不同。小学生学习四则运算,中学生学习代数几何,本科生学习高等数学。如下图:


为了描写小学生、中学生、本科生,我们可以写三个不同的类,但是会造成部分属性和功能的重复开发。我们也可以先设计一个学生类,描述出各种学生的共同属性或功能,然后再针对不同种类的学生做细节的修改。显然,第二种做法更为省力、合理。

面向对象的继承性

如果有一个类,我们可以将其实例化,成为若干个对象。另外,如果我们希望对这个类加以升级改造,我们可以将这个类继承,形成子类(或者称为派生类),被继承的类则称为父类(或者称为基类)。实例化和继承是一个类的两种发展方向。继承能够减少我们开发程序的工作量,提高类的重用性。
如果我们把编写一个类看作是一次生产,那么产品(即编写出来的类)可以有两种用途:一种是将产品直接使用,相当于将类实例化;另一种是将产品用于再生产,相当于将类继承。类在这种不断的“再生产”中变得更为强大、健全。
在第15章中,我们曾将链表结点类的实例对象作为链表类的成员数据。这称为对象的组合,它与类的继承也是完全不同的概念。继承(Inheritance)是概念的延续,子类和父类一般都是概念扩展的关系,我们通常把这种关系称为“是”关系。比如:本科生是学生,自行车是交通工具。而对象的组合是因功能需求产生的从属关系,我们通常把这种关系称为“有”关系。比如:链表有一个头结点,电脑有一个中央处理器等等。
关于如何更合理地设置类以及类与类之间关系的问题,会在软件工程这门课中作更详尽的介绍。

16.继承的实现

了解继承的概念之后,我们就来学习一下如何实现继承。

私有和保护

在第14章中我们说到,成员函数或成员数据可以是公有或者私有的。如果是公有的,那么它们可以被直接访问;如果是私有的,那么它们无法被直接访问。同时,我们还提到一个protected保留字,在没有使用继承的时候,它与private的效果是一样的,即无法被直接访问。如果使用了继承,我们就能体会到protected和private的差别。
private(私有)和protected(保护)都能实现类的封装性。private能够对外部和子类保密,即除了成员所在的类本身可以访问之外,别的都不能直接访问。protected能够对外部保密,但允许子类直接访问这些成员。public、private和protected对成员数据或成员函数的保护程度可以用下表来描述:


所以,当我们使用到继承的时候,必须考虑清楚:成员数据或成员函数到底应该是私有的还是保护的。

一个简单的例子

首先我们以一个学生类为例,介绍继承的写法:(程序17.3.1)

//student.h 
#include <iostream> 
using namespace std; 
class student//学生类作为父类 
{ 
   public: 
   student(char *n,int a,int h,int w);//带参数的构造函数 
   student();//不带参数的构造函数 
   void set(char *n,int a,int h,int w);//设置 
   char * sname(); 
   int sage(); 
   int sheight(); 
   int sweight(); 
   protected: 
   char name[10];//姓名 
   int age;//年龄 
   int height;//身高 
   int weight;//体重 
   private: 
   int test; 
}; 
char * student::sname() 
{ 
   return name; 
} 
int student::sage() 
{ 
   return age;  
} 
int student::sheight() 
{ 
   return height; 
} 
int student::sweight() 
{ 
   return weight; 
} 
void student::set(char *n,int a,int h,int w) 
{ 
   int i; 
   for (i=0;n[i]!='\0';i++) 
   { 
      name[i]=n[i]; 
   } 
   name[i]='\0'; 
   age=a; 
   height=h; 
   weight=w; 
   return; 
} 
student::student(char *n,int a,int h,int w) 
{ 
   cout <<"Constructing a student with parameter..." <<endl; 
   set(n,a,h,w); 
} 
student::student() 
{ 
   cout <<"Constructing a student without parameter..." <<endl; 
} 
//undergraduate.h 
#include "student.h" 
class Undergraduate:public student//本科生类作为子类,继承了学生类 
{ 
   public: 
   double score(); 
   void setGPA(double g);//设置绩点 
   bool isAdult();//判断是否成年 
   protected: 
   double GPA;//本科生绩点 
}; 
double Undergraduate::score() 
{ 
   return GPA; 
} 
void Undergraduate::setGPA(double g) 
{ 
   GPA=g; 
   return; 
} 
bool Undergraduate::isAdult() 
{ 
   return age>=18?true:false;//子类访问父类的保护成员数据 
} 
//main.cpp 
#include <iostream> 
#include "undergraduate.h" 
using namespace std; 
int main() 
{ 
   Undergraduate s1;//新建一个本科生对象 
   s1.set("Tom",21,178,60); 
   s1.setGPA(3.75); 
   cout <<s1.sname() <<endl; 
   cout <<s1.sage() <<endl; 
   cout <<s1.sheight() <<endl; 
   cout <<s1.sweight() <<endl; 
   cout <<s1.score() <<endl; 
   cout <<s1.isAdult() <<endl; 
   return 0; 
} 
运行结果:
Constructing a student without parameter... 
Tom 
21 
178 
60 
3.75 
1 
在使用继承之前,我们必须保证父类是已经定义好的。如果父类是虚无的、没有被定义的,那么子类也就没什么好继承的了。定义一个子类的语法格式为:
class 子类名:继承方式父类名;
根据程序17.3.1的运行结果,我们可以清楚地看到,学生类里面的公有和保护成员都已经被继承到本科生类。本科生类可以使用学生类的成员函数,也可以访问学生类的保护成员。而本科生类中定义的成员则是对学生类的补充,并且也能够被使用。

继承的方式

在程序17.3.1中,我们选择的继承方式是public。和成员的类型一样,除了public之外,继承方式还有protected和private。那么,这三种继承方式到底有什么区别呢?
public是公有继承,或称为类型继承。它主要体现的是概念的延伸和扩展,父类所有的公有、保护成员都将按部就班地继承到子类中。父类的公有成员在子类中依然是公有的,父类的保护成员在子类中依然是保护的。比如程序17.3.1中的学生类和本科生类就是这样的关系。
private是私有继承,或称为私有的实现继承。它主要体现的是父类成员的重用。父类所有的公有、保护成员继承到子类时,类型会发生改变。父类的公有成员在子类中变成了私有成员,父类的保护成员在子类中也变成了私有成员。这时,我们可以利用从父类继承而来的成员函数来实现子类的成员函数,并且不必担心外部直接访问父类的成员函数,破坏了子类的秩序。比如我们认为栈是一种特殊的链表,它只能从链表尾部添加或删除结点,栈的压栈和退栈功能可以方便地由链表类的成员函数实现。但是,如果外部还能直接访问从链表类继承而来的成员函数,那么就可以在栈的任何位置插入结点,栈就会被破坏。
protected是保护继承,或称为保护的实现继承。与私有继承类似,它也是体现父类成员的重用。只不过父类的公有成员和保护成员在子类中都变成了保护成员。因此,如果有一个孙类继承了子类,那么父类中的成员也将被继承,成为孙类的保护成员。
public、private和protected三种继承方式可以用下表描述。其中右下角的九个单元格表示各种父类成员在对应的继承方式下,成为子类成员后的性质。


在使用继承的时候,我们必须根据实际需要选择合适的继承方式。下面我们以栈继承链表为例,理解一下私有继承方式:(程序17.3.2)

//node.h 
#include <iostream> 
using namespace std; 
class Node 
{ 
friend class Linklist;//链表类作为友元类 
friend class Stack;//栈类作为友元类 
public: 
Node(); 
Node(Node &n); 
Node(int i,char c='0'); 
Node(int i,char c,Node *p,Node *n); 
~Node(); 
private: 
int idata; 
char cdata; 
Node *prior; 
Node *next; 
}; 
Node::Node() 
{ 
cout <<"Node constructor is running..." <<endl; 
idata=0; 
cdata='0'; 
prior=NULL; 
next=NULL; 
} 
Node::Node(int i,char c) 
{ 
cout <<"Node constructor is running..." <<endl; 
idata=i; 
cdata=c; 
prior=NULL; 
next=NULL; 
} 
Node::Node(int i,char c,Node *p,Node *n) 
{ 
cout <<"Node constructor is running..." <<endl; 
idata=i; 
cdata=c; 
prior=p; 
next=n; 
} 
Node::Node(Node &n) 
{ 
idata=n.idata; 
cdata=n.cdata; 
prior=n.prior; 
next=n.next; 
} 
Node::~Node() 
{ 
cout <<"Node destructor is running..." <<endl; 
} 
//linklist.h 
#include "node.h" 
#include <iostream> 
using namespace std;  
class Linklist 
{ 
public: 
Linklist(int i=0,char c='0'); 
Linklist(Linklist &l); 
~Linklist(); 
bool Locate(int i); 
bool Locate(char c); 
bool Insert(int i=0,char c='0'); 
bool Delete(); 
void Show(); 
void Destroy(); 
protected://原私有成员改为保护成员,以便于Stack类继承 
Node head; 
Node * pcurrent; 
}; 
Linklist::Linklist(int i,char c):head(i,c) 
{ 
cout<<"Linklist constructor is running..."<<endl; 
pcurrent=&head; 
} 
Linklist::Linklist(Linklist &l):head(l.head) 
{ 
cout<<"Linklist Deep cloner running..." <<endl; 
pcurrent=&head; 
Node * ptemp1=l.head.next; 
while(ptemp1!=NULL) 
{ 
Node * ptemp2=new Node(ptemp1->idata,ptemp1->cdata,pcurrent,NULL); 
pcurrent->next=ptemp2; 
pcurrent=pcurrent->next; 
ptemp1=ptemp1->next; 
} 
} 
Linklist::~Linklist() 
{ 
cout<<"Linklist destructor is running..."<<endl; 
Destroy(); 
} 
bool Linklist::Locate(int i) 
{ 
Node * ptemp=&head; 
while(ptemp!=NULL) 
{ 
if(ptemp->idata==i) 
{ 
pcurrent=ptemp; 
return true; 
} 
ptemp=ptemp->next; 
} 
return false; 
} 
bool Linklist::Locate(char c) 
{ 
Node * ptemp=&head; 
while(ptemp!=NULL) 
{ 
if(ptemp->cdata==c) 
{ 
pcurrent=ptemp; 
return true; 
} 
ptemp=ptemp->next; 
} 
return false; 
} 
bool Linklist::Insert(int i,char c) 
{ 
if(pcurrent!=NULL) 
{ 
Node * temp=new Node(i,c,pcurrent,pcurrent->next); 
if (pcurrent->next!=NULL) 
{ 
pcurrent->next->prior=temp; 
} 
pcurrent->next=temp; 
return true; 
} 
else 
{ 
return false; 
} 
} 
bool Linklist::Delete() 
{ 
if(pcurrent!=NULL && pcurrent!=&head) 
{ 
Node * temp=pcurrent; 
if (temp->next!=NULL) 
{ 
temp->next->prior=pcurrent->prior; 
} 
temp->prior->next=pcurrent->next; 
pcurrent=temp->prior; 
delete temp; 
return true; 
} 
else 
{ 
return false; 
} 
} 
void Linklist::Show() 
{ 
Node * ptemp=&head; 
while (ptemp!=NULL) 
{ 
cout <<ptemp->idata <<'\t' <<ptemp->cdata <<endl; 
ptemp=ptemp->next; 
} 
} 
void Linklist::Destroy() 
{ 
Node * ptemp1=head.next; 
while (ptemp1!=NULL) 
{ 
Node * ptemp2=ptemp1->next; 
delete ptemp1; 
ptemp1=ptemp2; 
} 
head.next=NULL; 
} 
//stack.h 
#include "linklist.h" 
class Stack:private Linklist//私有继承链表类 
{ 
public: 
bool push(int i,char c); 
bool pop(int &i,char &c); 
void show(); 
}; 
bool Stack::push(int i,char c) 
{ 
while (pcurrent->next!=NULL) 
pcurrent=pcurrent->next; 
return Insert(i,c);//用链表类的成员函数实现功能 
} 
bool Stack::pop(int &i,char &c) 
{ 
while (pcurrent->next!=NULL) 
pcurrent=pcurrent->next; 
i=pcurrent->idata; 
c=pcurrent->cdata; 
return Delete();//用链表类的成员函数实现功能 
} 
void Stack::show() 
{ 
Show();//用链表类的成员函数实现功能 
} 
//main.cpp 
#include <iostream> 
#include "stack.h" 
int main() 
{ 
Stack ss; 
int i,j; 
char c; 
for (j=0;j<3;j++) 
{ 
cout <<"请输入一个数字和一个字母:" <<endl; 
cin >>i >>c; 
if (ss.push(i,c)) 
{ 
cout <<"压栈成功!" <<endl; 
} 
} 
ss.show(); 
while (ss.pop(i,c)) 
{ 
cout <<"退栈数据为i=" <<i <<" c=" <<c <<endl; 
} 
return 0; 
} 
运行结果:
Node constructor is running... 
Linklist constructor is running... 
请输入一个数字和一个字母: 
1 a 
Node constructor is running... 
压栈成功! 
请输入一个数字和一个字母: 
2 b 
Node constructor is running... 
压栈成功! 
请输入一个数字和一个字母: 
3 c 
Node constructor is running... 
压栈成功! 
0 0 
1 a 
2 b 
3 c 
Node destructor is running... 
退栈数据为i=3 c=c 
Node destructor is running... 
退栈数据为i=2 c=b 
Node destructor is running... 
退栈数据为i=1 c=a 
Linklist destructor is running... 
Node destructor is running... 
我们看到,Stack类私有继承了Linklist类之后,利用Linklist的成员函数,方便地实现了压栈和退栈功能。

17.子类对象

对象在使用之前,始终是要经历“构造”这个过程的。在第15章,我们了解到当一个对象的成员数据是另一个对象的时候,就先运行成员对象的构造函数,再运行父对象的构造函数。但是继承的出现,会引入子类的构造函数。这时候,这些构造函数的运行顺序又是怎样的呢?

子类对象的构造

讨论子类对象的构造,就是在讨论子类对象的生成方式。它是先生成父类对象的成员,再对其进行扩展呢,还是先生成子类对象的成员,然后再对其进行补充?我们还是修改一下程序17.3.2,用事实来解决这个问题:(程序17.4.1)

//node.h和linklist.h同程序17.3.2 
//stack.h 
#include "linklist.h" 
class Stack:private Linklist//私有继承链表类 
{ 
   public: 
   bool push(int i,char c); 
   bool pop(int &i,char &c); 
   void show(); 
   Stack(int i,char c); 
   Stack(); 
}; 
Stack::Stack(int i,char c):Linklist(i,c)//将子类构造函数的参数传递给父类的构造函数 
{ 
   cout <<"Stack constructor with parameter is running..." <<endl; 
} 
Stack::Stack()//子类构造函数 
{ 
   cout <<"Stack constructor is running..." <<endl; 
} 
bool Stack::push(int i,char c) 
{ 
   while (pcurrent->next!=NULL) 
   pcurrent=pcurrent->next; 
   return Insert(i,c); 
} 
bool Stack::pop(int &i,char &c) 
{ 
   while (pcurrent->next!=NULL) 
   pcurrent=pcurrent->next; 
   i=pcurrent->idata; 
   c=pcurrent->cdata; 
   return Delete(); 
} 
void Stack::show() 
{ 
   Show(); 
} 
//main.cpp 
#include <iostream> 
#include "stack.h" 
int main() 
{ 
   Stack ss(1,'4');//调用带参数的构造函数 
   cout <<"Stack ss constructed" <<endl; 
   ss.show(); 
   Stack zz; //调用不带参数的构造函数 
   cout <<"Stack zz constructed" <<endl; 
   zz.show(); 
   return 0; 
} 
运行结果:
Node constructor is running... 
Linklist constructor is running... 
Stack constructor with parameter is running... 
Stack ss constructed 
1 4 
Node constructor is running... 
Linklist constructor is running... 
Stack constructor is running... 
Stack zz constructed 
0 0 
Linklist destructor is running... 
Node destructor is running... 
Linklist destructor is running... 
Node destructor is running... 
这个程序中有三个类,其中Stack类是Linklist类的子类,Node类的对象是Linklist类的成员数据。根据程序的运行结果,我们可以确定,父类的成员对象仍然是最先构造的,接着是运行父类的构造函数,最后运行子类的构造函数。也就是说子类对象是在父类对象的基础上扩展而成的。
另外,如果我们希望把子类的构造函数的参数传递给父类的构造函数时,可以在子类的构造函数定义中用以下格式调用父类的构造函数:
子类名::构造函数名(参数表):父类名(参数表)
如程序17.4.1就是用上述方法实现子类和父类的构造函数参数传递。这样的方法不仅使子类对象的初始化变得简单,并且使子类和父类的构造函数分工明确,易于维护。

子类对象的析构

在第15章中介绍析构函数的时候,我们就说它的运行顺序往往是和构造函数的运行顺序相反的。那么使用了继承之后,是否依然是这样的规律呢?我们继续修改程序17.4.1,尝试验证我们的猜想。 

//node.h和linklist.h同程序17.3.2 
//stack.h 
#include "linklist.h" 
class Stack:private Linklist 
{ 
   public: 
   bool push(int i,char c); 
   bool pop(int &i,char &c); 
   void show(); 
   Stack(int i,char c); 
   Stack(); 
   ~Stack();//析构函数 
}; 
Stack::Stack(int i,char c):Linklist(i,c) 
{ 
   cout <<"Stack constructor with parameter is running..." <<endl; 
} 
Stack::Stack() 
{ 
   cout <<"Stack constructor is running..." <<endl; 
} 
Stack::~Stack() 
{ 
   cout <<"Stack destructor is running..." <<endl; 
} 
bool Stack::push(int i,char c) 
{ 
   while (pcurrent->next!=NULL) 
   pcurrent=pcurrent->next; 
   return Insert(i,c); 
} 
bool Stack::pop(int &i,char &c) 
{ 
   while (pcurrent->next!=NULL) 
   pcurrent=pcurrent->next; 
   i=pcurrent->idata; 
   c=pcurrent->cdata; 
   return Delete(); 
} 
void Stack::show() 
{ 
   Show(); 
} 
//main.cpp 
#include <iostream> 
#include "stack.h" 
int main() 
{ 
   Stack zz; 
   cout <<"Stack zz constructed" <<endl; 
   zz.show(); 
   return 0; 
} 
运行结果:
Node constructor is running... 
Linklist constructor is running... 
Stack constructor is running... 
Stack zz constructed 
0 0 
Stack destructor is running... 
Linklist destructor is running... 
Node destructor is running... 
根据运行结果,我们可以确认:使用了继承之后,析构函数的运行顺序依然恰好与构造函数的运行顺序相反。

18.继承与对象指针

我们在第14章的最后学习了对象指针,并且在编写链表类的过程中已经能熟练地使用它了。现在有了继承之后,我们的心中有了疑问:父类指针能否指向子类对象?子类指针能否指向父类对象?如果那样使用指针,对象的功能是否会受到限制呢?

父类指针与子类对象

我们修改程序17.3.1,用程序的运行结果来解答我们的疑问:

//student.h和undergraduate.h同程序17.3.1 
//main.cpp 
#include <iostream> 
#include "undergraduate.h" 
using namespace std; 
int main() 
{ 
   Undergraduate s1;//新建一个本科生对象 
   Undergraduate *s1p;//新建一个子类的对象指针 
   student s2; 
   student *s2p;//新建一个父类的对象指针 
   s1p=&s2;//这行程序出错了 
   s2p=&s1; 
   s1.set("Tom",21,178,60); 
   cout <<s1.sname <<s1.sage <<endl; 
   s2p->set("Jon",22,185,68); 
   cout <<s1.sname <<s1.sage <<endl; 
   s1p->setGPA(2.5); 
   s2p->setGPA(3.0); //这行程序出错了 
   return 0; 
} 
编译结果:
main.cpp(10) : error C2440: '=' : cannot convert from 'class student *' to 'class Undergraduate *'
main.cpp(17) : error C2039: 'setGPA' : is not a member of 'student' 
根据编译结果,我们可以看到,在公有继承情况下父类的对象指针指向子类对象是允许的。如s2p学生指针指向本科生s1,因为本科生也是学生;子类的对象指针指向父类是禁止的。如s1p本科生指针不能指向学生s2,因为学生不一定是本科生。
此外,如果我们用父类的对象指针指向子类对象,那么这个指针无法使用子类中扩展出的成员。如s2p指针无法设置本科生的绩点,因为使用了学生指针,本科生就变成了学生的身份,学生身份不再有设置绩点的功能。
我们再次修改程序17.3.1,使得它能够运行:(程序17.5)
//student.h和undergraduate.h同程序17.3.1 
//main.cpp 
#include <iostream> 
#include "undergraduate.h" 
using namespace std; 
int main() 
{ 
   Undergraduate s1; 
  student s2; 
   student *s2p; 
   s2p=&s1; 
   s1.set("Tom",21,178,60); 
   cout <<s1.sname() <<'\t' <<s1.sage() <<endl; 
   s2p->set("Jon",22,185,68); 
   cout <<s1.sname() <<'\t' <<s1.sage() <<endl; 
   return 0; 
} 
运行结果:
Constructing a student without parameter... 
Constructing a student without parameter... 
Tom 21 
Jon 22 
现在程序能够正常运行了。可见,用s1设置本科生信息和用s2p指针设置学生信息都是可行的。

覆盖(Overlap)

假设我们为学生类和本科生类都写了一个名为study的成员函数。两者的名称相同,参数表相同,实现却不相同。当子类和父类有着两个名字和参数表完全相同的函数时,我们把这个现象称为覆盖(Overlap)。如下面的代码:

//student.h 
class student//学生类作为父类 
{ 
   public: 
   …… 
   void study(); 
   protected: 
   char name[10]; 
   int age; 
   int height; 
   int weight; 
}; 
…… 
void student::study() 
{ 
   cout <<"随便学些什么。" <<endl; 
   return; 
} 
//undergraduate.h 
class Undergraduate:public student 
{ 
   public: 
   …… 
   void study(); 
   protected: 
   double GPA;//本科生绩点 
}; 
…… 
void Undergraduate::study() 
{ 
   cout <<"学习高等数学和大学英语。" <<endl; 
   return; 
} 
如果有一个本科生对象s1和一个学生对象s2,那么显然s1.study()会是学习高等数学和大学英语,s2.study()会是随便学些什么。但是,如果有一个学生类的指针sp,它也能指向本科生对象,这时调用sp->study()会是怎么样的呢?我们发现,即使它指向一个本科生对象,它也只能“随便学些什么”。这样的结果在情理之中,却并不是我们期望的。我们希望程序能够“猜”到sp指针指向了哪种对象,并且调用各自的study成员函数。这个功能如何才能实现?在之后的几节我们会作讲解。

19.多态性

在本章的开头介绍一个RPG游戏的时候,我们就说到不同职业的玩家在发动普通攻击和特殊攻击时,有着不同的效果。在编写程序的时候,我们并不知道用户会选择哪种职业的玩家,那么又该如何保证各种攻击效果和用户选择的玩家是对应的呢?
在使用继承的时候,子类必然是在父类的基础上有所改变。如果两者完全相同,这样的继承就失去了意义。同时,不同子类之间具体实现也是有所区别的,否则就出现了一个多余的类。不同的类的同名成员函数有着不同的表现形式,称为多态性。多态性是符合人的认知规律的,即称呼相同,所指不同。比如,学生类及其子类都有学习这个成员函数,但本科生、中学生、小学生的学习内容并不相同;玩家类的子类都有攻击这项技能,但剑士、弓箭手和魔法师的攻击方法不同。
多态性往往只有在使用对象指针或对象引用时才体现出来。编译器在编译程序的时候完全不知道对象指针可能会指向哪种对象(引用也是类似的情况),只有到程序运行了之后才能明确指针访问的成员函数是属于哪个类的。我们把C++的这种功能称为“滞后联编”。多态性是面向对象的一个标志性特点,没有这个特点,就无法称为面向对象。

20.多态与虚函数

多态能够方便我们编写程序,可以让不同的类与它独特的成员函数一一对应。即使我们只是简单地“称呼”,程序也会很明白我们的心思。那么,多态应该如何实现呢?

多态的实现

在C++中,我们把表现多态的一系列成员函数设置为虚函数。虚函数可能在编译阶段并没有被发现需要调用,但它还是整装待发,随时准备接受指针或引用的“召唤”。设置虚函数的方法为:在成员函数的声明最前面加上保留字virtual。注意,不能把virtual加到成员函数的定义之前,否则会导致编译错误。
下面我们把各种学生的学习都设置为虚函数,了解如何实现多态:(程序17.7.1)

//student.h 
#include <iostream> 
using namespace std; 
class student 
{ 
   public: 
   student(char *n,int a,int h,int w); 
   student(); 
   void set(char *n,int a,int h,int w); 
   char * sname(); 
   int sage(); 
   int sheight(); 
   int sweight(); 
   virtual void study();//把学习设置为虚函数 
   protected: 
   char name[10]; 
   int age; 
   int height; 
   int weight; 
}; 
char * student::sname() 
{ 
   return name; 
} 
int student::sage() 
{ 
   return age; 
} 
int student::sheight() 
{ 
   return height; 
} 
int student::sweight() 
{ 
   return weight; 
} 
void student::set(char *n,int a,int h,int w) 
{ 
   int i; 
   for (i=0;n[i]!='\0';i++) 
   { 
      name[i]=n[i]; 
   } 
   name[i]='\0'; 
   age=a; 
   height=h; 
   weight=w; 
   return; 
} 
student::student(char *n,int a,int h,int w) 
{ 
   cout <<"Constructing a student with parameter..." <<endl; 
   set(n,a,h,w); 
} 
student::student() 
{ 
   cout <<"Constructing a student without parameter..." <<endl; 
} 
void student::study()//成员函数定义处没有virtual 
{ 
   cout <<"随便学些什么。" <<endl; 
   return; 
} 
//undergraduate.h 
#include "student.h" 
class Undergraduate:public student 
{ 
   public: 
   double score(); 
   void setGPA(double g); 
   bool isAdult(); 
   virtual void study();//把学习设置为虚函数 
   protected: 
   double GPA; 
}; 
double Undergraduate::score() 
{ 
   return GPA; 
} 
void Undergraduate::setGPA(double g) 
{ 
   GPA=g; 
   return; 
} 
bool Undergraduate::isAdult() 
{ 
   return age>=18?true:false; 
} 
void Undergraduate::study()//成员函数定义处没有virtual 
{ 
   cout <<"学习高等数学和大学英语。" <<endl; 
   return; 
} 
//pupil.h 
class Pupil:public student 
{ 
   public: 
   virtual void study();//把学习设置为虚函数 
}; 
void Pupil::study() 
{ 
   cout <<"学习语数外。" <<endl; 
   return; 
} 
//main.cpp 
#include <iostream> 
#include "undergraduate.h" 
#include "pupil.h" 
using namespace std; 
int main() 
{ 
   Undergraduate s1; 
   student s2; 
   Pupil s3; 
   student *sp=&s1;//sp指向本科生对象 
   s1.set("Tom",21,178,60); 
   sp->study();//体现多态性 
   sp=&s2; //sp指向学生对象 
   s2.set("Jon",22,185,68); 
   sp->study();//体现多态性 
   sp=&s3; //sp指向小学生对象 
   s3.set("Mike",8,148,45); 
   sp->study();//体现多态性 
   return 0; 
} 
运行结果:
Constructing a student without parameter... 
Constructing a student without parameter... 
Constructing a student without parameter... 
学习高等数学和大学英语。
随便学些什么。
学习语数外。
我们看到,将学习设置为虚函数之后,无论对象指针sp指向哪种学生对象,sp->study()的执行结果总是与对应的类相符合的。多态就通过虚函数实现了。
我们在编写成员函数的时候,可以把尽可能多的成员函数设置为虚函数。这样做可以充分表现多态性,并且也不会给程序带来不良的副作用。

无法实现多态的虚函数

使用虚函数可以实现多态,但是如果在使用虚函数的同时再使用重载,就会可能使虚函数失效。我们修改程序17.7.1,看看重载会给虚函数带来些什么麻烦:(程序17.7.2)

//student.h 
#include <iostream> 
using namespace std; 
class student 
{ 
   public: 
   student(char *n,int a,int h,int w); 
   student(); 
   void set(char *n,int a,int h,int w); 
   char * sname(); 
   int sage(); 
   int sheight(); 
   int sweight(); 
   virtual void study(int c=0);//设置为虚函数,带默认参数 
   protected: 
   char name[10];//姓名 
   int age;//年龄 
   int height;//身高 
   int weight;//体重 
}; 
…… 
void student::study(int c) 
{ 
   cout <<"随便学些什么。" <<endl; 
   return; 
} 
//undergraduate.h和pupil.h同程序17.7.1 
//main.cpp 
#include <iostream> 
#include "undergraduate.h" 
#include "pupil.h" 
using namespace std; 
int main() 
{ 
   Undergraduate s1; 
   student s2; 
   Pupil s3; 
   student *sp=&s1; 
   s1.set("Tom",21,178,60); 
   sp->study(1);//带参数 
   sp=&s2; 
   s2.set("Jon",22,185,68); 
   sp->study(); 
   sp=&s3; 
   s3.set("Mike",8,148,45); 
   sp->study(); 
   return 0; 
} 
运行结果:
Constructing a student without parameter... 
Constructing a student without parameter... 
Constructing a student without parameter... 
随便学些什么。
随便学些什么。
随便学些什么。
当学生类的study成员函数和本科生类的study成员函数参数格式不同时,即使把学生类中的study设置为虚函数,编译器也无法找到本科生类中与之完全相同的study函数。多态是在程序员没有指定调用父类还是某个子类的成员函数时,电脑根据程序员的要求,揣测并选择最合适的成员函数去执行。但是当成员函数的参数格式不同时,程序员在调用成员函数的各种参数无疑就是在暗示到底调用哪个成员函数。这时电脑岂敢自作主张揣测人类的心思?因此,要使用虚函数实现多态性,至少要使各个函数的参数格式也完全相同。

21.虚函数与虚析构函数

在类中,有两个与众不同的成员函数,那就是构造函数和析构函数。当构造函数与析构函数遭遇继承和多态,它们的运行状况又会出现什么变化呢?
多态性是在父类或各子类中执行最合适成员函数。一般来说,只会选择父类或子类中的某一个成员函数来执行。这可给析构函数带来了麻烦!如果有的资源是父类的构造函数申请的,有的资源是子类的构造函数申请的,而虚函数只允许程序执行父类或子类中的某一个析构函数,岂不是注定有一部分资源将无法被释放?为了解决这个问题,虚析构函数变得与众不同。
下面我们就来给析构函数的前面加上保留字virtual,看看运行的结果会怎么样:(程序17.8)

//animal.h 
#include <iostream> 
using namespace std; 
class Animal 
{ 
   public: 
   Animal(int w=0,int a=0); 
   virtual ~Animal();//虚析构函数 
   protected: 
   int weight,age; 
}; 
Animal::Animal(int w,int a) 
{ 
   cout <<"Animal consturctor is running..." <<endl; 
   weight=w; 
   age=a; 
} 
Animal::~Animal() 
{ 
   cout <<"Animal destructor is running..." <<endl; 
} 
//cat.h 
#include "animal.h" 
class Cat:public Animal 
{ 
   public: 
   Cat(int w=0,int a=0); 
   ~Cat(); 
}; 
Cat::Cat(int w,int a):Animal(w,a) 
{ 
      cout <<"Cat constructor is running..." <<endl; 
} 
Cat::~Cat() 
{ 
   cout <<"Cat destructor is running..." <<endl; 
} 
//main.cpp 
#include "cat.h" 
int main() 
{ 
   Animal *pa=new Cat(2,1); 
   Cat *pc=new Cat(2,4); 
   cout <<"Delete pa:" <<endl; 
   delete pa; 
   cout <<"Delete pc:" <<endl; 
   delete pc; 
   return 0; 
} 
运行结果:
Animal consturctor is running... 
Cat constructor is running... 
Animal consturctor is running... 
Cat constructor is running... 
Delete pa: 
Cat destructor is running... 
Animal destructor is running... 
Delete pc: 
Cat destructor is running... 
Animal destructor is running... 
我们惊讶地发现,虚析构函数不再是运行父类或子类的某一个析构函数,而是先运行合适的子类析构函数,再运行父类析构函数。即两个类的析构函数都被执行了,如果两块资源分别是由父类构造函数和子类构造函数申请的,那么使用了虚析构函数之后,两块资源都能被及时释放。
我们修改程序17.8,将Animal类析构函数前的virtual去掉,会发现运行结果中删除pa指向的Cat对象时,不执行Cat类的析构函数。如果这时Cat类的构造函数里申请了内存资源,就会造成内存泄漏了。
所以说,虚函数与虚析构函数的作用是不同的。虚函数是为了实现多态,而虚析构函数是为了同时运行父类和子类的析构函数,使资源得以释放。

22.抽象类与纯虚函数

在本章开头介绍的RPG游戏中共有4个类。其中玩家类作为父类,剑士类、弓箭手类、魔法师类分别继承玩家类,作为子类。当我们开始游戏时,需要选择创建某一个类的对象,才能进行游戏。然而,我们的选择不应该是4个类,而应该只能在剑士类、弓箭手类或魔法师类中做出选择。因为,单纯的玩家类是抽象的、不完整的,任何一个玩家必须有一个确定的职业之后,才能确定他的具体技能。又比如学生类,它也是非常抽象的。让一个小学生、中学生或本科生学习,他们都有各自学习的内容。而一个抽象概念的学生,他却不知道该学些什么。
这时,我们必须要对玩家类或学生类作一些限制了。由于玩家类和学生类直接实例化而创建的对象都是抽象而没有意义的,所以我们希望玩家类和学生类只能用于被继承,而不能用于直接创建对象。在C++中,我们可以把只能用于被继承而不能直接创建对象的类设置为抽象类(Abstract Class)。
之所以要存在抽象类,最主要是因为它具有不确定因素。我们把那些类中的确存在,但是在父类中无法确定具体实现的成员函数称为纯虚函数。纯虚函数是一种特殊的虚函数,它只有声明,没有具体的定义。抽象类中至少存在一个纯虚函数;存在纯虚函数的类一定是抽象类。存在纯虚函数是成为抽象类的充要条件。
那么我们该如何定义一个纯虚函数呢?纯虚函数的声明有着特殊的语法格式:
virtual 返回值类型成员函数名(参数表)=0;
请注意,纯虚函数应该只有声明,没有具体的定义,即使给出了纯虚函数的定义也会被编译器忽略。下面我们就修改一下程序17.7.1,将学生类变成一个抽象类:(程序17.9)

//student.h 
#include <iostream> 
using namespace std; 
class student//因为存在纯虚函数study,student类自动变成了抽象类 
{ 
   public: 
   student(char *n,int a,int h,int w); 
   student(); 
   void set(char *n,int a,int h,int w); 
   char * sname(); 
   int sage(); 
   int sheight(); 
   int sweight(); 
   virtual void study()=0;//声明study为纯虚函数 
   protected: 
   char name[10]; 
   int age; 
   int height; 
   int weight; 
}; 
char * student::sname() 
{ 
   return name; 
} 
int student::sage() 
{ 
   return age; 
} 
int student::sheight() 
{ 
   return height; 
} 
int student::sweight() 
{ 
   return weight; 
} 
void student::set(char *n,int a,int h,int w) 
{ 
    int i; 
   for (i=0;n[i]!='\0';i++) 
   { 
      name[i]=n[i];  
   } 
   name[i]='\0'; 
   age=a; 
   height=h; 
   weight=w; 
   return; 
} 
student::student(char *n,int a,int h,int w) 
{ 
   cout <<"Constructing a student with parameter..." <<endl; 
   set(n,a,h,w); 
} 
student::student() 
{ 
   cout <<"Constructing a student without parameter..." <<endl; 
} 
//undergraduate.h和pupil.h同程序17.7.1 
//main.cpp 
#include <iostream> 
#include "undergraduate.h" 
#include "pupil.h" 
using namespace std; 
int main() 
{ 
   Undergraduate s1; 
   /*student s2;//此时创建学生对象将会出现编译错误*/ 
   Pupil s3; 
   student *sp=&s1; 
   s1.set("Tom",21,178,60); 
   sp->study(); 
   sp=&s3; 
   s3.set("Mike",8,148,45); 
   sp->study(); 
   return 0; 
} 
运行结果:
Constructing a student without parameter... 
Constructing a student without parameter... 
学习高等数学和大学英语。
学习语数外。
我们看到,设置了纯虚函数之后并不影响多态的实现,但是却将父类变成了抽象类,限制了父类对象的创建。有了抽象类之后,就不会再出现不确定职业的玩家、不确定身份的学生了。

23.多重继承

2002年,河北省的一位造车高手奇思妙想设计出了一辆水陆两用型的小跑车。这辆车既可以在公路上奔驰,也可以在水波中荡漾,它同时具有车和船的特性。(如图17.10.1)


如果我们用继承的概念来分析水陆两用车,那么它的确是存在继承关系的。与一般的继承不同的是,水陆两用车的父类会有两个:一个是车,一个是船。(如下图17.10.2)

其有长、宽、高等属性,要搞清水陆两用车的长、宽、高到底是从哪个类继承来的,着实要花费一些功夫。应该说,C++中多重继承的思想是优秀的,但是它的实现却是混乱的。有不少人都认为多重继承是C++的一个败笔,它把原本简单的单一继承复杂化了,使程序员很难再把思路理清。所以,即使是经验丰富的程序员,在大多数情况下也不会去使用多重继承。在此,我们只要能够理解多重继承的概念即可,不必去掌握它的具体实现。有兴趣的读者也可以到网上查找相关的资料。





0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:361927次
    • 积分:4472
    • 等级:
    • 排名:第6870名
    • 原创:137篇
    • 转载:32篇
    • 译文:13篇
    • 评论:29条
    个人经历
    爱编程,爱晚起,偶尔也忙到深夜; 喜欢学习,努力工作,也享受生活; 我酷爱技术,崇尚简单的快乐和幸福; 我不是码农,我是程序员; 我和你一样,为理想而奋斗.
    文章分类
    博客专栏