多态性(二)

四. 抽象基类和纯虚函数

        我们在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口 ,而不希望用户实际地创建一个基类的对象。要做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),来是基类成为抽象类(abstract)。纯虚函数使用关键字virtual ,并且在其后面加上 = 0 。如果某人试着生成一个抽象类的对象,编译器会制止他。

        当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。建立公共接口的唯一原因是它能对于每个不同的子类有不同的表示。它建立一个基本的格式,用来确定什么是对于所有派生类是公共的——除此之外,别无用途。

// Pure abstract base classed
#include  < iostream >
using   namespace  std;
enum  note { middleC,Csharp,Cflat };         // Etc.

class  Instrument
{
public :
          
// Pure virtual functions:
           virtual   void  play(note)  const   =   0 ;
          
virtual   char *  what()  const   =   0 ;
           
// Assume this will modify the object:
           virtual   void  adjust( int =   0 ;
};

class  Wind :  public  Instrument
{
public :
          
void  play(note)  const
          {
                   cout
<< " Wind::play " << endl;
          }
          
char *  what()  const  {  return   " Wind " ; }
          
void  adjust( int ) {}
};

class  Percussion:  public  Instrument
{
public :
          
void  play(note)  const
          {
                     cout
<< " Percussion::play " << endl;
          }
          
char *  what()  const  {  return   " Percussion " ; }
          
void  adjust( int ) {}
};

class  Stringed :  public  Instrument
{
public :
          
void  play(note)  const
          {
                     cout
<< " Stringed::play " << endl;
          }
          
char *  what()  const  {  return   " Stringed " ; }
          
void  adjust( int ) {}
};

class  Brass :  public  Wind
{
public :
          
void  play(note)  const  
          {
                   cout
<< " Brass::play " << endl;
          }
          
char *  what()  const  {  return   " Brass " ; }
};

class  Woodwind :  public  Wind
{
public :
           
void  play(note)  const
           {
                    cout
<< " Woodwind::play " << endl;
           }
           
char *  what()  const  {  return   " Woodwind " ; }
};

// Identical function from before:
void  tune(Instrument &  i)
{
            i.play(middleC);
}

// New function
void  f(Instrument &  i) { i.adjust( 1 ); }

int  main()
{
            Wind flute;
            Percussion drum;
            Stringed violin;
            Brass flugelhorn;
            Woodwind recorder;
             tune(flute);
             tune(drum);
             tune(violin);
             tune(flugelhorn);
             tune(recorder);
             f(flugelhorn);
}

        注意,纯虚寒是禁止对抽象类的函数以传值方式调用。这也是防止对象切片(object slicing) 的一种方法。通过抽象类,可以保证在向上类型转换期间总是使用指针或引用。

        在基类中,对纯虚函数提供定义是可能的。这只是我们可能希望有一段公共代码,使一些派生类定义都能调用,而不必在每个函数中重复这段代码。不过纯虚函数不允许内联。

// Pure virtual base definitions
#include  < iostream >
using   namespace  std;

class  Pet
{
public :
        
virtual   void  speak()  const   =   0 ;
        
virtual   void  eat()  const   =   0 ;
};

void  Pet::eat()  const  
{
        cout
<< " Pet::eat() " << endl;
}

void  Pet::speak()  const  
{
         cout
<< " Pet::speak() " << endl;
}

class  Dog :  public  Pet
{
public :
        
// Use the common Pet code:
         void  speak()  const  { Pet::speak(); }
        
void  eat()  const  { Pet::eat(); }
};

int  main()
{
        Dog simba;
        simba.speak();
        simba.eat();
}

          其实C++的抽象类就是C#和JAVA里面接口的概念。看来JAVA、C#向C++借了不少东西。

五. 继承和派生

        可以想像,当实现继承和重新定义一些虚函数时,会发生什么事情。编译器对新类创建一个新VTABLE表,并且插入新函数的地址,对于没有重新定义的虚函数使用基类函数的地址。无论如何,对于可被创建的每个对象,在VTABLE中总有一个函数地址的全集,所以绝对不能对不在其中的地址进行调用。

       但是在派生类中继承或增加新的虚函数时会发生什么呢?看下面这个例子:

// Adding virtuals in derivation
#include  < iostream >
#include 
< string >
using   namespace  std;

class  Pet
{
         
string  pname;
public :
         Pet(
const   string &  petName) : pname(petName) {}
         
virtual   string  name()  const  {  return  pname;}
         
virtual   string  speak()  const  {  return   "" ; }
};

class  Dog :  public  Pet
{
         
string  name;
public :
         Dog(
const   string &  petName) : Pet(petName) {}
          
// New virtual function in the Dog class:
           virtual   string  sit()  const
          {
                   
return  Pet::name()  +   "  sits " ;
           }
           
string  speak()  const  
           {
                   
return  Pet::name()  + "  says 'Bark!' " ;
           }
};

int  main()
{
          Pet
*  p[] = { new  Pet( " generic " ), new  Dog( " bob " )};
          cout
<< " p[0]->speak() =  " << p[ 0 ] -> speak() << endl;
          cout
<< " p[1]->speak() =  " << p[ 1 ] -> speak() << endl;
          
// ! cout<<"p[1]->sit() = "<<p[1]->sit()<<endl;       // error
}

         类Pet中含有2个虚函数,而在类Dog 中又增加了第三个sit() 虚函数。可以看出我们无法从Pet 类中来调用sit() 函数。即编译器通过防止我们对只存在于派生类中的函数做虚函数调用来完成工作。

        有一些比较少见的情况,可能我们知道指针实际上指向哪一种特殊子类的对象。这时如果想调用只存在于这个子类中的函数,则必须类型转换这个指针。
                           ((Dog*)p[1])->sit(); 

