[读书笔记] - 《深度探索C++对象模型》第4章 Function语意学

Table of Contents

1. Member的各种调用方式

1.1 Nonstatic Member Functions

1.2 名称的特殊处理(Name Mangling)

1.3 Virtual Member Functions

1.4 Static Member Functions

2. Virtual Member Functions

2.1 单继承下的Virtual Functions

2.2 多重继承下的Virtual Functions

2.3 虚拟继承下的Virtual Functions

3. 其他


1. Member的各种调用方式

C++支持三种类型的member functions: static、nonstatic和virtual,每一种类型被调用的方式都不相同。

1.1 Nonstatic Member Functions

C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。下面是一个nonmember function的定义:

float magnitude3d(const Point3d* _this)
{
    return sqrt( _this->_x * _this->_x +
                 _this->_y * _this->_y +
                 _this->_z * _this->_z );
}

乍见之下似乎nonmember function比较没有效率,它间接地经由参数取用坐标成员,而member function确是直接取用坐标成员。然而实际上member function被内化为nonmmeber的形式。下面就是转化步骤:

1>改写函数的signature以安插一个额外的参数到member function中,用以提供一个存取管道,使class object得以调用该函数。该额外参数被称为this指针:

// non-const nonstatic member的增长过程
Point3d Point3d::magnitude(Point3d* const this)

如果member function是const,则变成:

// const nonstatic member的增长过程
Point3d Point3d::magnitude(const Point3d* const this)

2>将每一个“对nonstatic data member的存取操作”改为经由this指针来存取:

{
    return sqrt( _this->_x * _this->_x +
                 _this->_y * _this->_y +
                 _this->_z * _this->_z );
}

3>将member function重新写成一个外部函数。对函数名称进行“mangling”处理,使它在程序中成为独一无二的语汇:

extern magnitude__7Point3dFv(register Point3d* const this);

现在这个函数已经被转换好了,而其每一个调用操作也都必须替换:

obj.magnitude();
==> magnitude__7Point3dFv( &obj );

ptr->magnitude();
==> magnitude__7Point3dFv( ptr );

在函数内直接return创建的对象比先创建,再return有效率:

Point3d Point3d::normalize() const
{
    Point3d normal:

    normal._x = _x/2;
    normal._y = _y/2;
    normal._z = _z/2;

    return normal;
}

// 直接建构“normal”值比较有效率
Point3d Point3d::normalize() const
{
    return Point3d( _x/2, _y/2, _z/2 );
}

1.2 名称的特殊处理(Name Mangling)

class member的名称前面会被加上class名称,形成独一无二的命名。

class Bar { public: int ival; ... }
class Foo : public { public: int ival; ... }

// Foo 的内部描述
class Foo
{
public:
    int ival_3Bar;
    int ival_3Foo;
    ...
};

不管你要处理哪一个ival,通过“name mangling”,都可以绝对清楚地指出来。由于member functions可以被重载,所以需要更广泛的mangling手法,以提供绝对独一无二的名称。

class Point
{
public:
    void x(float newX);
    float x();
    ...
};

//内部描述
class Point
{
public:
    void x__5PointFf(float newX);
    float x__5PointFv();
};

两个实体如果拥有独一无二的name mangling,那么任何不正确的调用操作在链接时期就因无法决议(resolved)而失败。但是它只可以捕捉函数signature(函数名称+参数数目+参数类型)错误;如果“返回类型”声明错误,就没办法检查出来。

1.3 Virtual Member Functions

如果normalize()是一个virtual member function,那么以下的调用:

ptr->normalize();

将会被内部转化为:

(*ptr->vptr[1])(ptr);

其中:

1>vptr表示由编译器产生的指针,指向virtual table。事实上,其名称也会被“mangled”,因为在一个复杂的class派生体系中,可能存在有多个vptrs。

2>1是virtual table slot的索引值,关联到normalize()函数。

3>第二个ptr表示this指针。

使用class scope operator明确调用一个virtual funciton,其决议(resolved)方式会和nonstatic member function一样:

//明确地调用操作会压制虚拟机制
register float mag = Point3d::magnitude();
<==> register float mag = magnitude__7Point3dFv(this);

1.4 Static Member Functions

