C++对象模型(2) -- 虚函数

一、虚函数表指针位置分析

  一个类若有虚函数,这个类就会产生一个虚函数表。当类创建对象的时候,对象内就会维护一个虚函数表指针,该指针(vptr)会指向这个虚函数表的开始地址。接下来借助代码分析一下虚函数表指针的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A
{
public:
int i; //4字节
virtual void testfunc() {} //虚函数,vptr4字节。
};

int main()
{
A aobj;
int ilen = sizeof(aobj);
cout << ilen << endl; //8字节

char *p1 = reinterpret_cast<char *>(&aobj); //类型转换,硬转 &aobj这是对象aobj的首地址。
char *p2 = reinterpret_cast<char *>(&(aobj.i));
if (p1 == p2) //说明aobj.i和aobj的位置相同,说明i在对象aobj内存布局的上边。虚函数表指针vptr在下边
{
cout << "虚函数表指针位于对象内存的末尾" << endl;
}
else
{
cout << "虚函数表指针位于对象内存的开头" << endl;
}
return 1;
}

  通过测试,我们发现虚函数表指针在对象内存的开头处。

二、继承关系作用下虚函数的手工调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//父类
class Base
{
public:
virtual void f() { cout << "Base::f()" << endl; }
virtual void g() { cout << "Base::g()" << endl; }
virtual void h() { cout << "Base::h()" << endl; }
};
class Derive :public Base
{
virtual void g() { cout << "Derive::g()" << endl; }
};

int main()
{
//
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;

Derive *d = new Derive(); //派生类指针。
long *pvptr = (long *)d; //指向对象的指针d转成了long *类型。
long *vptr = (long *)(*pvptr);
//(*pvptr)表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,代表的是虚函数表指针地址。

for (int i = 0; i <= 4; i++) //循环5次,打印出虚函数的地址。
{
printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
}

  //上述循环运行结果:
  //vptr[0] = 0x:00DF11A4
  //vptr[1] = 0x:00DF1320
  //vptr[2] = 0x:00DF1334
  //vptr[3] = 0x:00000000
  //vptr[4] = 0x:69726544

typedef void(*Func)(void); //定义一个函数指针类型
Func f = (Func)vptr[0]; //f就是函数指针变量,vptr[0]是指向第一个虚函数的。
Func g = (Func)vptr[1];
Func h = (Func)vptr[2];
/*Func i = (Func)vptr[3];
Func j = (Func)vptr[4];*/

f(); //Base::f()
g(); //Derive::g()
h(); //Base::h()
//i();

Base *dpar = new Base(); //父类指针
long *pvptrpar = (long *)dpar;
long *vptrpar = (long *)(*pvptrpar);

for (int i = 0; i <= 4; i++) //循环5次;
{
printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]);
}

  //上述循环运行结果:
  //vptr Base[0] = 0x:00DF11A4
  //vptr Base[1] = 0x:00DF117C
  //vptr Base[2] = 0x:00DF1334
  //vptr Base[3] = 0x:00000000
  //vptr Base[4] = 0x:65736142

Func fpar = (Func)vptrpar[0];
Func gpar = (Func)vptrpar[1];
Func hpar = (Func)vptrpar[2];

cout << "--------------------" << endl;
fpar(); //Base::f()
gpar(); //Base::g()
hpar(); //Base::h()

return 1;
}

  从上面的运行结果来看(注释中已标出),对于子类对象来说,子类中要是有和父类同名的虚函数,则子类中的虚函数就会把父类中的同名虚函数覆盖,同时虚函数表中对应的虚函数地址必然会发生变化(变成子类中虚函数的地址)。但是如果是父类对象,则还是会调用父类中的虚函数(即不会被覆盖)。
  接着看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef void(*Func)(void); //定义一个函数指针类型

Derive derive;
long *pvptrderive = (long *)(&derive);
long *vptrderive = (long *)(*pvptrderive); //0x00b09b6c {project100.exe!void(* Derive::`vftable'[4])()} {11538847}
Func f1 = (Func)vptrderive[0]; //0x00b0119f {project100.exe!Base::f(void)}
Func f2 = (Func)vptrderive[1]; //0x00b0150f {project100.exe!Derive::g(void)}
Func f3 = (Func)vptrderive[2]; //0x00b01325 {project100.exe!Base::h(void)}
Func f4 = (Func)vptrderive[3]; //0x69726544
Func f5 = (Func)vptrderive[4]; //0x3a3a6576

Derive derive2 = derive; //调用拷贝构造函数
long *pvptrderive2 = (long *)(&derive2);
long *vptrderive2 = (long *)(*pvptrderive2);

