四. 抽象基类和纯虚函数
我们在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口 ,而不希望用户实际地创建一个基类的对象。要做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),来是基类成为抽象类(abstract)。纯虚函数使用关键字virtual ,并且在其后面加上 = 0 。如果某人试着生成一个抽象类的对象,编译器会制止他。
当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。建立公共接口的唯一原因是它能对于每个不同的子类有不同的表示。它建立一个基本的格式,用来确定什么是对于所有派生类是公共的——除此之外,别无用途。
#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) 的一种方法。通过抽象类,可以保证在向上类型转换期间总是使用指针或引用。
在基类中,对纯虚函数提供定义是可能的。这只是我们可能希望有一段公共代码,使一些派生类定义都能调用,而不必在每个函数中重复这段代码。不过纯虚函数不允许内联。
#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中总有一个函数地址的全集,所以绝对不能对不在其中的地址进行调用。
但是在派生类中继承或增加新的虚函数时会发生什么呢?看下面这个例子:
#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 < 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() 函数。按值传递的时候,编译器强迫派生类的对象变为基类的对象,这里调用了基类的拷贝构造函数。去掉了派生类中多于的部分,保留了基类的部分。所以,在函数传递参数的过程中,应该尽量避免对象切片。
六. 重载和重新定义
先看下面的例程:
#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中,我们不能在重新定义中修改虚函数的返回类型,但我们可以把它改成返回基类的指针或引用,则改函数的重新定义版本将会从基类返回的内容中返回一个指向派生类的指针或引用。
// 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 还需要进行向下类型转换。所以说,能返回确切的类型要更通用一些。