Function语意学
C++支持三种类型的memberfunctions:static、nonstatic和virtual,每一种类型被调用的方式都不相同。
Static函数有两个特点:它不能直接存取nonstatic数据;它不能被声明为const。
一Member的各种调用方式
1.NonstaticMember Functions
C++的设计准则之一就是:nonstaticmember function 至少必须和一般的nonmemberfunction有相同的效率。
如果normalize()是一个virtualmember function,那么一下调用:
ptr->normalize();
编译器的转化步骤为:
①改写函数的signature(函数原型)以安插一个额外的参数到memberfunction
中,用以提供一个存取管道,是classobject得意调用该函数。
②将对每一个“对nonstaticdata member的存取操作”改为经由this指针来存
取。
③将memberfunction重新写成一个外部函数。对函数名称进行“mangling”
处理,使它在程序中成为独一无二的语汇。
2.VirtualMember Functions
如果normalize()函数是一个virtualmember function,那么一下调用:
ptr->normalize()将被转化为(*ptr->vptr[1])(ptr);
其中:
n vptr表示有编译器产生的指针,指向virtual table。它被安插在每一个“声明有(或继承自)一个或多个virtualfunctions”的classobject中。事实上其名称也会被“mangled”,因为在一个复杂的class派生体系中,可能存在多个vptrs。
n 1是virtualtable slot的索引值,关联到normalize()函数。
n 第二个ptr表示this指针。
而对于调用:
obj.normalize();
编译器却没必要也不会把它转化为:
(*obj.vptr[1])(&obj);
而是会将它当作一般的nonstaticmember function一样决议:
normalize_7Point3dFV(&obj);
3.StaticMember Function
如果Point3d::normalize()是一个staticmember function,一下两个调用操作:
obj.normalize();
ptr->normalize();
将被转化为一般的nonmember函数调用,像这样:
//obj.normalize();
normalize_7Point3dSFV();
//ptr->normalize();
normalize_7Point3dSFV();
对于下面的写法:
((Point3d*)0)->object_count();
其中object_count()只是简单的传回_object_count这个staticdata member。
在引入staticmember functions之前,在c++语言要求所有的memberfunctions都必须经由该class的object来调用。而实际上,只有当一个或多个nonstaticdata members在memberfunction中被直接存取时,才需要class object。classobject提供了this指针给这种形式的函数调用使用。如果没有任何一个members被直接存取,事实上就不需要this指针,因此就没必要通过一个classobject来调用一个member function。
staticmember functions的主要特性是它没有this指针。一下的次要特性统统根源于其只要特性:
n 它不能够直接存取其class中的nonstaticmembers。
n 它不能够被声明为const、volatile或virtual(隐含函数中有nonstaticmembers)。
n 他不需要经由classobject才被调用——虽然大部分时候它是这样被调用的。
和staticmember data类似,如果取一个staticmember function的地址得到的是其在内存中的地址。如:
&Point3d::object_count();
会得到一个数值,类型是:
unsignedint (*)();
而不是:
unsignedint (Point3d::*)();
Static member function由于缺乏this指针,因此差不多等同于nonmemberfunction。它提供了一个意想不到的好处:成为一个callback函数。
二Virtual Member Functions
我们已经看过了virtualfunction的一般实现模型:每一个class有一个virtualtable,内含该class之中的有作用的virtualfunction的地址,然后每个object有一个vptr,指向virtualtable的所在。
1.单重继承下的virtualfunction
一个class只会有一个virtualtable(单重继承)。每一个对应的class object中所有的activevirtual function函数实体的地址。这些activevirtual function包括:
n 这个class所定义的函数实体。它会重写(overriding)一个可能存在的baseclass virtual function函数实体。
n 继承自baseclass的函数实体,这是在derived class决定不重写virtualfunction时才会出现。
n 一个pure_virtual_called()函数实体,它既可以扮演purevirtual function的空间保卫角色,也可以当作执行期异常处理函数。
每一个virtualfunction都被指派一个固定点索引值,这个索引在整个继承体系中保持与特定的virtualfunction的关联。例如在我们的Point class体系中:
classPoint
{
public:
virtual ~Point();
virtual Point& mult(float) = 0;
//......其他操作
float X() const { return _x;}
virtual float y() const { return 0; }
virtual float z() const { return 0; }
protected:
Point(float x = 0.0);
float _x;
};
内存布局如下:
当一个class派生自Point时,例如classPoint2d:
class Point2d : public Point
{
public:
Point2d(floatx = 0.0, float y = 0.0) : Point(x), _y(y) {}
~Point2d();
//重写baseclass virtual functions
Point2d&mult(float);
floaty() const { return _y;}
//......
protected:
float_y;
};
一共有三种可能性:
1) 它可以继承baseclass所声明的virtual functions的函数实体。正确的说,是该函数实体的地址会被拷贝到derivedclass的virtual table相对的slot中。
2) 它可以使用自己的函数实体(函数体重写)。这表示它自己的函数实体地址必须放在对应的slot之中。
3) 它可以加入新的virtualfunction。这时候virtual table的尺寸会增加一个slot,而新的函数实体地址会被放进该slot中。
Point2d的virtualtable在slot1中指出destructor,而在slot2中指出mult()(取代purevirtual function)。它自己的y()函数实体放在slot3。继承自Point的z()函数实体地址则放在slot4。
类似的情况,Point3d派生自Point2d,如下
classPoint3d : public Point2d
{
public:
Point2d(float x = 0.0, float y = 0.0,float z = 0.0)
: Point2d(x, y), _z(z) {}
~Point3d();
//重写baseclass virtual functions
Point3d& mult(float);
float z() const { return _z;}
//......
protected:
float _z;
};
其virtualtable中的slot1防止Point3d的destructor,slot放置Point3d::mult()函数地址。slot3放置继承自Point2d的y()函数地址,slot4放置自己的z()函数地址。
现在对于式子:
ptr->z();
那么,我们有足够的只是在编译时期设定virtualfunction的调用呢?
n 一般而言,我们并不知道ptr所指对象的真正类型。然而我知道,经由ptr可以存取到该对象的virtualtable
n 虽然不知道那个z()函数实体被调用,但我知道每一个z()函数地址都被放在slot4
唯一一个在执行期才能知道的东西是slot4所指到底是哪一个z()函数实体。
2.多重继承下的virtualfunction
在多重继承体系中支持virtualfunction,其复杂度围绕在第二个及后继的base class身上,以及“必须在执行期间调整this指针”这一点,一下class体系为例:
classBase1
{
public:
Base1();
virtual ~Base1();
virtual void speackClearly();
virtual Base1 *clone() const;
protected:
float data_Base1;
};
classBase2
{
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base2* clone() const;
protected:
float data_Base2;
};
classDerived: public Base1, public Base2
{
public:
Derived();
virtual ~Derived();
virtual Derived* clone() const;
protected:
float data_Derived;
};
“Derived支持virtual function”的困难度,统统落在Base2subobject身上,有三个问题需要解决,以此例而言分别是(1)virtualdestructor,(2)被继承下的Base2::mumble(),(3)一组clone()函数实体。
首先,我把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针
Base2* pbase2 = newDerived;
新的Derived对象的地址必须调整,以指向其Base2subobject。编译时会产生如下代码:
//转移以第二个baseclass
Derived* temp = newDerived;
Base2* pbase2 = temp? temp + sizeof(Base1) : 0;
如果没有这样的调整,指针的任何“非多态运用”(向下面那样)都将失败:
//即使pbase2被指定一个Derived对象,这也没问题
pbase2->data_Base2;
当程序员要删除pbase2所指的对象时:
//必须首先调用正确的virtualdestructor函数实体
//然后执行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
delete pbase2;
指针必须再一次被调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指的真正对象只有执行期才能确定。
delete操作带来的“必要的this指针调整”操作必须在执行期完成。
在多重继承下,一个derivedclass内含n-1个额外的virtualtable。n表示其上一层baseclass的数目(因此单一继承不会有额外的virtual table)。对于本例会有两个virtualtable被编译器产生出来:
(1) 一个主要实体,与Base1(最左端baseclass)共享
(2) 一个次要实体,与Base2(第二个baseclass)有关
多重继承下virtualtable的布局如下。
对于图中所说的三种情况如下:
(1) 通过一个“指向第二个baseclass”的指针,调用derived class virtual
function例如:
Base2* ptr = new Derived;
delete ptr;
从下图中,你可以看到调用操作的重点:ptr指向Derived对象的Base2subobject;为了能够正确执行,ptr必须调整指向Derived对象的起始地址。
(2) 通过一个“指向derivedclass”的指针,调用第二个base class中一个
继承而来的virtualfunction。自此情况下derived class指针必须再次调整,以指向第二个basesubobject。例如:
Derived* pder = new Derived;
//调用Base2::mumble();
//pder必须被向前调整sizeof(base1)个bytes
pder->mumble();
(3)第三种情况发生于一个语言扩充性质之下。详细略。
版权声明:本文为博主原创文章,未经博主允许不得转载。
原文:http://blog.csdn.net/lqlblog/article/details/46919491