Base base = derive; //直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象。
//所以Base base = derive;实际干了两个事情:
//第一个事情:生成一个base对象
//第二个事情:用derive来初始化base对象的值。
long *pvptrbase = (long *)(&base);
long *vptrbase = (long *)(*pvptrbase); //0x00b09b34 {project100.exe!void(* Base::`vftable'[4])()} {11538847}
Func fb1 = (Func)vptrbase[0]; //0x00b0119f {project100.exe!Base::f(void)}
Func fb2 = (Func)vptrbase[1]; //0x00b01177 {project100.exe!Base::g(void)}
Func fb3 = (Func)vptrbase[2]; //0x00b01325 {project100.exe!Base::h(void)}
Func fb4 = (Func)vptrbase[3]; //0x00000000
Func fb5 = (Func)vptrbase[4]; //0x65736142

  可以看出,当用子类对象初始化父类对象的时候,编译器给咱们做了一个选择,显然derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值。
  总结:
  (1)一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同。
  (2)父类中有虚函数就等于子类中有虚函数。话句话来说,父类中有虚函数表,则子类中肯定有虚函数表。因为你是继承父类的。并且只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。但如果子类只继承自一个父类,则不管是父类还是子类,它们内部都只会有一个虚函数表。
  (3)如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同。但,仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。
  (4)虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),则该表项所执行的该函数的地址应该相同。
  (5)超出虚函数表部分的内容不可知。

三、多重继承虚函数表分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
//基类1
class Base1
{
public:
virtual void f()
{
cout << "base1::f()" << endl;
}
virtual void g()
{
cout << "base1::g()" << endl;
}
};

//基类2
class Base2
{
public:
virtual void h()
{
cout << "base2::h()" << endl;
}
virtual void i()
{
cout << "base2::i()" << endl;
}
};

//子类
class Derived :public Base1, public Base2
{
public:
virtual void f() //覆盖父类1的虚函数
{
cout << "derived::f()" << endl;
}
virtual void i() //覆盖父类2的虚函数
{
cout << "derived::i()" << endl;
}

//如下三个我们自己的虚函数
virtual void mh()
{
cout << "derived::mh()" << endl;
}

virtual void mi()
{
cout << "derived::mi()" << endl;
}

virtual void mj()
{
cout << "derived::mj()" << endl;
}
};

int main()
{
cout << sizeof(Base1) << endl;
cout << sizeof(Base2) << endl;
cout << sizeof(Derived) << endl;

Derived ins;
Base1 &b1 = ins; //多态
Base2 &b2 = ins;
Derived &d = ins;

typedef void(*Func)(void);
long *pderived1 = (long *)(&ins);
long *vptr1 = (long *)(*pderived1); //取第一个虚函数表指针。

long *pderived2 = pderived1 + 1; //跳过4字。
long *vptr2 = (long *)(*pderived2); //取第二个虚函数表指针。

Func f1 = (Func)vptr1[0]; //0x00ab15d7 {project100.exe!Derived::f(void)}
Func f2 = (Func)vptr1[1]; //0x00ab15f0 {project100.exe!Base1::g(void)}
Func f3 = (Func)vptr1[2]; //0x00ab15cd {project100.exe!Derived::mh(void)}
Func f4 = (Func)vptr1[3]; //0x00ab15ff {project100.exe!Derived::mi(void)}
Func f5 = (Func)vptr1[4]; //0x00ab15eb {project100.exe!Derived::mj(void)}
Func f6 = (Func)vptr1[5]; //非正常
Func f7 = (Func)vptr1[6];
Func f8 = (Func)vptr1[7];

Func f11 = (Func)vptr2[0]; //0x00ab15af {project100.exe!Base2::h(void)}
Func f22 = (Func)vptr2[1]; //0x00ab15b9 {project100.exe!Derived::i(void)}
Func f33 = (Func)vptr2[2]; //非正常
Func f44 = (Func)vptr2[3];

b1.f();
b2.i();
d.f();
d.i();
d.mh();
d.g();

cout << "-----------------" << endl;
f1();
f2();
f3();
f4();
f5();
cout << "-------------" << endl;
f11();
f22();

return 1;
}

  (1)一个对象,如果它的类有多个基类则有多个虚函数表指针(注意是多个虚函数表指针,而不是多个虚函数表)。
  (2)在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr);
  (3)如上程序中,子类对象ins有两个虚函数表指针,vptr1,vptr2。类Derived有两个虚函数表,因为它继承自两个基类。
  (4)子类和第一个基类公用一个vptr(因为vptr指向一个虚函数表,所以也可以说子类和第一个基类共用一个虚函数表vtbl),因为我们注意到了类Derived的虚函数表1里边的5个函数,而g()正好是base1里边的函数。
  (5)子类中的虚函数覆盖了父类中的同名虚函数。比如derived::f(),derived::i();

