C++多态技术

C++多态技术
作者:荣耀

摘要

本文描述了C++中的各种多态性。重点阐述了面向对象的动态多态和基于模板的静态多态,并初步探讨了两种技术的结合使用。

关键词

多态  继承  虚函数  模板    函数重载 泛型编程  泛型模式   

导言

多态(polymorphism)一词最初来源于希腊语polumorphos,含义是具有多种形式或形态的情形。在程序设计领域,一个广泛认可的定义是 一种将不同的特殊行为和单个泛化记号相关联的能力。和纯粹的面向对象程序设计语言不同,C++中的多态有着更广泛的含义。除了常见的通过类继承和虚函数机制生效于运行期的动态多态(dynamic polymorphism)外,模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为静态多态(static polymorphism)。

事实上,带变量的宏和函数重载机制也允许将不同的特殊行为和单个泛化记号相关联。然而,习惯上我们并不将它们展现出来的行为称为多态(或静态多态)。今天,当我们谈及多态时,如果没有明确所指,默认就是动态多态,而静态多态则是指基于模板的多态。不过,在这篇以C++各种多态技术为主题的文章中,我们首先还是回顾一下C++社群争论已久的另一种多态:函数多态(function polymorphism),以及更不常提的宏多态(macro polymorphism

函数多态

也就是我们常说的函数重载(function overloading)。基于不同的参数列表,同一个函数名字可以指向不同的函数定义:  

// overload_poly.cpp

#include <iostream>
#include <string>

// 定义两个重载函数

int my_add(int a, int b)
{
    return a + b;
}

int my_add(int a, std::string b)
{
    return a + atoi(b.c_str());
}

int main()
{
    int i = my_add(1, 2);                // 两个整数相加
    int s = my_add(1, "2");              // 一个整数和一个字符串相加
    std::cout << "i = " << i << "\n";
    std::cout << "s = " << s << "\n";
}

根据参数列表的不同(类型、个数或兼而有之),my_add(1, 2)my_add(1, "2")被分别编译为对my_add(int, int)my_add(int, std::string)的调用。实现原理在于编译器根据不同的参数列表对同名函数进行名字重整,而后这些同名函数就变成了彼此不同的函数。比方说,也许 某个编译器会将my_add()函数名字分别重整为my_add_int_int()my_add_int_str()

宏多态

带变量的宏可以实现一种初级形式的静态多态:  
// macro_poly.cpp

#include <iostream>
#include <string>

// 定义泛化记号:宏ADD
#define ADD(A, B) (A) + (B);

int main()
{
    int i1(1), i2(2);
    std::string s1("Hello, "), s2("world!");
    int i = ADD(i1, i2);                        // 两个整数相加
    std::string s = ADD(s1, s2);                  // 两个字符串相加
    std::cout << "i = " << i << "\n";
    std::cout << "s = " << s << "\n";
}
当程序被编译时,表达式ADD(i1, i2)ADD(s1, s2)分别被替换为两个整数相加和两个字符串相加的具体表达式。整数相加体现为求和,而字符串相加则体现为连接。程序的输出结果符合直觉:  
1 + 2 = 3
Hello, + world! = Hello, world!

动态多态

这就是众所周知的的多态。现代面向对象语言对这个概念的定义是一致的。其技术基础在于继承机制和虚函数。例如,我们可以定义一个抽象基类Vehicle和两个派生于Vehicle的具体类CarAirplane

// dynamic_poly.h

#include <iostream>

// 公共抽象基类Vehicle
class Vehicle
{
public:
    virtual void run() const = 0;
};

// 派生于Vehicle的具体类Car
class Car: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a car\n";
    }
};

// 派生于Vehicle的具体类Airplane
class Airplane: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a airplane\n";
    }
};
客户程序可以通过指向基类Vehicle的指针(或引用)来操纵具体对象。通过指向基类对象的指针(或引用)来调用一个虚函数,会导致对被指向的具体对象之相应成员的调用:

// dynamic_poly_1.cpp

#include <iostream>
#include <vector>
#include "dynamic_poly.h"

// 通过指针run任何vehicle
void run_vehicle(const Vehicle* vehicle)
{
    vehicle->run();            // 根据vehicle的具体类型调用对应的run()
}

int main()
{
    Car car;
    Airplane airplane;
    run_vehicle(&car);         // 调用Car::run()
    run_vehicle(&airplane);    // 调用Airplane::run()
}

