《深度探索C++对象模型》读书笔记(四)

来源:互联网 发布:易语言iphone在线源码 编辑:程序博客网 时间:2024/06/13 22:39

4. The Semantics of Function

C++支持三中类型的member function:static、nonstatic和virtual,每一种类型被调用的方式都不相同。对于static成员函数,不能直接读写nonstatic数据,也不能声明为const。

4.1 成员函数的三种调用方式

A. 非静态成员函数

C++设计准则之一:非静态成员函数至少必须和一般的非成员函数有相同的效率。选择成员函数不应该带来额外负担,因为编译器会按以下步骤,把成员函数实例转换为对等的非成员函数实例:

  1. 改写函数原型,安插一个额外的参数(即this指针)到成员函数中,使得class object可以调用这个函数
  2. 将成员函数中,每一个对非静态数据的操作,改为通过this指针来操作
  3. 将成员函数重写成一个外部函数,将函数名经过mangle处理,编码成为程序中独一无二的语汇

B. 虚成员函数

// 如果normalize()是一个虚成员函数ptr->normalize();// ptr指针调用会在内部转为(*ptr->vptr[1]) (ptr);

正如本书前面所述的虚函数实现多态的对象模型:

  • 编译器产生指针vptr指向vbtl,vptr安插在每一个继承虚函数的class object。当复杂的类继承体系中,会存在多个vptr,此时vptr名称会被mangled。
  • vptr[1]存有normalize函数的地址,关联到该函数
  • 第二个ptr表示this指针。

C. 静态成员函数

static member function是在cfont 2.0引入的,其主要特性是没有this指针

  • 它不能够直接读写其class中的nonstatic member
  • 它不能够被声明为const、volatile或virtual
  • 它不需要经由class object才被调用

一个静态成员函数,会在class外声明,并给予一个经过mangled的名称。如果对其取值,或得到其在内存中的位置,由于没有this指针,其地址类型不是一个指向class member function的指针,而是一个nonmember函数指针。


4.2 虚成员函数

A. 单继承下的虚函数

这一节详细说明了vptr和vtbl的实现原理,全书最有收获的地方。

C++中的多态表示“以一个public base class的指针或引用,寻址出一个derived class object”。即使用基类指针,也能在运行时runtime正确调用到其指向对象(派生类)的成员(主要是派生类改写的继承自基类的虚成员函数)。

为了实现多态的特性,一个class只会有一个virtual tables,每一个vtbl中含有其对应的class object所有(active)virtual function的地址,一个函数地址放在一个slot。这些地址在编译时期就可知道,并且固定不变,在runtime不会新增或替换,class object通过这些地址正确调用对应类的virtual function。(vtbl的构建和存取完全由编译器控制,不需要runtime介入。)

vtbl中存储的地址指向的virtual functions包括:

  • 当前class定义的虚函数,它会改写(override)继承自基类的同名虚函数;
  • 继承自基类的虚函数,这是当前派生类没有改写该函数出,保留了基类该函数的地址;
  • 一个pure_virtual_called()函数,既可以扮演pure virtual function的空间保卫角色,也能当做runtime异常处理函数。意外被调用时,通常结束掉这个程序,

对于以下单一继承的类布局,Point2d继承Point,Point3d继承Point2d。

class Point {public:     virtual ~Point();                     // slot 1     virtual Point& mult(float) = 0;       // slot 2: 纯虚函数没有定义,放pure_virtual_called()的地址     float x() const { return 0; }     virtual float y() const { return 0; } // slot 3     virtual float z() const { return 0; } // slot 4protected:     Point2d(float x = 0.0);     // ...};class Point2d : public Point {public:     Point2d(float x = 0.0, floaty = 0.0): Point(x), _y(y) {}     ~Point2d();                            // slot 1: 覆盖了基类虚析构函数的地址     // 改写基类的虚函数     Point2d& mult(float);                  // slot 2: 指向当前类的虚函数     float y() const {return _y;}           // slot 3: 指向当前类的虚函数                                            // slot 4: 继承基类的slot4指向的函数Point::z()     // ...};class Point3d : public Point2d {public:     Point3d(float x = 0.0, float y =0.0, float z = 0.0) : Point2d(x, y), _z(z) {}     ~Point3d();                            // slot 1: 覆盖了基类虚析构函数的地址     // 改写基类的虚函数     Point3d& mult(float);                  // slot 2: 指向当前类的虚函数                                              // slot 3: 继承了基类的slot3指向的函数Point2d::y()     float z() const {return _z;}           // slot 4: 指向当前类的虚函数     // ...};

当一个类继承基类时,可能有三种可能:

