1 .前言
虚函数是在c中实现多态性(polymorphism )的机制。 核心理念是通过基类访问派生类定义的函数。 假设有以下类级别:
类a
{
公共:
virtualvoidfoo ({ cout ' a :3360 foo ) ) is called' endl; }
(;
class b :公共a
{
公共:
virtualvoidfoo ({ cout ' b :3360 foo ) ) is called' endl; }
(;
那么,使用时,我们可以:
A * a=new B (;
a-foo (; //其中a是指向a的指针,但调用的函数(foo )是b!
这个例子是虚函数的典型应用,通过这个例子,你可能对虚函数有一些概念。 它等于所谓的“后内联编辑”和“动态内联编辑”,其中一个类函数的调用由执行时间而不是编译时间决定。 因为不知道在编写代码时调用基类的函数还是调用哪个派生类的函数,所以将其设置为“虚”函数。
虚函数只能通过指针或引用获得多态性的效果。 如果是以下代码,则是虚函数,但不是多态性。
类a
{
公共:
虚拟语音foo (;
(;
class b :公共a
{
虚拟语音foo (;
(;
void bar () )
{
A;
a.foo (; //A:foo ()被调用
}
1.1多态性
在理解虚函数意思的基础上,很容易思考什么是多态性。 上述类层次结构的情况也是如此,但使用起来很复杂。
语音信箱(a* a ) ) )。
{
a-foo (; //调用的是A:foo (还是B:foo )?
}
foo ) )是虚数函数,因此在名为bar的函数中,只有此代码不知道在此调用的是a:3360foo )还是b33603360foo ) ),但a是a类中的入
这种相同代码产生不同效果的特征被称为“多态性”。
1.2多态性有什么用?
多态性这么不可思议,能用来做什么? 虽然这个命题很难用一两句话来概括,但是一般的C教程(或者其他面向对象的语言教程)都用一个画画的例子来展示多态性的用途。 我不要重复这个例子。 如果不知道这个例子,找本书应该有很多介绍。 我试图从抽象的角度进行说明,但稍后再结合那幅画的例子,可能会让你更容易理解。
在面向对象的编程中,首先对数据进行抽象(基类的确定)和继承(派生类的确定),构成类层次结构。 如果这个类级别的用户在使用它们时,仍然在需要基类时编写面向基类的代码,而在需要派生类时编写面向派生类的代码,则类级别将完全暴露给用户。 如果对此类级别有任何更改(如果添加了新类),用户必须“知道”(为新类编写代码)。 由此,等级和其使用者的结合增加,也有人将其作为程序的“bad smell”之一来列举。
多态性可以使程序员摆脱这一困境。 回顾1.1的例子,bar ) )作为A-B类级别的用户,虽然不知道该类级别有多少个类,分别被称为什么,但是同样可以很好地发挥作用。 如果有从a类派生的c类(bar ) )也不需要“知道”)。 这多亏了多态性。 编译器为虚函数生成了代码,该代码可以确定在运行时调用的函数。
1.3如何“动态联编”
编译器如何生成代码来确定在可重用时间对虚函数调用的函数呢? 也就是说,虚函数实际上是如何由编译器处理的呢? Lippman在深度搜索c对象模型[1]的不同章节中阐述了几种方式,这里简要介绍“标准”方式。
我说的“标准”方式是所谓的“VTABLE”的结构。 当编译器发现类中有声明为virtual的函数时,它会为此创建一个虚函数表,即VTABLE。 VTABLE实际上是一个函数指针数组,每个虚函数占用此数组的slot。 一个类中无论有多少个实例,都只有一个VTABLE。 派生类具有自己的VTABLE,但派生类的VTABLE与基类的VTABLE具有相同的函数排序顺序,同名的虚函数位于两个数组中的相同位置。 创建类实例时,编译器会向每个实例的内存布局中添加指向该类的VTABLE的vptr字段。 这些方法允许编译器在看到虚函数调用时重写该调用,以涵盖1.1示例。
语音信箱(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来保存这个类层次中不同类的实例指针,那么你一定希望在对这个集合中的类进行操作的时候,不要把每个指针再cast回到它原来的类型(派生类),而是希望对他们进行同样的操作。那么就应该将这个“一样的操作”声明为virtual。
现实中,远不只我举的这两个例子,但是大的原则都是我前面说到的“如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的”。这句话也可以反过来说:“如果你发现基类提供了虚函数,那么你最好override它”。
4.参考资料
[1] 深度探索C++对象模型,Stanley B.Lippman,顺心的康乃馨译
[2] Design Patterns, Elements of Reusable Object-Oriented Software, GOF
欢迎大家阅读《C++中的虚函数(virtual function)_c语言》,跪求各位点评,若觉得好的话请收藏本文,by 搞代码
搞代码网(gaodaima.com)提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发送到邮箱[email protected],我们会在看到邮件的第一时间内为您处理,或直接联系QQ:872152909。本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:C++中的虚函数(virtual function)_c语言