此例中,关键的多态接口元素为虚函数run()。由于run_vehicle()的参数为指向基类Vehicle的指针,因而无法在编译期决定使用哪一个版本的run()。在运行期,为了分派函数调用,虚函数被调用的那个对象的完整动态类型将被访问。这样一来,对一个Car对象调用 run_vehicle(),实际上将调用Car::run(),而对于Airplane对象而言将调用Airplane::run()
或许动态多态最吸引人之处在于处理异质对象集合的能力:  

// dynamic_poly_2.cpp

#include <iostream>
#include <vector>
#include "dynamic_poly.h"

// run异质vehicles集合
void run_vehicles(const std::vector<Vehicle*>& vehicles)
{
    for (unsigned int i = 0; i < vehicles.size(); ++i)
    {
        vehicles[i]->run();     // 根据具体vehicle的类型调用对应的run()
    }
}

int main()
{
    Car car;
    Airplane airplane;
    std::vector<Vehicle*> v;    // 异质vehicles集合
    v.push_back(&car);
    v.push_back(&airplane);
    run_vehicles(v);            // run不同类型的vehicles
}
run_vehicles()中,vehicles[i]->run()依据正被迭代的元素的类型而调用不同的成员函数。这从一个侧面体现了面向对象编程风格的优雅。

静态多态

如果说动态多态是通过虚函数来表达共同接口的话,那么静态多态则是通过彼此单独定义但支持共同操作的具体类来表达共同性,换句话说,必须存在必需的同名成员函数。  
我们可以采用静态多态机制重写上一节的例子。这一次,我们不再定义vehicles类层次结构,相反,我们编写彼此无关的具体类CarAirplane(它们都有一个run()成员函数):  

// static_poly.h

#include <iostream>

//具体类Car
class Car
{
public:
    void run() const
    {
        std::cout << "run a car\n";
    }
};

//具体类Airplane
class Airplane
{
public:
    void run() const
    {
        std::cout << "run a airplane\n";
    }
};

run_vehicle()应用程序被改写如下:
  
// static_poly_1.cpp

#include <iostream>
#include <vector>
#include "static_poly.h"

// 通过引用而run任何vehicle
template <typename Vehicle>
void run_vehicle(const Vehicle& vehicle)
{
    vehicle.run();            // 根据vehicle的具体类型调用对应的run()
}
 
int main()
{
    Car car;
    Airplane airplane;
    run_vehicle(car);         // 调用Car::run()
    run_vehicle(airplane);    // 调用Airplane::run()
}
现在Vehicle用作模板参数而非公共基类对象(事实上,这里的Vehicle只是一个符合直觉的记号而已,此外别无它意)。经过编译器处理后,我们最 终会得到run_vehicle<Car>()run_vehicle<Airplane>()两个不同的函数。这和动态多态不同,动态多态凭借虚函数分派机制在运行期只有一个 run_vehicle()函数。  
我们无法再透明地处理异质对象集合了,因为所有类型都必须在编译期予以决定。不过,为不同的vehicles引入不同的集合只是举手之劳。由于无需再将集合元素局限于指针或引用,我们现在可以从执行性能和类型安全两方面获得好处:

// static_poly_2.cpp

#include <iostream>
#include <vector>
#include "static_poly.h"

// run同质vehicles集合
template <typename Vehicle>
void run_vehicles(const std::vector<Vehicle>& vehicles)
{
    for (unsigned int i = 0; i < vehicles.size(); ++i)
    {
        vehicles[i].run();            // 根据vehicle的具体类型调用相应的run()
    }
}

int main()
{
    Car car1, car2;
    Airplane airplane1, airplane2;

    std::vector<Car> vc;              // 同质cars集合
    vc.push_back(car1);
    vc.push_back(car2);
    //vc.push_back(airplane1);        // 错误:类型不匹配
    run_vehicles(vc);                 // run cars

    std::vector<Airplane> vs;         // 同质airplanes集合
    vs.push_back(airplane1);
    vs.push_back(airplane2);
    //vs.push_back(car1);             // 错误:类型不匹配
    run_vehicles(vs);                 // run airplanes
}

两种多态机制的结合使用  

在一些高级C++应用中,我们可能需要结合使用动态多态和静态多态两种机制,以期达到对象操作的优雅、安全和高效。例如,我们既希望一致而优雅地处理 vehiclesrun问题,又希望安全而高效地完成给飞行器(飞机、飞艇等)进行空中加油这样的高难度动作。为此,我们首先将上面的 vehicles类层次结构改写如下:  

// dscombine_poly.h

