虚函数

来源:互联网 发布:算法时代 编辑:程序博客网 时间:2024/05/30 23:27

虚函数

    虚函数就是在基类中说明为virtual 并在派生类中重定义的函数。为了说明一个函数为虚函数,需在其说明的前面加上关键字virtual。派生类中函数的重定义忽略基类中函数的定义。从根本上讲,基类中说明的虚函数在很大程度上扮演了说明一般类行为和规定接口的位置持有者角色。派生类对虚函数的重定义指明了函数(算法)执行的实际操作。换句话说,虚函数定义通用类,重定义虚函数实现具体算法。

    正常存取时,虚函数同任何其它类型的类成员函数完全一样。使虚函数显得如此重要且能支持运行时的多态性的原因,就在于当通过指针存取时它们的所作所为。一个基类指针能用来指向从该基类派生的任何类。当一个基类指针指向包含虚函数的派生对象时,c++根据该指针指向的对象的类型决定调用函数的哪一种形式。因此,当它指向不同的对象时,就执行不同形式的虚函数。

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!

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

虚函数的定义要遵循以下重要规则:

  1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。

  2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。

  3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。

  4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。

  5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。

  6.析构函数可以是虚函数,而且通常声名为虚函数。

 

深度探索虚函数(参考资料《深度探索C++对象模型》)

    编译期间的两个程序扩张操作(只要有一个class声明了一个或多个virtual functions就会如此):

 增加一个virtual function table(vtbl),内含每一个有作用的virtual function 的地址。

 将一个指向virtual function table 的指针(vptr),安插在每一个class object 内。

 

首先,我定义两个类,ZooAnimalBear:

Class ZooAnimal{

Public:

ZooAnimal();

Virtual ~ZooAnimal();

 

Virtual void animate();

Virtual void draw();

// ...

}

 

Class Bear :public ZooAnimal{

Public:

Bear();

Void animate();

Void draw();

Virtual void dance();

virtual table for bear

}

           

 

Bear::animate()

 

Bear::draw()

 

Bear::dance()

vptr

Bear yogi;

执行如下时虚表及指针:

Bear yogi;

Bear winnie = yogi;

 

 

vptr

Bear winnie=yogi;

 

 

 

 

 


ZooAnimal franny = yogi

 

 

 

每个类都有一个虚表,一个类的多个类对象共享一个虚表。

 

 

 

 

 

 

 

4.2  多态性

 

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

    // dynamic_poly.h 

    #include 

    // 公共抽象基类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 

    #include 

    #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 

    #include 

    #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()依据正被迭代的元素的类型而调用不同的成员函数。这从一个侧面体现了面向对象编程风格的优雅。

 

 

 

 

 

附加知识:细谈C++多态性的

在我们讨论多态的时候,先看看什么是硬编码和软编码:硬编码就是把代码写死了,导致弹性不足,降低了可扩展性,例如在代码里的

if...else...switch...case...

 

  这些代码通常都属于硬编码,项目中的这些代码多了,就相当于说明这个代码的灵活性、扩展性、弹性等等的少了。

 

  所以,我们要尽量使用软编码,通俗点就是别把话说死了,留点转弯的余地。多态性就是这种软编码特性的反映,下面我们一起来研究一下多态性。

 

  多态性是一种抽象,把事物的特征抽象出来,然后事物的具体形态我们就不关心了。

 

  例如对工人这种事物来说,他的特征就是工作,至于是什么工人,他做什么工作,我们就不用关心了,只要我们以工人.工作这种方式去调用。那他就会为我们工作了。

 

  那为什么我们不抽象出其他的特征,只抽象出工作这个特征呢?因为我们只对这个特征感兴趣,他的什么吃饭、睡觉、如厕等的特性我们都不关心了。有了多态,我们就可以实现软编码了!

 

  讲解了多态的概念之后,我们来看看多态的实现(C++的实现):

 

  多态的实现是通过虚函数表(VTable),每个类如果有虚函数,那它就有一个虚函数表,所有的对象都共享这一个VTable。这个概念也叫做动态联编,还有静态联编,这些概念都是通过在程序执行的时候表现出来的性质来定的,我们下面会看看它的究竟体现在哪里。

 

  先看一段代码:

 

