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 第五版
版权声明:本文为博主原创文章,未经博主允许不得转载。