#include <iostream>
#include <vector>

// 公共抽象基类Vehicle
class Vehicle
{
    public:
    virtual void run() const = 0;
};

// 派生于Vehicle的具体类Car
class Car: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a car\n";
    }
};

// 派生于Vehicle的具体类Airplane
class Airplane: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a airplane\n";
    }
 
    void add_oil() const
    {
        std::cout << "add oil to airplane\n";
    }
};

// 派生于Vehicle的具体类Airship
class Airship: public Vehicle
{
public:
    virtual void run() const
    {
        std::cout << "run a airship\n";
    }
  
    void add_oil() const
    {
        std::cout << "add oil to airship\n";
    }
};

我们理想中的应用程序可以编写如下:
  
// dscombine_poly.cpp

#include <iostream>
#include <vector>
#include "dscombine_poly.h"

// run异质vehicles集合
void run_vehicles(const std::vector<Vehicle*>& vehicles)
{
    for (unsigned int i = 0; i < vehicles.size(); ++i)
    {
        vehicles[i]->run();                 // 根据具体的vehicle类型调用对应的run()
    }
}

// 为某种特定的aircrafts同质对象集合进行空中加油
template <typename Aircraft>
void add_oil_to_aircrafts_in_the_sky(const std::vector<Aircraft>& aircrafts)
{
    for (unsigned int i = 0; i < aircrafts.size(); ++i)
    {
        aircrafts[i].add_oil();
    }
}

int main()
{
    Car car1, car2;
    Airplane airplane1, airplane2;

    Airship airship1, airship2;
    std::vector<Vehicle*> v;                // 异质vehicles集合
    v.push_back(&car1);
    v.push_back(&airplane1);
    v.push_back(&airship1);
    run_vehicles(v);                        // run不同种类的vehicles

    std::vector<Airplane> vp;               // 同质airplanes集合
    vp.push_back(airplane1);
    vp.push_back(airplane2);
    add_oil_to_aircrafts_in_the_sky(vp);    // airplanes进行空中加油

    std::vector<Airship> vs;                // 同质airships集合
    vs.push_back(airship1);
    vs.push_back(airship2);
    add_oil_to_aircrafts_in_the_sky(vs);    // airships进行空中加油
}

我们保留了类层次结构,目的是为了能够利用run_vehicles()一致而优雅地处理异质对象集合vehiclesrun问题。同时,利用函数模板 add_oil_to_aircrafts_in_the_sky<Aircraft>(),我们仍然可以处理特定种类的vehicles — aircrafts(包括airplanesairships)的空中加油问题。其中,我们避开使用指针,从而在执行性能和类型安全两方面达到了预 期目标。
  
结语 

长期以来,C++社群对于多态的内涵和外延一直争论不休。在comp.object这样的网络论坛上,此类话题争论至今仍随处可见。曾经有人将动态多态(dynamic polymorphism)称为inclusion polymorphism,而将静态多态(static polymorphism)称为parametric polymorphismparameterized polymorphism。  

我注意到2003年斯坦福大学公开的一份C++ and Object-Oriented Programming教案中明确提到了函数多态概念:Function overloading is also referred to as function polymorphism as it involves one function having many forms。文后的参考文献单元给出了这个网页链接。
  
可能你是第一次看到宏多态(macro polymorphism)这个术语。不必讶异 也许我就是造出这个术语的第一人。显然,带变量的宏(或类似于函数的宏或伪函数宏)的替换机制除了免除小型函数的调用开销之外,也表现出了类似的多态 性。在我们上面的例子中,字符串相加所表现出来的符合直觉的连接操作,事实上是由底部运算符重载机制(operator overloading)支持的。值得指出的是,C++社群中有人将运算符重载所表现出来的多态称为ad hoc polymorphism。  

David VandevoordeNicolai M. Josuttis在他们的著作C++ Templates: The Complete Guide一书中系统地阐述了静态多态和动态多态技术。因为认为和其他语言机制关系不大,这本书没有提及宏多态(以及函数多态)。(需要说明的是,笔者本人是这本书的繁体中文版译者之一,本文正是基于这本书的第14The Polymorphic Power of Templates编写而成)

动态多态只需要一个多态函数,生成的可执行代码尺寸较小,静态多态必须针对不同的类型产生不同的模板实体,尺寸会大一些,但生成的代码会更快,因为无需通 过指针进行间接操作。静态多态比动态多态更加类型安全,因为全部绑定都被检查于编译期。正如前面例子所示,你不可将一个错误的类型的对象插入到从一个模板 实例化而来的容器之中。此外,正如你已经看到的那样,动态多态可以优雅地处理异质对象集合,而静态多态可以用来实现安全、高效的同质对象集合操作。