如果Point3d::normalize()是一个static member function,以下两个调用操作将被转换为一般的nonmember函数调用:

obj.normalize();
==> normalize__7Point3dSFv();

ptr->normalize();
==> normalize__7Point3dSFv();

在引入static member functions之前,C++语言要求所有的member functions都必须经由该class的object来调用。而实际上,只有当一个或多个nonstatic data members在member function中被直接存取时,才需要class object。Class object提供了this指针给这种形式的函数调用使用。这个this指针把“在member function中存取的nonstatic class members”绑定于“object内对应的members”之上。如果没有任何一个members被直接存取,事实上就不需要this指针,因此也就没有必要通过一个class object来调用一个member function。不过C++语言到当前为止并不能够识别这种情况。

如果class的设计者把static data member声明为nonpublic(这一直被视为是一种好的习惯),那么他就必须提供一个或多个member functions来存取该member。因此,虽然你可以不靠class object来存取一个static member,但其存取函数却得绑定于一个class object之上。

static member functions的主要特性就是没有this指针。以下的次要特性统统根源于其主要特性:

1>它不能直接存取其class中的nonstatic members;

2>它不能被声明为const、volatile或virtual;

3>它不需要经由class object才被调用——虽然大部分时候它是这样被调用的。

static member function由于缺乏this指针,因此差不多等同于nonmember function。

2. Virtual Member Functions

在C++中,多态(polymorphism)表示“以一个public base class的指针(或reference),寻址出一个derived class object”的意思。

在C++中,virtual functions可以在编译时期获知,此外,这一组地址是固定不变的,执行期不可能新增或替换它。由于程序执行时,表格的大小和内容都不会改变,所以其建构和存取皆可以由编译器完全掌握,不需要执行期的任何介入。

为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格。为了找到函数地址,每一个virtual function被指派一个表格索引值。

这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot中激活virtual function。

2.1 单继承下的Virtual Functions

单继承下,一个class只会有一个virtual table。每一个table内含其对应的class object中所有active virutal functions函数实体的地址。这些active virtual functions包括:

1>这个class所定义的函数实体。它会改写(overriding)一个可能存在的base class virtual function函数实体;

2>继承自base class的函数实体。这是在derived class决定不改写virtual function时才会出现的情况;

3>一个pure_virtual_called()函数实体,它既可以扮演pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数(有时候会用到)。

每一个virtual function都被指派一个固定的索引值,这个索引在整个继承体系中保持与特定的virtual function的关联。

class Point
{
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 Point2d : public Point
{
public:
    Point2d(float x = 0.0, float y = 0.0)
        : Point(x), _y(y) { }
    ~Point2d();

    //改写base class virtual functions
    Point2d& mult(float);
    float y() const { return _y; }
protected:
    float _y;
};
    
class Point3d : public Point2d
{
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
        : Point2d(x, y), _z(z) { }
    ~Point3d();

