C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型

一、虚函数表指针(vptr)及虚基类表指针(bptr)

C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括:

virtual function机制:用以支持一个有效率的“执行期绑定”;

virtual base class:用以实现多次在继承体系中的基类,有一个单一而被共享的实体。

1、虚函数表指针

C++中,有两种数据成员:static和nonstatic,以及三种成员函数:static、nonstatic和virtual。已知下面这个class Point的声明:

class Point
{
public:
    Point(float xval);
    virtual ~Point();
    float x() const;
    static int PointCount();
protected:
    virtual ostream& print (ostream &os) const;
    float _x;
    static int _point_count;
};   

那么这个class Point在机器中将会被怎么样表现呢?

C++对象模型中,非static数据成员被配置于每一个对象之内,static数据成员则被存放在所有的对象之外,通常被放置在程序的全局(静态)存储区内,故不会影响个别的对象大小。static和非static函数也被放在所有的对象之外。

virtual函数则以两个步骤支持之:

(1)每一个类产生出一堆指向virual functions的指针,放在表格之中,这个表格被称为virtual table(vtbl);

(2) 每一个对象被添加了一个指针,指向相关的virtual table。通常通常这个指针被称为vptr(虚函数表指针)。vptr的设定和重置都由每一个类的构造函数、析构函数和复制构造函数自动完成。每一个类 所关联的type_info信息(用以支持runtime type identification,RTTI)也经由virtual table被指出来,通常是放在表格的第一个slot处。如图给出C++对象模型。

注意:后续内容将会发现type_info信息并没有存放在第一个slot处,存放在第一个slot处的时Point::~Point()。

(包含虚函数的类对象头4个字节存放指向虚函数表的指针)

注意:若不是虚函数,一般的函数不会出现在虚函数表,因为不用通过虚函数表指针间接去访问。

由于vptr在对象中的偏移不会随着派生层次的增加而改变,而且改写的虚函数在派生类 vtable中的位置与它在基类vtable中的位置始终保持一致,有了这两条保证,再加上被改写虚函数与其基类中对应虚函数的原型和调用规范都保持一 致,自然就能轻松地调用起实际所指对象的虚函数了。

【例子】

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void Fun1()
    {
        cout << "Base::Fun1 ..." << endl;
    }

    virtual void Fun2()
    {
        cout << "Base::Fun2 ..." << endl;
    }

    void Fun3() //被Derived继承后被隐藏
    {
        cout << "Base::Fun3 ..." << endl;
    }
};

class Derived : public Base
{
public:
    /*virtual */
    void Fun1()
    {
        cout << "Derived::Fun1 ..." << endl;
    }

    /*virtual */
    void Fun2()
    {
        cout << "Derived::Fun2 ..." << endl;
    }

    void Fun3()
    {
        cout << "Derived::Fun3 ..." << endl;
    }
};

int main(void)
{
    Base *p;
    Derived d;
    p = &d;
    p->Fun1();      // Fun1是虚函数,基类指针指向派生类对象,调用的是派生类对象的虚函数(间接)
    p->Fun2();
    p->Fun3();      // Fun3非虚函数,根据p指针实际类型来调用相应类的成员函数(直接)
    Base &bs = d;
    bs.Fun1();
    bs.Fun2();
    bs.Fun3();
    d.Fun1();
    d.Fun2();
    d.Fun3();
    cout << sizeof(Base) << endl;
    cout << sizeof(Derived) << endl;
    return 0;
}

运行结果:

Derived::Fun1 ...

Derived::Fun2 ...

Base::Fun3 ...

Derived::Fun1 ...

Derived::Fun2 ...

Base::Fun3 ...

Derived::Fun1 ...

Derived::Fun2 ...

Derived::Fun3 ...

4

4

sizeof(Base); 和 sizeof(Derived); 都是4个字节,其实就是虚表指针,据此可以画出对象的模型:

Derived类继承了Base类的虚函数Fun1,Fun2, 但又重新实现了,即覆盖了。程序中通过基类的指针或引用可以通过vptr间接访问到Derived::Fun1, Derived:::Fun2,但因为Fun3不是虚函数(基类的Fun3 被继承后被隐藏),故p->Fun3(); 和bs.Fun3(); 根据指针或引用的实际类型去访问,即访问到被Derived继承下来的基类Fun3。

【例1】

一般情况下,下面那些操作会执行失败?()(多选)

#include <iostream>
#include <string>
using namespace std;

class A
{
public:
    string a;
    void f1() {cout << "Hello World" << endl;}
    void f2()
    {
        a = "Hello World";
        cout << a << endl;
    }
    virtual void f3() {cout << "Hello World" << endl;}
    virtual void f4()
    {
        a = "Hello World";
        cout << a << endl;
    }
};

A. A* aptr = NULL; aptr->f1();

B. A* aptr = NULL; aptr->f2();

C. A* aptr = NULL; aptr->f3();

D. A* aptr = NULL; aptr->f4();

解答:BCD。因为A没有使用任何成员变量,且f1函数是非虚函数(不存在于具体对象中),是静态绑定的,所以A

不需要使用对象的信息,故正确。

在B中f2()使用了成员变量,而成员变量只能存在于对象中;

在C中f3()时虚函数,需要使用虚表指针(存在于具体对象中);

D同C。

可见BCD都需要有具体存在的对象,故不正确。

以上可修改为对象访问,则都正确,如下

#include <iostream>
#include <string>
using namespace std;

class A
{
public:
    string a;
    void f1() {cout << "Hello World" << endl;}
    void f2()
    {
        a = "Hello World";
        cout << a << endl;
    }
    virtual void f3() {cout << "Hello World" << endl;}
    virtual void f4()
    {
        a = "Hello World";
        cout << a << endl;
    }
};

int main()
{
    A aptr;
    aptr.f1();
    aptr.f2();
    aptr.f3();
    aptr.f4();
    return 0;
}

【例2】

请问下面代码的输出结果是什么?

#include <iostream>
#include <string>
using namespace std;

class A
{
public:
    A()
    {
        a = 1;
        b = 2;
    }
private:
    int a;
    int b;
};

class B
{
public:
    B() {c = 3;}
    void print() {cout << c <<  endl;}
private:
    int c;
};

int main()
{
    A a;
    B* pb = (B*)(&a);
    pb->print();
    return 0;
}

运行结果:

1

解 答:1。这里将指向B类型的指针指向A类型的对象,由于函数print并不位于对象中,且print是非虚函数,故执行静态绑定(若是动态绑定,则需要 vptr的信息,而对象a中不存在vptr信息,则执行会出错)。当调用print函数时,需要输出c的值,程序并不知道指针pb指向的对象不是B类型的 对象,只是盲目地按照偏移值去取,c在类B的对象中的偏移值跟a在类A的对象中的偏移值相等(都位于对象的起始地址处),故取到a的值1。

2、虚函数表的实现

父类指针是如何通过虚函数表找到子类的虚函数呢?

通过C++对象模型,我们可以通过Base的实例来得到虚函数表。

【例1】

#include <iostream>
#include <string>
using namespace std;

class Base
{
public:
    virtual void f() {cout << "Base::f" << endl;}
    virtual void g() {cout << "Base::g" << endl;}
    virtual void h() {cout << "Base::h" << endl;}
};

int main()
{
    typedef void(*Fun) (void);
    Base b;
    Fun pFun = NULL;
    cout << "虚函数表地址:" << (int*)(&b) << endl;
    cout << "虚函数表-第一个函数地址" << (int*)*(int*)(&b) << endl;
    //Invoke the first virtual function
    pFun = (Fun)*((int*)*(int*)(&b));
    pFun();
    pFun = (Fun)*((int*)*(int*)(&b)+1);
    pFun();
    pFun = (Fun)*((int*)*(int*)(&b)+2); //Base::h()
    pFun();
    return 0;
}