静态多态为C++带来了泛型编程(generic programming)的概念。泛型编程可以认为是组件功能基于框架整体而设计的模板编程。STL就是泛型编程的一个典范。STL是一个框架,它提 供了大量的算法、容器和迭代器,全部以模板技术实现。从理论上讲,STL的功能当然可以使用动态多态来实现,不过这样一来其性能必将大打折扣。  

静态多态还为C++社群带来了泛型模式(generic patterns)的概念。理论上,每一个需要通过虚函数和类继承而支持的设计模式都可以利用基于模板的静态多态技术(甚至可以结合使用动态多态和静态多态两种技术)而实现。正如你看到的那样,Andrei Alexandrescu的天才作品Modern C++ Design: Generic Programming and Design Patterns AppliedAddison-Wesley)和Loki程序库已经走在了我们的前面。

参考文献
1. David Vandevoorde, Nicolai M. Josuttis, C++ Templates: The Complete Guide, Addison Wesley, 2002.
2. Chris Neumann, CS193d (Summer 2003) C++ and Object-Oriented Programming, http://www.stanford.edu/class/cs193d/, 2003.


类的多态特性是支持面向对象的语言最主要的特性,有过非面向对象语言开发经历的人,通常对这一章节的内容会觉得不习惯,因为很多人错误的认为,支持类的封装的语言就是支持面向对象的,其实不然,Visual BASIC 6.0 是典型的非面向对象的开发语言,但是它的确是支持类,支持类并不能说明就是支持面向对象,能够解决多态问题的语言,才是真正支持面向对象的开发的语言,所以务必提醒有过其它非面向对象语言基础的读者注意! 

  多态的这个概念稍微有点模糊,如果想在一开始就想用清晰用语言描述它,让读者能够明白,似乎不太现实,所以我们先看如下代码:

//例程1  
#include <iostream>     
using namespace std;   
   
class Vehicle 
{   
public:   
    Vehicle(float speed,int total) 
    { 
        Vehicle::speed=speed; 
        Vehicle::total=total; 
    } 
    void ShowMember() 
    { 
        cout<<speed<<"|"<<total<<endl; 
    } 
protected:   
    float speed; 
    int total; 
};   
class Car:public Vehicle   
{   
public:   
    Car(int aird,float speed,int total):Vehicle(speed,total)   
    {   
        Car::aird=aird;   
    } 
    void ShowMember() 
    { 
        cout<<speed<<"|"<<total<<"|"<<aird<<endl; 
    } 
protected:   
    int aird; 
};   
 
void main()   
{   
    Vehicle a(120,4); 
    a.ShowMember(); 
    Car b(180,110,4); 
    b.ShowMember(); 
    cin.get(); 
}

   在c++中是允许派生类重载基类成员函数的,对于类的重载来说,明确的,不同类的对象,调用其类的成员函数的时候,系统是知道如何找到其类的同名成员, 上面代码中的a.ShowMember();,即调用的是Vehicle::ShowMember(),b.ShowMember();,即调用的是 Car::ShowMemeber();。


1.简介
    虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:

class A
{
public:
    virtual void foo() { cout << "A::foo() is called" << endl;}
};

class B: public A
{
public:
    virtual void foo() { cout << "B::foo() is called" << endl;}
};

那么,在使用的时候,我们可以:

A * a = new B();
a->foo();       // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B!

    这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓推迟联编或者动态联编上,一个类函数的调 用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为函数。

    虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:

class A
{
public:
    virtual void foo();
};

class B: public A
{
    virtual void foo();
};

void bar()
{
    A a;
    a.foo();   // A::foo()被调用
}

1.1 多态
    在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:

void bar(A * a)
{
    a->foo();  // 被调用的是A::foo() 还是B::foo()
}

因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。

这种同一代码可以产生不同效果的特点,被称为多态