    //改写base class virtual functions
    Point3d& mult(float);
    float z() const { return _z; }

protected:
    float _z;
};

对于class Point,virtual destructor被赋值slot 1,而mult()被赋值slot2。此例并没有mult()的函数定义,所以pure_virtual_called()的函数地址会被放在slot2中。如果该函数被意外地被调用,通常的操作是结束掉这个程序。

class Point2d继承自class Point,Point2d的virtual table在slot1中指出destructor,而在slot2中指出mult()(取代pure virtual function)。它自己的y()函数实体地址放在slot3,继承自Point的z()函数实体地址则放在slot4。

现在,如果我有这样的式子:

ptr->z();

那么如何有足够的知识在编译时期设定virtual function的调用呢?

1>一般而言,我并不知道ptr所指对象的真正类型。然而我知道,经由ptr可以存取到该对象的virtual table;

2>虽然我不知道哪一个z()函数实体会被调用,但我知道每一个z()函数地址都被放在slot4。

这些信息使得编译器可以将该调用转化为:

(*ptr->vptr[4])(ptr);

在这个转化中,vptr表示编译器所安插的指针,指向virtual table;4表示z()被赋值的slot编号(关联到Point体系的virtual table)。唯一一个在执行期才能知道的东西是:slot4所指的到底是哪一个z()函数实体?

2.2 多重继承下的Virtual Functions

在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。

class Base1
{
public:
    Base1();
    virtual ~Base1();
    virtual void speakClearly();
    virtual Base1* clone() const;
protected:
    float data_Base1;
};

class Base2
{
public:
    Base2();
    virtual ~Base2();
    virtual void mumble();
    virtual Base2* clone() const;
protected:
    float data_Base2;
};

class Derived : public Base1, public Base2
{
public:
    Derived();
    virtual ~Derived();
    virtual Derived* clone() const;
protected:
    float data_Derived;
};

有三种情况,第二或后继的base class会影响对virtual functions的支持:

1>通过一个“指向第二个base class”的指针,调用derived class virtual function。

Base2* pbase2 = new Derived;

新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下的代码:

// 转移以支持第二个base class
Derived* temp = new Derived;
Base2* pbase2 = temp ? temp + sizeof(Base1) : 0;

如果没有这样的调整,指针的任何“非多态运用”都将失败:

// 即使pbase2被指定一个Derived对象,这也应该没有问题
pbase2->data_Base2;

当要删除pbase2所指的对象时:

// 必须首先调用正确的virtual destructor函数实体
// 然后施行delete运算符
// pbase2 可能需要调整,以指出完整对象的起始点
delete pbase2;

指针必须被再一次调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指的真正对象只有在执行期才能确定。

一般规则是,经由指向“第二或后继的base class”的指针(或reference)来调用derived class virtual function,该调用操作所连带的“必要的this指针调整”操作,必须在执行期完成。

在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目(因此,单一继承将不会有额外的virtual tables)。对于本例的Derived而言,会有两个virtual tables被编译器产生出来:

(1)一个主要实体,与Base1(最左端base class)共享;

(2)一个次要实体,与Base2(第二个base class)有关。

针对每一个virtual tables,Derived对象中有对应的vptr。vptrs将在constructors中被设立初值。

用以支持“一个class拥有多个virtual tables”的传统方法是,将每一个tables以外部对象的形式产生出来,并给与独一无二的名称。例如,Derived所关联的两个tables可能有这样的名称:

vtbl__Derived;     //主要表格
vtbl__Base2__Derived; //次要表格

于是当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtual table是主要表格vtbl__Derived。而当你将一个Derived对象地址指定给一个Base2指针时,被处理的virtual table是次要表格vtbl__Base2__Derived。

2>通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。

Derived* pder = new Derived;

// 调用Base2::mumble()
// pder 必须被向前调整sizeof(Base1)个bytes
pder->mumber();

3>允许一个virtual function的返回值类型有所变化,可能是base type,也可能是publicly derived type。

本例的Derived::clone()传回一个Derived class指针,默默地改写了它的两个base class函数实体。

Base* pb1 = new Derived;

//调用Derived* Derived::clone()
//返回值必须被调整,以指向Base2 subobject
Base2* pb2 = pb1->clone();

当进行pb1->clone()时,pb1会被调整指向Derived对象的起始地址,于是clone()的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。

2.3 虚拟继承下的Virtual Functions

class Point2d
{
public:
    Point2d(float = 0.0, float = 0.0);
    virtual ~Point2d();
    virtual void mumble();
    virtual float z();
protected:
    float _x, _y;
};

class Point3d : public virtual Point2d
{
public:
    Point3d(float = 0.0, float = 0.0, float = 0.0);
    ~Point3d();
    float z();
protected:
    float _z;
};

虽然Point3d有唯一一个(同时也是最左边的)base class,也就是Point2d,但Point3d和Point2d的起始部分并不像“非虚拟的单一继承”情况那样一致。由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针。

当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进了迷宫一样。因此,最好不要在一个virtual base class中声明nonstatic data members。

3. 其他

取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的结果是它在内存中真正的地址。然而这个值也是不完全的,它也需要被绑定于某个class object的地址上,才能够通过它调用该函数。所有的nonstatic member functions都需要对象的地址(以参数this指出)。

如果取一个static member function的地址,获得的将是其在内存中的位置,也就是其地址。由于static member function没有this指针,所以其地址的类型并不是一个“指向class member function的指针”,而是一个“nonmember函数指针”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值