运行结果:

虚函数表地址:0xbfc488f8

虚函数表-第一个函数地址0x80489b0

Base::f

Base::g

Base::h

解释:通过这个实例,我们通过强行把&b转成int*,取得虚函数表的地址,然后再次取址就可以得到第一个虚函数的地址了,也就是Base::f()。

图示如下:

注意:在上面这个图中,在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符"\0"一样,标志了虚函数表的结束,这个结束标志符在不同编译器下是不同的。同时类Base的对象大小为4,即类中仅有一个指针vptr(指向虚函数表)。

【例2】

画出下列类A、B、C、D的对象的虚函数表。

#include <iostream>
#include <string>
using namespace std;

class A
{
public:
    virtual void a() {cout << "a() in A" << endl;}
    virtual void b() {cout << "b() in A" << endl;}
    virtual void c() {cout << "c() in A" << endl;}
    virtual void d() {cout << "d() in A" << endl;}
};

class B: public A
{
public:
    void a() {cout << "a() in B" << endl;}
    void b() {cout << "b() in B" << endl;}
};

class C: public A
{
public:
    void a() {cout << "a() in C" << endl;}
    void b() {cout << "b() in C" << endl;}
};

class D: public B, public C
{
public:
    void a() {cout << "a() in D" << endl;}
    void d() {cout << "a() in D" << endl;}
};

int main()
{
    cout << sizeof(A) << endl;
    cout << sizeof(B) << endl;
    cout << sizeof(C) << endl;
    cout << sizeof(D) << endl;
    return 0;
}

运行结果:

4

4

4

8

解答:如下所示:

3、虚基类表指针(bptr)

C++支持单一继承,也支持多重继承。如:

class D: public B, public C {...};

甚至,继承关系也可以指定为虚拟(virtual,也就是共享的意思):

class B: virtual public A {...};
class C: virtual public A {...};

以上继承方式称为菱形继承。菱形继承指的是:B、C虚拟继承A,然后D普通继承B、C,如下图所示。

在虚拟继承的情况下,基类不管在继承串链中被派生出多少次,永远只会存在一个实体。

在虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量。此指针被称为bptr。

注意:在同时存在vptr与bptr时,某些编译器会将其进行优化,合并为一个指针。

【例1】

如下列代码的输出结果是什么?

#include <iostream>
#include <string>
using namespace std;

class X{};
class Y: public virtual X{};
class Z: public virtual X{};
class A: public Y, public Z{};

int main()
{
    cout << "sizeof(X): " << sizeof(X) << endl;
    cout << "sizeof(Y): " << sizeof(Y) << endl;
    cout << "sizeof(Z): " << sizeof(Z) << endl;
    cout << "sizeof(A): " << sizeof(A) << endl;
    return 0;
}

运行结果:

sizeof(X): 1

sizeof(Y): 4

sizeof(Z): 4

sizeof(A): 8

解答:X类是空,编译器会安插进去一个byte,一个隐晦的1字节。下图给出X、Y、Z的对象布局。

Y和Z的大小都是4B,其对象内仅包含一个bptr,且不许要对齐处理。

主要讨论A的大小,由菱形继承的图可以知道,class A的占用空间由下面几部分构成:

(1)被大家共享的唯一一个class X实体,大小为1B,目前的编译器通常做了优化,省去这单单为了占位的1B,故此部分为0;

(2)Base class Y的大小(为4B)减去“因virtual base class X而配置“的大小(本题中为0),故结果为4B。

(3)Base class Z的大小(为4B)减去“因virtual base class X而配置“的大小(本题中为0),故结果为4B。

(4)class A自己的大小:0B。

前述四项综合,共8B。然后考虑字节对齐,不需要对齐,故sizeof(A)为8.

参考:

C++ primer 第四版

C++ primer 第五版

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-02 06:55:06

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型的相关文章

C++ Primer 学习笔记_73_面向对象编程 --再谈文本查询示例