四、vptr、vtbl创建时机

  (1)vptr(虚函数表指针)跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来。即运行的时候。实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加为vptr赋值的代码,这是在编译期间编译器为构造函数增加的。当程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了。
  (2)虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容。然后也是在编译期间在相应的类构造函数中添加给vptr赋值的代码,这样程序运行的时候,当运行到创建类对象的代码时,会调用类的构造函数,执行到类的构造函数中的给vptr赋值的代码,这样这个类对象的vptr(虚函数表指针)就有值了。

五、普通类包含虚函数时引发的虚函数调用问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class X
{
public:
int x;
int y;
int z;
//X() :x(0), y(0), z(0)
X()
{
//编译器角度 伪码;
//vptr = vtbl; //下边的memset会把vptr(虚函数表指针)清0
memset(this, 0, sizeof(X));
cout << "构造函数被执行" << endl;
}
//X(const X &tm) :x(tm.x), y(tm.y), z(tm.z)
X(const X &tm)
{
memcpy(this, &tm, sizeof(X));
cout << "拷贝构造函数被执行" << endl;
}
virtual ~X()
{
cout << "析构函数被执行" << endl;
}
virtual void virfunc()
{
cout << "虚函数virfunc()被执行" << endl;
}
void ptfunc()
{
cout << "普通函数ptfunc()被执行" << endl;
}
};
int main()
{
//X x0; //调用构造函数
///*x0.x = 100;
//x0.y = 200;
//x0.z = 300;*/
//x0.virfunc(); //虚函数表指针为null居然可以成功调用虚函数;

//X x1(x0); //调用拷贝构造函数
//cout << "x1.x=" << x1.x << " x1.y=" << x1.y << " x1.z=" << x1.z << endl;

//X *px0 = new X();
//px0->ptfunc(); //正常调用
//px0->virfunc(); //无法正常调用
//delete px0; //无法正常调用
//new出来的对象,虚函数变得无法正常执行了;

int i = 9;
printf("i的地址 = %p\n", &i);
X x0;
printf("ptfunc()的地址=%p\n", &X::ptfunc); //打印正常的成员函数地址。
//long *pvptrpar = (long *)(&x0);
//long *vptrpar = (long *)(*pvptrpar);
//printf("virfunc的地址 = %p\n", vptrpar[1]);//虚函数virfunc地址
x0.ptfunc();
x0.virfunc(); //不叫多态,属于静态联编,我们推断:这个函数ptfunc()和virfunc()函数,是在编译的就确定好的;

X *pX0 = new X();
pX0->ptfunc();
pX0->virfunc(); //通过虚函数表指针,找虚函数表,然后从虚函数表中找到virfunc虚函数的地址并调用。这就是多态

//更明白:虚函数,多态,这种概念专门给指针或者引用用的;
X &xy = *pX0;
xy.virfunc();
X &xy2 = x0;
xy2.virfunc();
return 1;
}

  (1)如果一个普通类中包含了虚函数,那么在构造函数中使用如上所示的memset或者拷贝构造函数中使用如上所示的memcpy方法,那么就会出现程序崩溃的情形。
  (2)某些情况下,编译器会往类内部增加一些我们看不见但真实存在的成员变量(隐藏成员变量),比如你类中增加了虚函数,系统默认往类对象中增加虚函数表指针,这个虚函数表指针就是隐藏的成员变量。有了这种变量的类,就不是普通的类了。同时,这种隐藏的成员变量的增加(使用)或者赋值的时机,往往都是在执行构造函数或者拷贝构造函数的函数体之前进行。那么你如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了。
  (3)静态联编是指:我们编译的时候就能确定调用哪个函数。把调用语句和被调用函数绑定到一起。动态联编是在程序运行时,根据时机情况,动态的把调用语句和被调用函数绑定到一起,动态联编一般旨有在多态和虚函数情况下才存在。
  (4)对多态,虚函数,父类,子类。虚函数主要解决的问题父类指针指向子类对象这种情况。如果一个类中只有虚函数,没有继承,那么虚函数和普通函数没有区别,就算虚函数表指针被置空,仍然可通过对象正常调用,因为这是静态联编,不是多态。但是如果用new出来的对象调用,就会失败,因为虚函数表指针为空,找不到虚函数表。