虚函数

可用于实现多态公有继承的机制:

  • 在派生类中重新定义基类的方法。
  • 使用虚方法

类Brass和类BrassPlus如下所示:

class Brass
{
private:
char fullName[MAX];
long acctNum;
double balance;
public:
virtual void WithDraw(double amt);
virtual void ViewAcct()const;
virtual ~Brass(){}
};
class BrassPlus: public Brass
{
private:
double rate;
public:
virtual void ViewAcct()const;
virtual void WithDraw(double amt);
};

如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果ViewAcct()不是虚拟的,则程序的行为如下:

Brass dom("Dominic Banker",11224,4183.45);
BrassPlus dot("Dorothy Banker",12118,2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct();//调用Brass::ViewAcct()
b2_ref.ViewAcct();//调用Brass::ViewAcct()

如果ViewAcct是虚拟的,则行为如下:

1 Brass dom("Dominic Banker",11224,4183.45);
2 BrassPlus dot("Dorothy Banker",12118,2592.00);
3 Brass & b1_ref = dom;
4 Brass & b2_ref = dot;
5 b1_ref.ViewAcct();//调用Brass::ViewAcct()
6 b2_ref.ViewAcct();//调用BrassPlus::ViewAcct()

用途:

假设要同时管理Brass和BrassPlus账户,我们无法用同一个数组来保存Brass和BrassPlus对象,但是我们可以创建一个指向Brass的指针数组。这样,每个元素的类型都相同,而且Brass指针既可以纸箱Brass对象,也可以纸箱BrassPlus对象。因此,可以用一个数组来表示多种类型的对象。这就是多态性。

 虚函数的工作原理:

编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(vtbl)。虚函数表中存储了为类对象声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则虚函数的地址也将被添加到vtbl中。注意,无论类中包含多少个虚函数,都只要再对象中添加1个地址成员,只是表的大小不同而已。

使用虚函数时,在内存和执行速度方面有一定的成本,包括:

  • 每个对象都将增大,增大量为存储地址的空间。
  • 对每个类,编译器都创建一个虚函数地址表。
  • 每个函数调用都需要执行一步额外的操作,即到表中查找地址。

虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

虚函数要点:

  • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚拟的。
  • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而使用为引用或指针类型定义的方法。这称为动态联编。这种行为使得基类指针或引用可以指向派生类对象。
  • 如果定义的类将被用做基类,则应将那些要在派生类中重新定义的类方法声明为虚函数。

注意事项:

  1. 构造函数不能是虚函数。
  2. 析构函数应当是虚函数,除非类不用做基类。
  3. 友元不能是虚函数,因为友元不是类成员,而只有成员才能使虚函数。
  4. 如果派生类没有重新定义函数,将使用该函数的基类版本。
  5. 重新定义隐藏方法。

*Tip:通常应该给基类提供一个虚拟析构函数,即使它并不需要析构函数。

析构函数应当是虚函数,除非类不用做基类。例如假设给BrassPlus添加一个char*成员,该成员指向由new分配的内存。当BrassPlus对象过期时,必须调用~BrassPlus析构函数来释放内存。

请看下面的代码:

Brass* p = new BrassPlus;
delete p;

delete语句将调用~Brass()析构函数。这将释放由BrassPlus对象中的Brass部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚拟的,则上述代码将先调用~BrassPlus析构函数释放由BrassPlus组件指向的内存,然后调用~Brass()析构函数来释放由Brass组件指向的内存。这意味着,即使基类不需要显示析构函数提供服务,也不应该依赖于默认构造函数,而应提供虚拟析构函数,即使它不执行任何操作:

virtual ~Brass(){}

给类定义一个虚拟析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。

 *Tip:重新定义隐藏方法

假设创建了如下所示的代码:

class Dwelling
{
public:
virtual void showperks(int a)const;
...
};
class Hovel: public Dwelling
{
public:
virtual void showperks()const;
...
};

 这可能会出现类似于下面这样的警告:

Warning: Hovel::showperks(void) hides Dwelling::showperks(int)

也可能不会出现警告。但不管结果如何,代码将具有如下含义:

Hove trump;
trump.showperks();//valid
trump.showperks(5);//invalid

重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。也就是说重新定义方法并不是重载。派生类重新定义函数,将不是使用相同的函数特征覆盖基类声明,而是隐藏同名的基类函数,不管函数的参数特征如何。

这引出了两条经验规则:

第一,如果重新定义继承的方法,应确保与原来的原型完全相同。但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特征被称为返回类型协变。如:

class Dwelling
{
public:
virtual Dweling& build(int n);
...
};
class Hovel: public Dwelling
{
public:
virtual Hovel & build(int n);
...
};

 注意,这种例外只适用于返回值,而不适用于参数。

第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如:

 

class Dwelling
{
public:
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks()const;
...
};
class Hovel: public Dwelling
{
public:
virtual void showperks(int a)const;
virtual void showperks(double x)const;
virtual void showperks()const;
...
};

 如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。如果不需要修改,则新定义可调用基类版本。

posted @ 2015-07-12 23:33  Rosanne  阅读(269)  评论(0编辑  收藏  举报