虽然很难找到一本不讨论多态性的C++书籍或杂志,但是,大多数这类讨论使多态性和C++虚函数的使用看起来很难。我打算在这篇文章中通过从几个方面和结合一些例子使读者理解在C++中的虚函数实现技术。说明一点,写这篇文章只是想和大家交流学习经验因为本人学识浅薄,难免有一些错误和不足,希望大家批评和指正,在此深表感谢!
一、 基本概念
首先,C++通过虚函数实现多态."无论发送消息的对象属于什么类,它们均发送具有同一形式的消息,对消息的处理方式可能随接手消息的对象而变"的处理方式被称为多态性。"在某个基类上建立起来的类的层次构造中,可以对任何一个派生类的对象中的同名过程进行调用,而被调用的过程提供的处理可以随其所属的类而变。"虚函数首先是一种成员函数,它可以在该类的派生类中被重新定义并被赋予另外一种处理功能。
二、 虚函数的定义与派生类中的重定义
01.
class
类名{
02.
public
:
03.
virtual
成员函数说明;
04.
}
05.
06.
class
类名:基类名{
07.
public
:
08.
virtual
成员函数说明;
09.
}
三、 虚函数在内存中的结构
1.我们先看一个例子:
01.
#include "iostream.h"
02.
#include "string.h"
03.
04.
class
A {
05.
public
:
06.
virtual
void
fun0() { cout <<
"A::fun0"
<< endl; }
07.
};
08.
09.
10.
int
main(
int
argc,
char
* argv[])
11.
{
12.
A a;
13.
cout <<
"Size of A = "
<<
sizeof
(a) << endl;
14.
return
0;
15.
}
结果如下:Size of A = 4
2.如果再添加一个虚函数:virtual void fun1() { cout << "A::fun" << endl;}
得到相同的结果。如果去掉函数前面的virtual修饰符
01.
class
A {
02.
public
:
03.
void
fun0() { cout <<
"A::fun0"
<< endl; }
04.
};
05.
06.
07.
int
main(
int
argc,
char
* argv[])
08.
{
09.
A a;
10.
cout <<
"Size of A = "
<<
sizeof
(a) << endl;
11.
return
0;
12.
}
结果如下:Size of A = 1
3.在看下面的结果:
01.
class
A {
02.
public
:
03.
virtual
void
fun0() { cout <<
"A::fun0"
<< endl; }
04.
int
a;
05.
int
b;
06.
};
07.
int
main(
int
argc,
char
* argv[])
08.
{
09.
A a;
10.
cout <<
"Size of A = "
<<
sizeof
(a) << endl;
11.
return
0;
12.
}
结果如下:Size of A = 12
其实虚函数在内存中结构是这样的:
图一
在window2000下指针在内存中占4个字节,虚函数在一个虚函数表(VTABLE)中保存函数地址。在看下面例子。
01.
class
A {
02.
public
:
03.
virtual
void
fun0() { cout <<
"A::fun0"
<< endl; }
04.
virtual
void
fun1() { cout <<
"A::fun1"
<< endl; }
05.
int
a;
06.
int
b;
07.
};
08.
int
main(
int
argc,
char
* argv[])
09.
{
10.
A a;
11.
cout <<
"Size of A = "
<<
sizeof
(a) << endl;
12.
return
0;
13.
}
结果如下:结果如下:
Size of A = 4
虚函数的内存结构如下,你也可以通过函数指针,先找到虚函数表(VTABLE),然后访问每个函数地址来验证这种结构,在国外网站作者是:Zeeshan Amjad写的"ATL on the Hood中有详细介绍"
图二
4.我们再来看看继承中虚函数的内存结构,先看下面的例子
01.
class
A {
02.
public
:
03.
virtual
void
f() { }
04.
};
05.
class
B {
06.
public
:
07.
virtual
void
f() { }
08.
};
09.
class
C {
10.
public
:
11.
virtual
void
f() { }
12.
};
13.
class
Drive :
public
A,
public
B,
public
C {
14.
};
15.
int
main() {
16.
Drive d;
17.
cout <<
"Size is = "
<<
sizeof
(d) << endl;
18.
return
0;
19.
}
结果如下:Size is = 12 ,相信大家一看下面的结构图就会很清楚,
图三
5.我们再来看看用虚函数实现多态性,先看个例子:
01.
class
A {
02.
public
:
03.
virtual
void
f() { cout <<
"A::f"
<< endl; }
04.
};
05.
class
B :
public
A{
06.
public
:
07.
virtual
void
f() { cout <<
"B::f"
<< endl;}
08.
};
09.
class
C :
public
A {
10.
public
:
11.
virtual
void
f() { cout <<
"C::f"
<< endl;}
12.
};
13.
class
Drive :
public
C {
14.
public
:
15.
virtual
void
f() { cout <<
"D::f"
<< endl;}
16.
};
17.
18.
int
main(
int
argc,
char
* argv[])
19.
{
20.
A a;
21.
B b;
22.
C c;
23.
Drive d;
24.
a.f();
25.
b.f();
26.
c.f();
27.
d.f();
28.
return
0;
29.
}
30.
结果:A::f
31.
B::f
32.
C::f
33.
D::f
不用解释,相信大家一看就明白什么道理!注意:多态不是函数重载
6.用虚函数实现动态连接在编译期间,C++编译器根据程序传递给函数的参数或者函数返回类型来决定程序使用那个函数,然后编译器用正确的的函数替换每次启动。这种基于编译器的替换被称为静态连接,他们在程序运行之前执行。另一方面,当程序执行多态性时,替换是在程序执行期进行的,这种运行期间替换被称为动态连接。如下例子:
01.
class
A{
02.
public
:
03.
virtual
void
f(){cout < <
"A::f"
< < endl;};
04.
};
05.
06.
class
B:
public
A{
07.
public
:
08.
virtual
void
f(){cout < <
"B::f"
< < endl;};
09.
};
10.
class
C:
public
A{
11.
public
:
12.
virtual
void
f(){cout < <
"C::f"
< < endl;};
13.
};
14.
void
test(A *a){
15.
a->f();
16.
};
17.
int
main(
int
argc,
char
* argv[])
18.
{
19.
B *b=
new
B;
20.
C *c=
new
C;
21.
char
choice;
22.
do
{
23.
cout< <
"type B for class B,C for class C:"
< < endl;
24.
cin>>choice;
25.
if
(choice==
''
b
''
)
26.
test(b);
27.
else
if
(choice==
''
c
''
)
28.
test(c);
29.
}
while
(1);
30.
cout< < endl< < endl;
31.
return
0;
32.
}
在上面的例子中,如果把类A,B,C中的virtual修饰符去掉,看看打印的结果,然后再看下面一个例子想想两者的联系。如果把B和C中的virtual修饰符去掉,又会怎样,结果和没有去掉一样。
7.在基类中调用继承类的函数(如果此函数是虚函数才能如此)
还是先看例子:
01.
class
A {
02.
public
:
03.
virtual
void
fun() {
04.
cout <<
"A::fun"
<< endl;
05.
}
06.
void
show() {
07.
fun();
08.
}
09.
};
10.
11.
class
B :
public
A {
12.
public
:
13.
virtual
void
fun() {
14.
cout <<
"B::fun"
<< endl;
15.
}
16.
};
17.
18.
int
main() {
19.
A a;
20.
a.show();
21.
22.
return
0;
23.
}
打印结果:A::fun
在6中的例子中,test(A *a)其实有一个继承类指针向基类指针隐式转化的过程。可以看出利用虚函数我们可以在基类调用继承类函数。但如果不是虚函数,继承类指针转化为基类指针后只可以调用基类函数。反之,如果基类指针向继承类指针转化的情况怎样,这只能进行显示转化,转化后的继承类指针可以调用基类和继承类指针。如下例子:
01.
class
A {
02.
public
:
03.
void
fun() {
04.
cout <<
"A::fun"
<< endl;
05.
}
06.
07.
};
08.
class
B :
public
A {
09.
public
:
10.
void
fun() {
11.
cout <<
"B::fun"
<< endl;
12.
}
13.
void
fun0() {
14.
cout <<
"B::fun0"
<< endl;
15.
}
16.
};
17.
int
main() {
18.
A *a=
new
A;
19.
B *b=
new
B;
20.
A *pa;
21.
B *pb;
22.
pb=
static_cast
(a);
//基类指针向继承类指针进行显示转化
23.
pb->fun0();
24.
pb->fun();
25.
return
0;
26.
}
参考资料:
1.科学出版社 《C++程序设计》
2.Zeeshan Amjad 《ATL on the Hood》
1.简介
虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次:
01.
class
A
02.
{
03.
public
:
04.
virtual
void
foo() { cout <<
"A::foo() is called"
<< endl;}
05.
};
06.
07.
class
B:
public
A
08.
{
09.
public
:
10.
virtual
void
foo() { cout <<
"B::foo() is called"
<< endl;}
11.
};
那么,在使用的时候,我们可以:
1.
A * a =
new
B();
2.
a->foo();
// 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:
01.
class
A
02.
{
03.
public
:
04.
virtual
void
foo();
05.
};
06.
07.
class
B:
public
A
08.
{
09.
virtual
void
foo();
10.
};
11.
12.
void
bar()
13.
{
14.
A a;
15.
a.foo();
// A::foo()被调用
16.
}
在了解了虚函数的意思之后,再考虑什么是多态就很容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:
1.
void
bar(A * a)
2.
{
3.
a->foo();
// 被调用的是A::foo() 还是B::foo()?
4.
}
因为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中的例子:
1.
void
bar(A * a)
2.
{
3.
a->foo();
4.
}
会被改写为:
1.
void
bar(A * a)
2.
{
3.
(a->vptr[1])();
4.
}
因为派生类和基类的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关键字考虑下面的类层次:
01.
class
A
02.
{
03.
public
:
04.
virtual
void
foo();
05.
};
06.
07.
class
B:
public
A
08.
{
09.
public
:
10.
void
foo();
// 没有virtual关键字!
11.
};
12.
13.
class
C:
public
B
// 从B继承,不是从A继承!
14.
{
15.
public
:
16.
void
foo();
// 也没有virtual关键字!
17.
};
这种情况下,B::foo()是虚函数,C::foo()也同样是虚函数。因此,可以说,基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字。
2.2 纯虚函数如下声明表示一个函数为纯虚函数:
1.
class
A
2.
{
3.
public
:
4.
virtual
void
foo()=0;
// =0标志一个虚函数为纯虚函数
5.
};
一个函数声明为纯虚后,纯虚函数的意思是:我是一个抽象类!不要把我实例化!纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,我的派生类都会有这个函数。
2.3 虚析构函数析构函数也可以是虚的,甚至是纯虚的。例如:
1.
class
A
2.
{
3.
public
:
4.
virtual
~A()=0;
// 纯虚析构函数
5.
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:
01.
class
A
02.
{
03.
public
:
04.
A() { ptra_ =
new
char
[10];}
05.
~A() {
delete
[] ptra_;}
// 非虚析构函数
06.
private
:
07.
char
* ptra_;
08.
};
09.
10.
class
B:
public
A
11.
{
12.
public
:
13.
B() { ptrb_ =
new
char
[20];}
14.
~B() {
delete
[] ptrb_;}
15.
private
:
16.
char
* ptrb_;
17.
};
18.
19.
void
foo()
20.
{
21.
A * a =
new
B;
22.
delete
a;
23.
}
在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?
如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。
纯虚的析构函数并没有什么作用,是虚的就够了。通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可以被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
2.4 虚构造函数?构造函数不能是虚的。
3. 虚函数使用技巧 3.1 private的虚函数考虑下面的例子:
01.
class
A
02.
{
03.
public
:
04.
void
foo() { bar();}
05.
private
:
06.
virtual
void
bar() { ...}
07.
};
08.
09.
class
B:
public
A
10.
{
11.
private
:
12.
virtual
void
bar() { ...}
13.
};
在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
这种写法的语意是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。
3.2 构造函数和析构函数中的虚函数调用一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。例如:
01.
class
A
02.
{
03.
public
:
04.
A() { foo();}
// 在这里,无论如何都是A::foo()被调用!
05.
~A() { foo();}
// 同上
06.
virtual
void
foo();
07.
};
08.
09.
class
B:
public
A
10.
{
11.
public
:
12.
virtual
void
foo();
13.
};
14.
15.
void
bar()
16.
{
17.
A * a =
new
B;
18.
delete
a;
19.
}
如果你希望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, GO