  • 它可以继承基类的虚函数:该函数的地址会被拷贝到derived class的vtbl的对应slot之中(原来在基类vtbl是slot3,在派生类的vtbl也是slot3)。
  • 它改写了基类的虚函数:自己的函数地址必须放在对应的slot之中。
  • 它可以加入新的虚函数:这时候vtbl的size会增大一个slot,放入新的虚函数地址。

具体的vtbl布局如下图:

单继承虚函数

按照上述原理实现vtbl后,在编译时期设定vtbl。
对于ptr->z(),虽然通过ptr调用函数z(),并不知道ptr指向的对象类型(Point、Point2d还是Point3d),但ptr可以获取到该对象的vtbl,而且函数z()是放在slot4(虽然不知道是哪个z()),这两点信息可以让编译器把指针调用转换为(*ptr->vptr[4]) (ptr),实现调用对应类的函数。这就是所谓的,只有在运行时,才知道slot4指的是哪个类的z()函数,即指针调用哪个类的z()函数。

B. 多继承下的虚函数

多继承支持虚函数,复杂度主要在于派生类的第二个及以后的基类上。

class B1 {virtual Base1 *clone() const;};class B2 {virtual Base2 *clone() const;};class Derived: public Base1, public Base2 {virtual Derived *clone() const;};Base2 *pbase2 = new Derived; // 新的Derived对象地址必须调整以指向其Base2 subobject// ==>编译时转换为:Derived *temp = new Derived;Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0; // 调整offset

当用第二个或以后的基类指针,来调用派生类虚函数时,必须在runtime完成this指针调整。

Bjarne在cfront的做法是加大vtbl,每一个table slot不再是指针,而是一个包含offset以及地址的结构体。缺点是连坐处罚了所有虚函数调用。

Thunk是另一种比较有效率的方法,thunk是一小段assembly代码,用来根据offset调整this指针并跳到相应的虚函数。Thunk技术允许slot内含一个简单指针(没有额外空间代价),可以(不需要调整this指针时)指向虚函数,也可以(需要调整this指针时)指向一个相关的thunk。

// Thunk ==> C++pbase2_dtor_thunk:     this += sizeof(base1);     Derived::~Derived(this);

一个继承n个基类的派生类包含n-1个额外的vtbl,对于Derived而言,有两个vtbl产生,一个和Base1共享,命名为vtbl_Derived;一个和Base2有关,命名为vtbl_Base2_Derived。

多继承下,在调用析构函数时有两种情况,是否需要调整this指针。 如下例子,虽然两个delete调用相同的Derived析构函数,但:

  • Base1是第一个基类,它指向Derived对象起始处,因此pbase1不需要调整this指针,vtbl的slot放置真正的析构函数地址。
  • Base2是第二个基类,需要调整this指针,vtbl需要相关的thunk地址。
Base1 *pbase1 = new Derived;Base2 *pbase2 = new Derived;delete pbase1;delete pbase2;

因此,当用一个Base1或Derived指针指向一个Derived对象地址,被处理的vtbl是vtbl_Derived;当用Base2指针指向一个Derived对象时,被处理的vtbl是vtbl_Base2_Derived。

多继承虚函数

有三种情况,第二或后继的基类会影响对虚函数的支持:

  1. 第二基类指针ptr调用子类的虚函数:ptr指向子类对象的Base2 Subobject,要向后调整sizeof(Base1)个bytes,从而指向Derived对象起始处。
  2. 子类指针pder调用第二基类的虚函数(继承来的):pder要向前调整sizeof(Base1)个bytes
  3. 当允许一个虚函数返回derived类时:如下例子,pb1调用clone(),pb1调整指向Derived对象的起始地址, Derived::clone()会被调用,回传一个指针,指向一个新的Derived指针,该对象的指针在赋值给pb2之前,必须先经过调整以指向Base2 subobject。
Base2 *pb1 = new Derived;Base2 *pb1 = pb->clone(); // 会调用子类的clone, 返回值要调整指向Base2 subobject

C. 虚继承下的虚函数

虚继承虚函数


4.4 指向成员函数的指针

A. 指向virtual成员函数的指针

B. 多继承下指向成员函数的指针


4.5 内联函数

编译器判断可以合理展开一个inline函数,表明在某个层次上,其执行成本比一般的函数调用及返回机制带来的代价低。

inline函数的处理流程:
1. 分析函数定义,以决定函数的intrinsic inline ability。如果函数因为复杂度或构建问题,被判断为不可成为inline,它会被转为static函数,在被编译模块产生对应的函数定义。
2. 真正的inline函数扩展操作是在调用点,会带来参数的求值操作(evaluation)以及临时性对象的管理。

inline函数在扩展时,每个形参都会被实参取代。当面对会带来副作用的实参,编译器需要引入临时对象,避免重复求值。

当inline函数内定义了局部变量,inline函数被扩展的时候,为了维护局部变量,会对该局部变量进行mangling改名,也会产生临时变量。

0 0
原创粉丝点击