class C0

...{

public:

void Test()

...{

cout << "call C0 Test()" << endl;

}

};

 

  这个类没有虚函数,调用的时候就是静态调用。调用的代码如下:

 

// 静态编译(早绑定 early binding

C0 *pO0;

C0 obj0;

pO0 = &obj0;

pO0->Test();

它的反汇编代码如下:

 

// 直接调用函数(已经知道地址)

00401432 mov ecx,dword ptr [ebp-0Ch]

00401435 call @ILT+160(C0::Test) (004010a5)

下面看看带虚函数的类:

 

class C1

...{

public:

virtual void Test()

...{

cout << "call C1 Test()" << endl;

}

};

 

class C11 : public C1

...{

public:

void Test()

...{

cout << "call C11 Test()" << endl;

}

};

 

  它的调用:

 

C11 obj11;

 

C1 *pObj1;

pObj1 = &obj11;

// 这里生成的汇编代码

// 0040144A lea edx,[ebp-14h] // 寻址找到pObj1

// 0040144D mov dword ptr [ebp-1Ch],edx

 

pObj1->Test();

// 这里生成的汇编代码

// 00401450 mov eax,dword ptr [ebp-1Ch] // 取得虚表地址

// 00401453 mov edx,dword ptr [eax]

// 00401455 mov esi,esp

// 00401457 mov ecx,dword ptr [ebp-1Ch] // 根据虚表的位置来取得Test()函数

// 0040145A call dword ptr [edx] // 调用Test()函数

 

  根据上述的汇编代码,我们可以知道,在多态调用函数的时候,程序执行以下步骤:

 

  1、寻址找到pObj1

 

  2、由于C11重载了Test虚函数,所以*pObj1指向的就是C11VTable的地址

 

  3、调用pObj1->Test()时,程序通过Vptr(虚表的指针,对象的首地址),找到VTable,再根据偏移调用Test函数。

 

  由于上述的多态调用过程是一个动态的过程(在运行时去函数来调用),而不是编译完就直接把函数地址摆在那里了,所以被称作动态联编

 

  上面把多态的的特点结合代码说了一遍,希望能说清楚了。

 

  下面再验证一个类的虚表的问题,如果你对虚表已经很熟悉了,就不用再往下看了。

 

  在很多书上都已经说明了C++的对象模型,这里只是做个验证。看看这段代码:

 

class C1

...{

public:

virtual void Test()

...{

cout << "call C1 Test()" << endl;

}

};

 

class C11 : public C1

...{

public:

void Test()

...{

cout << "call C11 Test()" << endl;

}

};

 

class C12 : public C1

...{

public:

void Test()

...{

cout << "call C12 Test()" << endl;

}

};

 

  我们可以知道 Test() 是虚函数,从C1派生的类必定有自己的虚表。而且根据别的资料,虚表指针是放在对象的首地址的,我们下面就来验证一下:

 

// 验证首地址

C11 obj110;

C11 obj111;

 

printf("obj110 的地址:%x ", &obj110);

printf("obj111 的地址:%x ", &obj111);

printf("obj110 虚表的地址:%x ", *(&obj110));

printf("obj111 虚表的地址:%x ", *(&obj111));

 

  结果是:

 

  obj110 的地址:12ff7c

  obj111 的地址:12ff78

  obj110 虚表的地址:432098

  obj111 虚表的地址:432098

 

  由上面的结果我们可以验证:

 

  1、一个类一个VTABLE,而不是一个对象一个VTABLE

 

  2、对象的首地址的内容就是VTABLE的地址。

 

  总结一下:

 

  C++的多态性包括其概念和实现,本文从编译器生成的代码来讨论C++多态特性,特别说明了为什么多态特性被称为动态联编,它和静态联编有什么不同,它们的体现在哪里。另外还对对象的虚表做了些验证。好了,希望本文能对你认识C++的多态性有一定的帮助!