c++ 虚函数 工作机制 原理( virtual function )
/*
*晚上花了几个小时翻译了下,第一次翻译这么长的文字;挺累呀,翻译的很多地方也不算通顺,权当自娱自乐了。
*版权所有 xt2120#gmail 谢绝转载
*/
c++ 虚函数 原理 机制 c 虚函数表 表指针
上个月,我介绍了虚拟函数。我概述了如何使用虚拟函数来实现一个设备无关的文件系统,并详细描述了如何创建一个具有多态行为的几何图形类。这个月我将继续解释虚拟函数的工作机制。首先,扼要重复一下其中的关键概念。
在c++中在基础类和基类之间的公共继承定义了一个is-a的关系。这就是说,给出了定义:
class D : public B {...}
D从B公共派生而来,所以任何D对象同时也是一个B对象。一个接收B对象指针或引用作为形式参数的函数也会允许一个指向D对象的指针(或引用,相应的)作为参数。更一般的情况,将一个指向D对象的指针转换为一个指向B对象的指针是一个标准转换而不需要cast。例如,如果d是一个类D的对象,B是D的公共继承类,你可以写下如下代码:
B *pb = &d;
这会将&d(一个 typde D*表达式)转换为B*.将一个D对象绑定到B&上也是一个标准转换(B & rb = d;)
which converts &d (an expression of type D * ) to B * . Binding a D object to a B & is also a standard conversion.当讨论指向基类和继承类对象的指针行为时,它将能够帮助你区分一个对象的动态类型和静态类型。一个对象的静态类型是用于引用那个对象的表达式。一个对象的动态类型是其“实际的”类型--当指针被创建时对象的类型。
Listing 3 Class circle derived form shape
比如,使用上面定义和初始化的pb,*pb的静态类型为B,但是其动态类型为D(B* pb = &d;)。或者考虑:
B &rb = d;
rb的静态类型为B同时其动态类型为D。*pb的静态类型总是B,但是其动态类型在程序执行的时候可能会发生变化。例如,如果b是一个B对象,那么
pb = &b;
会改变*pb的动态类型为B。
一个派生类继承了其基类的所有成员。一个派生类不能丢弃任何其所继承的成员,但是它能够使用覆写(override)一个继承的成员函数。
在c++中,一个非静态成员函数默认是非虚拟函数。c++通过静态绑定来解析一个非虚函数调用。也就是说,如果pb被声明为B*,并且B有一个非虚函数,那么pb->f总是调用B的函数f。即使在调用时pb实际指向的是一个D对象(此中D从B派生而来并且覆写了函数),那么调用pb->f仍旧调用的是隶属于B的函数f,而不是D的f。
从另一方面来说,虚拟成员函数调用是动态绑定的。如果pb是B*指针并且f在中被声明为一个虚拟成员函数,那么pb->f所实际调用的函数取决于*pb的动态类型。这样的话,如果pb实际指向的是B对象,那么pb->f调用的隶属于B的f。而如果pb实际是想一个D对象(D由B派生而来并且覆写了函数f),那么pb->f调用的是D的f。
一个至少有一个虚拟成员函数的类被称作多态类型,这样类型的对象们展示了多态性。多态能让你为逻辑上相似但实体行为不同的子类型继承体系定义统一的接口。通过使用多态,你能够将一个派生类对象指针或引用传递给只知其基类型对象的函数。对象仍将保持其动态类型以使得成员函数调用能够施行派生类的行为。
listing.1
class shape
{
public:
enum palette { BLUE, GREEN, RED };
shape(palette c);
virtual double area() const;
virtual const char *name() const;
virtual ostream &put(ostream &os) const;
palette color() const;
private:
palette_color;
static const char *color_image[RED - BLUE + 1];
};
inline ostream &operator<<(ostream &os, const shape &s)
{
return s.put(os);
}
// End of File
Listing 1显示了shape类定义,一个多态的几何类。
Listing 2 Member function and static member data definitions for class shape
shape::shape(palette c) : _color(c) { }
shape::palette shape::color( ) const
{
return _color;
}
double shape::area() const
{
return 0;
}
const char *shape::name() const
{
return "point";
}
ostream &shape::put(ostream &os) const
{
return os << color_image[_color] << '' << name();
}
const char *shape::color_image[shape::RED - shape::BLUE + 1] =
{ "blue", "green", "red" };
// End of File
Listing 2显示了相应的成员函数和静态成员函数定义。Class shape有三个虚拟函数,area,name 和put,两个非虚成员函数,color和shape(一个ctor)。
class circle : public shape
{
public:
circle(palette c, double r);
double area() const;
const char *name() const;
ostream &put(ostream &os) const;
private:
double radius;
};
circle::circle(palette c, double r) : shape(c), radius(r) { }
double circle::area() const
{
const double pi = 3.1415926;
return pi * radius * radius;
}
const char *circle::name() const
{
return "circle";
}
ostream &circle::put(ostream &os) const
{
return shape::put(os) << "with radius = " << radius;
}
// End of File
Listing 4 Class rectangle derived from shape
class rectangle : public shape
{
public:
rectangle(palette c, double h, double w);
double area() const;
const char *name() const;
ostream &put(ostream &os) const;
private:
double height, width;
};
rectangle::rectangle(palette c, double h, double w)
: shape(c), height(h), width(w) { }
double rectangle::area() const
{
return height * width;
}
const char *rectangle::name() const
{
return "rectangle";
}
ostream &rectangle::put(ostream &os) const
{
return shape::put(os) << " with height = " << height
<< " and width = " << width;
}
// End of File
Listing 3 和Listing 4相应的展示了类circle 和 rectangle完整定义,这两个类都从shape派生而来。每一个派生类定义了自己的构造函数,并且使用合适的定义将各自继承而来的虚函数覆写了。
Listing 5 A function that returns the shape with the largest area in a collection of shapes
const shape *largest(const shape *sa[], size_t n)
{
const shape *s = 0;
double m = 0;
double a;
for (size_t i = 0; i < n; ++i)
if ((a = sa[i]->area()) > m)
{
m = a;
s = sa[i];
}
return s;
}
// End of File
Listing 5包含了一个函数展示了多态的威力。函数largest从一个shapes集合中找到具有最大面积的shape。既然shape是多态类型,那么调用sa[i]->area不用调用者准确知晓*sa[i]实际的shape类型就能返回其面积。
vptrs and vtbls
ARM(Ellis)和新兴的c++标准都在竭力描述虚函数的行为特征,就如同其所描述的C++语言的其他部分,而没有没有建议具体的实现策略。但是,ARM里面在第十章的结尾派生类的评注中确实描述了实现技术。我觉得仰仗一个模型来实现简化了虚拟函数习性的细节描述。下面就是这样的一个模型。
典型的c++实现里为每一个多态类的对象增加了一个指针。这个指针被称作vptr。不论何时一个多态类的构造函数初始化一个对象时,它都会将对象的vptr设置为一个叫做vtbl的函数指针表的地址。vtbl中的每一个条目都一个虚函数的地址。一个既定类的所有对象都共享同样的vtbl;这个vtbl包含了包含了该类中的每一个虚函数的入口地址。
Figure 1 Layout of class shape
![]()
例如,上图显示了类shape的一个对象的布局(在listing 1中定义的)和其相应的vtbl。每一个shape对象都拥有同样顺序的两个值域:vptr和一个_colo 成员数据。vptr指向了shape的vtbl,包含了shape虚函数的地址(shape::area,shape::name和shape::put)。非虚函数shape::color和shape::shape(构造函数)不会在vtbl中占用任何空间,也不会占用对象本身的任何空间。Figure 2 Layout of class circle
![]()
Figure 3 Layout of class rectangle
![]()
图2和图3相应显示了circle和rectangle对象及其相关的vtbl的布局。注意到circle和reatangle对象起始一部分是shape对象,所以一个指向circle或者rectangle的指针同时也是一个指向shape的指针,同时从一个circle *或者ractangle*转换成shape* 不需要指针计算。两个派生类的vtbls和基类的vtbl拥有同样的函数指针序,尽管指针值是不同的。例如,area函数的vtbl入口在每一个从shape派生而来的类中都排在第一位。name的vtbl入口总在第二,put的入口序总为3。
然而一个非虚函数调用所产生的调用指令直接指向了在转换中(编译和链接)就已确定的地址,一个虚函数调用产生的额外的代码以定位vtbl中的函数地址。
ARM一书中建议将vtbl看作函数地址的一个队列,以使得每一次对被调用函数的定位能够vtbl的下标来定位。比如,如果ps只一个指向shape的指针,那么
a = ps->area();
会被转换成如下这个样子:
a = (*(ps->vtbl[0]))(ps);
同样
ps->put(cout);
会被转换成
(*(ps->vtbl[2]))(ps,cout);
形如ps->vtbl[n]的表达式就表示了*ps对象vtbl的入口,所以(*(ps->vtbl [n])) 就是第nth虚拟函数自身了。实际上,如同在c语言里一样,你不需要在调用表述里显式提领一个函数指针,你可以将
(*(ps->vtbl[2])) (ps, cout);
简化成
(ps->vtbl[2])(ps, cout);
每一个虚函数可能会有不同的签名式(形式参数类型次序)和返回类型返回类型。所以严格来讲,你不能将vtbls实现成函数数组,因为数组需要其所有类型都具有同样的类型。比如, shape::area 类型为 double (*)() , shape::put 类型为 void (*)(ostream &) .我宁愿将vtbl模塑为一个结构,其内所有的成员都是指向函数的指针。具体来讲,你可以为shape的vtbl定义如下的结构类型
struct shape_virtual_table
{
double (*area)();
const char *(*name)();
ostream &(*put)(ostream &os);
};
并且定义实际的shape vtbl如下:
shape_virtual_table shape_vtbl =
{
&shape::area,
&shape::name,
&shape::put
};
与此类似,你能够如下定义circle vtbl:
shape_virtual_table circle_vtbl =
{
&circle::area,
&circle::name,
&circle::put
};
(我说“类似这样”,因为这样的代码实际通不过编译,这样的代码只是演示下vtbl的通常的布局)使用这样的转换模式,
派生类或多或少或不覆写基类中的虚函数。一个派生类继承了其所未覆写的虚函数的定义。Listing 6 和 图4共同例示了选择性地覆盖基类中的虚函数的效果。
a = ps->area();
转换成
a = (*ps->vtbl->area)(ps);
或简化版
a = ps->vtbl->area(ps);
同样
ps->put(cout);
转换成
(*ps->vtbl->put)(ps, cout);
就是
ps->vtbl->put(ps, cout);
一次具有n个参数的虚拟函数调用将被转换成具有n+1个参数的调用(通过vtbl入口)。增加的那个参数就是被应用函数的对象的地址;在上例中,其值总是ps。在被调用函数中,这个额外的参数就成了this的值。虚函数不能为静态成员,所以他们总是隐式的有一个this参数。
记住我所描述的只是一种典型的实现策略。c++ translators 可能在实现虚函数时并不一致,但是效果是一样的。vptr并不需要在每个多态对象的开头。但是,对于任何从多态类型B派生而来的类D而言,D的vptr在D中的偏移量和B的vptr在B中的偏移量一致。类似地,vtbl中的函数指针的顺序也并不一定和类中声明的顺序一致,但对于任何任何从多态类B派生而来的D,D的vtbl的初始部分必须和B的vtbl的布局一致,即便因D已经覆写了某些所继承的虚函数而造成D的实际的指针值与B的不同。
简而言之,一个C++ TRANSLATOR必须确保派生对象的基子对象与任何基类型对象的布局一致,并且派生类型vtbl基portion部分和基类对象的vtbl一致。因此,当translator转换一个虚函数调用时不需要预见任何派生类的声明。不考虑p的动态类型,一个如p->f这样的虚函数调用总是转成这样的代码
构造f的实际实际参数列表
循p的vtpr到一个vtbl
将控制权移交给vtbl中相应指向f的入口地址。
所有既定多态类型的多态对象能够共享共同的vtbl实体。一些c++成功剔除了vtbls的重复。另一些产生多酚vtbls的拷贝,由于开发环境所限或者为提供更好的系统性能。许多实现提供了编译和链接选项让你自行作出决定。
这个实现模型展示了虚函数导入了小小的时间和空间上的代价:
为一个已有虚函数的类增加了一个或多个虚函数并没有为该类的每一个对象增加一个vptr。
每一个多态类增加了至少多增加了一个vtbl到程序数据区。
多态类的每一个构造函数必须初始化vptr
每一个虚函数调用必须定位通过查询vtbl以定位函数地址(通常需要2-4条额外的机器指令)
在c++中,成员函数默认为非虚的,因为c++坚持原则“不为不使用的部分付出代价”。如果你乐意承担虚函数调用的代价,你得明确的说出来。
选择性的覆写
Listing 6 Selective virtual overriding
#include <iostream.h>
class B
{
public:
virtual void f();
virtual void g();
virtual void h();
};
class C : public B
{
public:
void f(); // virtual
};
class D : public C
{
public:
void h(); // virtual
};
void B::f()
{
cout << "B::f()/n";
}
void B::g()
{
cout << "B::g()/n";
}
void B::h()
{
cout << "B::h()/n";
}
void C::f()
{
cout << "C::f()/n";
}
void D::h()
{
cout << "D::h()/n";
}
int main()
{
C c;
D d;
B *pb = &c; // ok, &c is a C * which is a B *
pb->f(); // calls C::f()
pb->g(); // calls B::g()
pb->h(); // calls B::h()
C *pc = &d; // ok, &d is a D * which is a C *
pc->f(); // calls C::f()
pc->g(); // calls B::g()
pc->h(); // calls D::h()
B &rb = *pc; // ok, *pc is a C which is a B
rb.f(); // calls C::f()
rb.g(); // calls B::g()
rb.h(); // calls D::h()
return 0;
}
// End of File
Figure 4 Selectively overriding only some virtual functions
![]()
listing 6 显示了一个简单的class 继承体系,图4显示了相应的vtbls。类B定义了三个虚函数f,g和h。由B派生而来的C只覆写了函数f,所以C的vtbl中的g和h的入口仍旧是B的g和h。由C派生的类D只覆写了函数h,所以D的vtbl中的f和g的入口和C的vtbl中的一致。既然C和D都没有覆写g,所有三个;类的vtbl对于g的入口都具有同样的值,也就是B'g。
在Listing 6中,pc有一个静态类型C*.但是当程序执行到这句前
B &rb = *pc;
pc的动态类型为D*。所以所有应用到rb上的调用都使用D的vtbl。
@IS2120#CNBLOGS.T2169364049[T1,L65,R1,V259]:备忘
$ € ₤ ₭ ₪ ₩ ₮ ₦ ₱ ฿ ₡ ₫ ﷼ ¥ ﷼ ₫ ₡ ฿ ₱ ₦ ₮ ₩ ₪ ₭ ₤ € $