面向对象编程 --再谈文本查询示例 引言: 扩展第10.6节的文本查询应用程序,使我们的系统可以支持更复杂的查询. 为了说明问题,将用下面的简单小说来运行查询: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful fiery bird, he

C++ Primer 学习笔记_74_面向对象编程 --再谈文本查询示例[续/习题]

面向对象编程 --再谈文本查询示例[续/习题] //P522 习题15.41 //1 in TextQuery.h #ifndef TEXTQUERY_H_INCLUDED #define TEXTQUERY_H_INCLUDED #include <iostream> #include <fstream> #include <sstream> #include <vector> #include <set> #include <map&g

C++ Primer 学习笔记_66_面向对象编程 --定义基类和派生类[续]

算法旨在用尽可能简单的思路解决问题,理解算法也应该是一个越看越简单的过程,当你看到算法里的一串概念,或者一大坨代码,第一感觉是复杂,此时不妨从例子入手,通过一个简单的例子,并编程实现,这个过程其实就可以理解清楚算法里的最重要的思想,之后扩展,对算法的引理或者更复杂的情况,对算法进行改进.最后,再考虑时间和空间复杂度的问题. 了解这个算法是源于在Network Alignment问题中,图论算法用得比较多,而对于alignment,特别是pairwise alignment, 又经常遇到maxim

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

C++ Primer 学习笔记_65_面向对象编程 --概述、定义基类和派生类

面向对象编程 --概述.定义基类和派生类 引言: 面向对象编程基于的三个基本概念:数据抽象.继承和动态绑定. 在C++中,用类进行数据抽象,用类派生从一个类继承另一个:派生类继承基类的成员.动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数. 继承和动态绑定在两个方面简化了我们的程序:[继承]能够容易地定义与其他类相似但又不相同的新类,[派生]能够更容易地编写忽略这些相似类型之间区别的程序. 面向对象编程:概述 面向对象编程的关键思想是多态性(polymorphism)

C++ Primer 学习笔记_72_面向对象编程 --句柄类与继承[续]

面向对象编程 --句柄类与继承[续] 三.句柄的使用 使用Sales_item对象能够更easy地编写书店应用程序.代码将不必管理Item_base对象的指针,但仍然能够获得通过Sales_item对象进行的调用的虚行为. 1.比較两个Sales_item对象 在编写函数计算销售总数之前,须要定义比較Sales_item对象的方法.要用Sales_item作为关联容器的keyword,必须能够比較它们.关联容器默认使用keyword类型的小于操作符,可是假设给Sales_item定义小于操作符,

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

C++ Primer 学习笔记_67_面向对象编程 --转换与继承、复制控制与继承

面向对象编程 --转换与继承.复制控制与继承 I.转换与继承 引言: 由于每一个派生类对象都包括一个基类部分,因此能够像使用基类对象一样在派生类对象上执行操作. 对于指针/引用,能够将派生类对象的指针/引用转换为基类子对象的指针/引用. 基类类型对象既能够作为独立对象存在,也能够作为派生类对象的一部分而存在,因此,一个基类对象可能是也可能不是一个派生类对象的部分,因此,没有从基类引用(或基类指针)到派生类引用(或派生类指针)的(自己主动)转换. 关于对象类型,尽管一般能够使用派生类型的对象对基类

C++ Primer 学习笔记_69_面向对象编程 --继承情况下的类作用域

面向对象编程 --继承情况下的类作用域 引言: 在继承情况下,派生类的作用域嵌套在基类作用域中:如果不能在派生类作用域中确定名字,就在外围基类作用域中查找该名字的定义. 正是这种类作用域的层次嵌套使我们能够直接访问基类的成员,就好像这些成员是派生类成员一样: Bulk_item bulk; cout << bulk.book() << endl; 名字book的使用将这样确定[先派生->后基类]: 1)bulk是Bulk_item类对象,在Bulk_item类中查找,找不到名