1.2 多态有什么用?
    多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,我就不再重复这个例子了,如果你不知道这个例子,随便找本书应该都有介绍。我试图从一个抽象的角度描述一下,回头再结合那个画图的例子,也许你就更容易理解。

    在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需 要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类), 都需要使用者知道(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合,有人把这种情况列为程序中的“bad smell”之一。

    多态可以使程序员脱离这种窘境。再回头看看1.1中的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类 都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要知道(修改)。这完全归功于多态--编译器针对虚函数产生了可 以在运行时刻确定被调用函数的代码。

1.3 如何动态联编
    编译器是如何针对虚函数产生可以再运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在深度探索C++对象模型[1]中的不同章节讲到了几种方式,这里把标准的方式简单介绍一下。

    我所说的标准方式,也就是所谓的“VTABLE”机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是 VTABLEVTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生 类有自己 VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译 器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改 写,针对1.1中的例子:

void bar(A * a)
{
    a->foo();
}

会被改写为:

void bar(A * a)
{
    (a->vptr[1])();
}

    因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。

    虽然实际情况远非这么简单,但是基本原理大致如此。

1.4 overloadoverride
    虚函数总是在派生类中被改写,这种改写被称为“override”。我经常混淆“overload”“override”这两个单词。但是随着各类C++的书越来越多,后来的程序员也许不会再犯我犯过的错误了。但是我打算澄清一下:

override是指派生类重写基类的虚函数,就象我们前面B类中重写了A类中的foo()函数。重写的函数必须有一致的参数表和返回值(C++标准允许 返回值不同的情况,这个我会在语法部分简单介绍,但是很少编译器支持这个feature)。这个单词好象一直没有什么合适的中文词汇来对应,有人译为 覆盖,还贴切一些。
overload约定成俗的被翻译为重载。是指编写一个与已有函数同名但是参数表不同的函数。例如一个函数即可以接受整型数作为参数,也可以接受浮点数作为参数。
2. 虚函数的语法
    虚函数的标志是“virtual”关键字。

2.1 使用virtual关键字
    考虑下面的类层次:

class A
{
public:
    virtual void foo();
};

class B: public A
{
public:
    void foo();    // 没有virtual关键字!
};

class C: public B  // B继承,不是从A继承!
{
public:
    void foo();    // 也没有virtual关键字!
};

    这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。

2.2 纯虚函数
    如下声明表示一个函数为纯虚函数:

class A
{
public:
    virtual void foo()=0;   // =0标志一个虚函数为纯虚函数
};

    一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的接口。它告诉使用者,我的派生类都会有这个函数。

2.3 虚析构函数
    析构函数也可以是虚的,甚至是纯虚的。例如:

class A
{
public:
    virtual ~A()=0;   // 纯虚析构函数
};

    当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:

class A
{
public:
    A() { ptra_ = new char[10];}
    ~A() { delete[] ptra_;}        // 非虚析构函数
private:
    char * ptra_;
};

class B: public A
{
public:
    B() { ptrb_ = new char[20];}
    ~B() { delete[] ptrb_;}
private:
    char * ptrb_;
};

void foo()
{
    A * a = new B;
    delete a;
}

    在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?

    如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。

    纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。

2.4 虚构造函数?
    构造函数不能是虚的。

3. 虚函数使用技巧 3.1 private的虚函数
    考虑下面的例子:

class A
{
public:
    void foo() { bar();}
private:
    virtual void bar() { ...}
};

class B: public A
{
private:
    virtual void bar() { ...}
};

    在这个例子中,虽然bar()A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函 数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()A::bar()override不起作用的情况。

    这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。

3.2 构造函数和析构函数中的虚函数调用
    一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不了。也就是说不能在构造函数和析构函数中让自己多态。例如:

class A
{
public:
    A() { foo();}        // 在这里,无论如何都是A::foo()被调用!
    ~A() { foo();}       // 同上
    virtual void foo();
};

class B: public A
{
public:
    virtual void foo();
};

void bar()
{
    A * a = new B;
    delete a;
}

    如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()

3.3 多继承中的虚函数 3.4 什么时候使用虚函数
    在你设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。

    以设计模式[2]Factory Method模式为例,CreatorfactoryMethod()就是虚函数,派生类override这个函数后,产生不同的Product类,被 产生的Product类被基类的AnOperation()函数使用。基类的AnOperation()函数针对Product类进行操作,当然 Product类一定也有多态(虚函数)。

    另外一个例子就是集合操作,假设你有一个以A类为基类的类层次,又用了一个std::vector<A *>来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生 类),而是希望对他们进行同样的操作。那么就应该将这个一样的操作声明为virtual

    现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。这句话也可以反过来说:如果你发现基类提供了虚函数,那么你最好override

4.参考资料
[1] 深度探索C++对象模型,Stanley B.Lippman,侯捷译

[2] Design Patterns, Elements of Reusable Object-Oriented Software, GOF


评论