`

C++中的虚函数

c++ 
阅读更多
原文出处:http://objects.nease.net/

先看代码
class A
{
public:
    void funPrint(){cout<<"funPrint of class A"<<endl;};
};

class B:public A
{
public:
    void funPrint(){cout<<"funPrint of class B"<<endl;};
};

void main()
{
    A *p; //定义基类的指针
    A a;
    B b;
    p=&a;
    p->funPrint();
    p=&b;
    p->funPrint();
}

大家以为这段代码的输出结果是什么?有的人可能会马上回答funPrint of class A 与 funPrint of class B 因为第一次输出是引用类A的实例啊,第二次输出是引用类B的实例啊。那么我告诉你这样想就错啦,答案是funPrint of class A 与 funPrint of class A 至于为什么输出这样的结果不在本文讨论的范围之内;你就记住,不管引用的实例是哪个类的当你调用的时候系统会调用左值那个对象所属类的方法。比如说 上面的代码类A B都有一个funPrint 函数,因为p是一个A类的指针,所以不管你将p指针指向类A或是类B,最终调用的函数都是类A的funPrint 函数。这就是静态联篇,编译器在编译的时候就已经确定好了。可是如果我想实现跟据实例的不同来动态决定调用哪个函数呢?这就须要用到虚函数(也就是动态联篇)

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的函数,就会为其搞一个虚函数表,也就是VTABLE。VTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个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 overload和override
    虚函数总是在派生类中被改写,这种改写被称为“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模式为例,Creator的factoryMethod()就是虚函数,派生类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
分享到:
评论

相关推荐

    C++虚函数实现原理

    ### C++虚函数实现原理 #### 一、引言 在C++中,虚函数机制是面向对象编程中实现多态的重要方式之一。通过虚函数,可以在基类中定义一个接口,并由派生类来具体实现该接口的功能,进而允许在运行时根据对象的实际...

    C++中虚函数的实现机制

    ### C++中虚函数的实现机制 #### 一、虚函数的概念及其重要性 在C++编程语言中,虚函数是实现多态性的关键机制之一。多态性是指同一个操作作用于不同的对象,可以有不同的解释,进而触发不同的行为。在面向对象...

    C++中虚函数的原理和作用

    "C++ 中虚函数的原理和作用" 在 C++ 语言中,虚函数(Virtual Function)是一种非常重要的机制,它允许在继承关系中实现多态性(Polymorphism)。虚函数的存在使得我们可以在不同的类中实现相同的接口,但是具有...

    c++ 对比虚函数的动态绑定

    在C++编程语言中,虚函数与动态绑定是面向对象编程中的重要概念,它们使得程序在运行时能够根据对象的实际类型来调用相应的成员函数,从而实现多态性。下面将详细解析C++中虚函数的动态绑定机制,以及如何通过示例...

    C++中虚函数工作原理和(虚)继承类的内存占用大小计算1

    在C++中,虚函数是实现多态性的重要机制,允许通过基类指针调用派生类重写的成员函数。虚函数的工作原理基于虚函数表(VTABLE)和虚指针(VPTR)。每当我们定义一个含有虚函数的类,编译器会自动生成一个虚函数表,...

    C++ 多态 虚函数 虚函数表 最是详细

    高质量的C++多态讲解,详细讲解虚函数,虚函数表,虚函数继承,虚函数继承下的内存分配等

    c++虚函数与虚函数表

    ### C++虚函数与虚函数表的理解 #### 一、虚函数的概念 在C++中,虚函数(Virtual Function)是一种特殊类型的成员函数,它允许基类指针或引用指向派生类对象,并通过该基类指针或引用调用派生类中重写的同名函数。...

    用C++实现虚函数

    虚函数是C++中面向对象编程的一个重要特性,它允许我们通过基类指针或引用调用派生类中的重写方法,实现多态性。本文将深入探讨如何使用C++实现虚函数,包括虚函数的基本用法、虚析构函数的概念以及如何计算类的大小...

    C++中虚函数和纯虚函数区别[归类].pdf

    C++虚函数和纯虚函数的区别 C++ 中的虚函数和纯虚函数是两种不同的函数声明方式,用于实现多态(polymorphism)机制。 虚函数 虚函数声明如下:virtual ReturnType FunctionName(Parameter) ;虚函数必须实现,如果...

    C++_虚函数表解析

    在 C++ 中,虚函数表是一个用于存储类中所有虚函数地址的数据结构。每个具有虚函数的类都有一个虚函数表,而每个该类的实例都包含一个指向该虚函数表的指针。当通过基类指针调用虚函数时,实际上是在查找虚函数表中...

    C++虚函数及虚函数表解析

    C++的虚函数和虚函数表是面向对象编程中实现多态性的重要机制。多态性允许通过基类指针或引用调用不同子类的重写方法,从而实现更灵活的设计和代码复用。 虚函数(Virtual Function)是基类中声明的一种特殊函数,...

    C++虚函数表解析

    ### C++虚函数表解析深度剖析 在C++编程语言中,**虚函数表**(Virtual Table,简称V-Table)是实现多态性的重要机制之一。多态性允许使用父类类型的指针或引用调用派生类的成员函数,从而达到在运行时根据对象的实际...

    C++中虚函数与纯虚函数的用法

    本文较为深入的分析了C++中虚函数与纯虚函数的用法,对于学习和掌握面向对象程序设计来说是至关重要的。具体内容如下: 首先,面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承、动态...

    C++中的虚函数表图解

    在C++中,虚函数是实现多态性的重要机制,多态是指使用父类型的指针或引用能够调用子类的成员函数,从而允许不同类型的对象以统一的方式进行处理。这种特性使得C++具备了泛型编程的能力,即用不变的代码解决可变的...

    C++虚函数.docx

    ### C++中的虚函数及其实现机制 #### 一、引言 C++作为一种支持面向对象编程的语言,提供了许多强大的特性,其中最重要的一个特性就是多态性。多态性允许我们编写更加灵活和可扩展的代码。在C++中,多态性的主要...

    C++ 虚函数表详解

    C++虚函数表详解 C++中的虚函数表是实现多态机制的关键组件。虚函数表(Virtual Table,简称V-Table)是一种机制,用于存储类的虚函数的地址,解决继承和覆盖的问题,使得父类的指针可以正确地调用子类的成员函数。...

Global site tag (gtag.js) - Google Analytics