一个简单的例子
class A {
public:
virtual void f() { cout << "A::f()" << endl; };
};
class B :public A {
public:
void f() { cout << "B::f()" << endl; };
};
int main() {
A* a = new B();
a->f();
}
输出结果:
B::f()
实现原理—V-Table
- 首先需要明白c++中类的存储结构中并不为普通的成员函数分配相应空间,而对于成员函数的调用交由编译器来实现,即在编译过程中通过查找符号表获得函数的地址。这一点我们可以通过如下代码来加以验证:
class A
{
public:
void f() { cout << "A::f()" << endl; }
virtual void f1() { cout << "A::f1()" << endl; }
};
int main(void)
{
A* a = NULL;
a->f();
a->f1();
}
输出结果:
A::f()
而在第二个函数调用时发生内存错误,具体理由继续往下看就知道了。
- 在不使用虚函数的情况下,虽然基类指针指向的是子类的实例,但编译器只会根据当前的变量类型来选择对应的函数,会产生如下结果:
class A {
public:
void f() { cout << "A::f()" << endl; };
};
class B :public A {
public:
void f() { cout << "B::f()" << endl; };
};
int main() {
A* a = new B();
a->f();
}
输出结果:
A::f()
-
我们的意图是想要程序最终调用函数是依据变量所指向的对象类型,而非变量的类型。要实现这一点可以在内存中添加相应的类型说明字段(我瞎想的,显然不太合适),而c++的实现是通过虚函数表(emmmm)。虚函数表中保存着成员虚函数(与普通成员函数有别)的地址,而这些虚函数可以在子类中实现多态。而在实际的对象存储结构中分配虚函数表的4字节地址的内存空间(大家可以通过*sizeof()*函数来验证),这四字节地址就是编译器联系对象的实际类型的纽带,至于怎么联系就继续往下看吧~。
-
先看如下代码:
typedef void(*Pfun)();
class A {
public:
int a=1;
virtual void f() { cout << "A::f()" << endl; };
};
class B {
public:
int b = 2;
virtual void f() { cout << "B::f()" << endl; };
};
class C :public A,public B {
public:
int c = 3;
virtual void g() { cout << "C::g()" << endl; };
};
int main() {
C c;
cout << sizeof(C) << endl;
//以下操作均是基于指针运算、可结合下图来理解。
((Pfun)*(int*)*(int*)(&c))();
((Pfun)*((int*)*(int*)(&c)+1))();
cout << *((int*)(&c) + 1) << endl;
((Pfun)*(int*)*((int*)(&c)+2))();
cout << *((int*)(&c) + 3) << endl;
cout << *((int*)(&c) + 4) << endl;
}
输出结果:
20
A::f()
C::g()
1
B::f()
2
3
这里class C 继承class A、class B,结合以上程序和下图理解对象c的实际存储结构:
在子类对象c的存储结构中依次是父类A、B对象,最后是子类新增的成员变量。若子类有新的虚函数如*C::g()*则直接添加到第一个父类对象A的虚函数表中。那么如果现在用父类指针指向当前对象并调用f()会发生什么呢?
int main() {
C c;
((A*)(&c))->f();
((B*)(&c))->f();
}
输出结果:
A::f()
B::f()
这里分别通过查找两个虚函数表找到对应的函数(emmmmmmmmmmmm)。等等感觉被忽悠了~!!很多教材上都说的是C++多态会根据内存中实际的对象类型来选择调用的函数,而内存中类型是C,此时C有两个f()分别是A::f()和B::f()。理论上应该是重复定义,因为根本不知道要调用哪一个f()!别急,我们来看看如下代码:
int main() {
C c;
cout<<(A*)(&c)<<endl;
cout<<(B*)(&c)<<endl;
}
输出结果:
00B9FB54
00B9FB5C
只是做了一些类型转换怎么两者的值还不一样了???我们有必要来看一下汇编代码,你永远不知道编译器背着你偷偷干了些什么~
cout<<(A*)(&c)<<endl;
...
000426B9 lea eax,[c]
000426BC push eax
...
cout<<(B*)(&c)<<endl;
...
000426E6 lea ecx,[c]
000426E9 add ecx,8
...
这里当我们在做类型转换的时候,编译器将原先的指针加上了8,也就是c图中第二个虚函数表指针的位置。我们猜想之后的操作分别按照类型A和类型B来进行操作,但和查找普通函数不一样,虚函数地址查找通过查找虚函数表来完成。
- 通过上面的学习,我们了解到了父类指针调用虚函数的过程,那么如果子类实现覆盖了虚函数会是怎么样的呢,答案是子类的虚函数直接覆盖了父类的虚函数表中对应的内容,如下图所示,那么当再遇到如上的情况时,父类通过虚函数表查找虚函数所得到的对应的虚函数就是背子类覆盖过的内容,因此可以达到多态。
我们可以通过如下程序来验证此结论:
//省略class A 和 class B 和上面一致
class C :public A,public B {
public:
int c = 3;
virtual void g() { cout << "C::g()" << endl; };
virtual void f() { cout << "C::f()" << endl; };
};
int main() {
C c;
cout << sizeof(C) << endl;
//以下操作均是基于指针运算、可结合下图来理解。
((Pfun)*(int*)*(int*)(&c))();
((Pfun)*((int*)*(int*)(&c) + 1))();
cout << *((int*)(&c) + 1) << endl;
((Pfun)*(int*)*((int*)(&c) + 2))();
cout << *((int*)(&c) + 3) << endl;
cout << *((int*)(&c) + 4) << endl;
}
输出结果:
20
C::f()
C::g()
1
C::f()
2
3
总结
C++类通过引入虚函数,即在class存储结构中添加虚函数表指针,使得在编译阶段有多种查找函数地址的方式。对于普通函数则通过查找符号表,而对于虚函数则通过查找虚函数表。符号表具有唯一性,而虚函数表在子类继承时将copy一份,并可以通过覆盖的方式修改符号表从而实现多态。