        关于对象切片的问题:

        当多态地处理对象时,传地址与传值有明显的不同。这是因为地址都有相同的长度,传递派生类对象的地址和传递基类对象的地址是相同的。

        但是如果对一个对象进行向上类型转换,而不使用地址或引用。这时,这个对象被"切片",直到剩下来的是适合于目的的子对象。

#include  < iostream >
#include 
< string >
using   namespace  std;

class  Pet
{
       
string  pname;
public :
       Pet(
const   string &  name) : pname(name) {}
       
virtual   string  name()  const  {  return  pname; }
       
virtual   string  description()  const  
       {
               
return   " This is  " + pname;
       }
};

class  Dog :  public  Pet
{
         
string  favoriteActivity;
public :
         Dog(
const   string &  name, const   string &  activity)
                   :Pet(name),favoriteActivity(activity) {}
         
string  description()  const  
         {
                   
return  Pet::name() + "  likes to  " + favoriteActivity;
          }
};

void  describe(Pet p)
{
          cout
<< p.description() << endl;
}

int  main()
{
           Pet p(
" Alfred " );
           Dog d(
" Fluffy " , " sleep " );
           describe(p);
           describe(d);
}

        输出的结果是两次都是调用的基类description() 函数。按值传递的时候,编译器强迫派生类的对象变为基类的对象,这里调用了基类的拷贝构造函数。去掉了派生类中多于的部分,保留了基类的部分。所以,在函数传递参数的过程中,应该尽量避免对象切片。

六. 重载和重新定义

       先看下面的例程:

// Virtual functions restrict overloading
#include  < iostream >
#include 
< string >
using   namespace  std;

class  Base
{
public :
        
virtual   int  f()  const
        {
                 cout
<< " Base::f() " ;
                 
return   1 ;
         }
         
virtual   void  f( string const  {}
         
virtual   void  g()  const  {}
};

class  Derived1 :  public  Base
{
public :
        
void  g()  const  {}
};

class  Derived2 :  public  Base
{
public :
         
// Overriding a virtual function:
          int  f()  const
        {
                cout
<< " Derived2::f() " ;
                
return   2 ;
        }
};

class  Derived3 :  public  Base
{
public :
        
// Cannot change return type:
        
//  void f() const { cout<<"Derived3::f() "; } }
};

class  Derived4 :  public  Base
{
public :
        
// Change argument list:
         int  f( int const  
        {
                 cout
<< " Derived4::f() " ;
                 
return   4 ;
        }
};

int  main()
{
         
string  s( " hello " );
         Derived1 d1;
         
int  x = d1.f();
         d1.f(s);
         Derived2 d2;
          x
= d2.f();
          
// d2.f(s);
          Derived4 d4;
          x
= d4.f( 1 );
          
// x=d4.f()
          
// d4.f(s);
          Base &  br = d4;     // Upcast
          
// br.f(1);
          br.f();
          br.f(s);
}

        在Derived3中,编译器不允许我们改变重新定义过的函数的返回值(如果f() 不是虚函数,则是允许的)。如果重新定义了基类中的一个重载成员函数,则在派生类中其它的重载函数将会被隐藏。这与普通函数的重定义是一样的。

       虽然在Derived3中,我们不能在重新定义中修改虚函数的返回类型,但我们可以把它改成返回基类的指针或引用,则改函数的重新定义版本将会从基类返回的内容中返回一个指向派生类的指针或引用。

// Returning a pointer or reference to derived
// type during overriding
#include  < iostream >
#include 
< string >
using   namespace  std;

class  PetFood
{
public :
          
virtual   string  foodType()  const   =   0 ;
};

class  Pet
{
public :
          
virtual   string  type()  const   =   0 ;
          
virtual  PetFood *  eats()  =   0 ;
};

class  Bird :  public  Pet
{
public :
           
string  type()  const  {  return   " Bird " ; }
          
class  BirdFood :  public  PetFood
          {
           
public :
                       
string  foodType()  const
                       {
                                 
return   " Bird food " ;
                        }
             };
            
// Upcast to base type:
            PetFood *  eats() {  return   & bf; }
private :
            BirdFood bf;
};

class  Cat :  public  Pet
{
public :
          
string  type()  const  {  return   " Cat " ; }
          
class  CatFood :  public  PetFood
          {
               
public :
                        
string  foodType()  const  {  return   " Cat food " ; }
           };
          
// Return exact type instread:
           CatFood *  eats() {  return   & cf; }
private :
           CatFood cf;
};

int  main()
{
           Bird b;
           Cat c;
           Pet
*  p[] = { & b, & c};
            
for ( int  i = 0 ;i < sizeof (p) / sizeof ( * p);i ++ )
                    cout
<< p[i] -> type() << "  eats  "
                           
<< p[i] -> eats() -> foodType() << endl;
            
// Can return the exact type:
             Cat::CatFood *  cf = c.eats();
             Bird::BirdFood
*  bf;
             
// Cannot return the exact type:
            
// bf=b.eats();
            
// Must downcast
            bf = dynamic_cast < Bird::BirdFood *> (b.eats());
}

        成员函数Pet::eats() 返回一个指向PetFood的指针,在Bird中,完全按基类中的形式重载这个成员函数,并且包含了返回类型。也就是说,Bird::eats() 把BirdFood向上类型转换到PetFood。

       但在Cat中,eats() 的返回类型是指向CatFood的指针,而CatFood是派生于PetFood的。编译它的唯一原因是,返回类型是从基类函数的返回类型中继承而来的。

       在main() 函数的后面,可以看出,Cat 返回的确切类型可以直接应用。而Bird 还需要进行向下类型转换。所以说,能返回确切的类型要更通用一些。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值