c++中的虚函数作用主要是实现多态,用父类指针指向子类实例,通过父类指针调用子类成员函数,而这个成员函数在父类中被声明为虚函数。
要实现父类指针调用子类成员函数,主要满足以下两个条件:
1、子类实例指针转换成父类指针时,指针所指的子类实例内容必须与父类内容完全一致(成员及顺序);主要是通过在子类继承父类时,按继承顺序将父类内容与子类成员组成子类内容来实现,将子类实例指针偏移一定量就能得到父类指针,因为指针所指子类实例内容与与父类完全一致。
2、父类的虚函数方法必须在继承时,由子类对应的函数将覆盖,进而使父类调用相应的函数时实际调用的是子类函数;主要是通过虚函数表(virtual table)实现
我们以下面的例子来看虚函数表及其在继承时的变化
涉及的几个类的继承关系如下图:
I.code
vtable.cpp
#include <iostream>
using namespace std;
class BaseA {
public:
virtual void a() { cout << "BaseA::a" << endl; }
virtual void b() { cout << "BaseA::b" << endl; }
private:
int m_a;
int m_b;
};
class BaseB {
public:
virtual void a() { cout << "BaseB::a" << endl;}
void b() { cout << "BaseB::b" << endl; }
virtual void c() { cout << "BaseB::c" << endl; }
private:
int m_b;
int m_c;
};
class DeriveAB : public BaseA, public BaseB {
public:
virtual void a() { cout<< "DeriveAB::a" << endl; }
virtual void b() { cout<< "DeriveAB::b" << endl; }
virtual void xa() { cout << "DeriveAB::xa" << endl; }
private:
int m_a;
int m_b;
int m_c;
};
class BaseC {
public:
virtual void c() { cout << "BaseC::c" << endl; }
virtual void d() { cout << "BaseC::d" << endl; }
private:
int m_c;
int m_d;
};
class DeriveABC : public DeriveAB, public BaseC {
public:
virtual void a() { cout << "DeriveABC::b"<<endl; }
virtual void b() { cout << "DeriveABC::b"<< endl; }
void c() { cout << "DeriveABC::c"<< endl; }
virtual void xxa() { cout << "DeriveABC::xxa"<< endl;}
private:
int m_a;
int m_b;
};
BaseA a;
BaseB b;
BaseC c;
DeriveAB ab;
DeriveABC abc;
void deriveab()
{
((BaseA *)&ab)->b();
((BaseB *)&ab)->b();
}
void deriveabc()
{
((BaseA*)&abc)->b();
((BaseB*)&abc)->b();
((DeriveAB *)&abc)->b();
}
int main()
{
deriveab();
cout<<endl;
deriveabc();
return 0;
}
Makefile:
all:
@g++ vtable.cpp -o vtable
debug:
@gcc -g vtable.cpp -o vtable -lstdc++
asm:
@gcc -x c++ -S vtable.cpp
clean:
@rm vtable
运行结果如下:
[redhat@localhost vtable]$ ./vtable
DeriveAB::b
BaseB::b
DeriveABC::b
BaseB::b
DeriveABC::b
可以看出:
DeriveAB在继承BaseA时,会将BaseA的b函数覆盖成自己的b函数,因为BaseA类将b函数声明为虚函数;
DeriveAB在继承BaseB时,只能将BaseB的b函数隐藏,但是不能覆盖成自己的b函数,因为BaseB类将b函数声明为非虚函数;
II.子类的内容排布
i.DeriveAB子类的内容如下图所示:
类的实例所占内存是由类成员(包括父类及子类)及虚函数表(虚函数表个数由最顶级包含虚函数的基类个数决定,如DeriveABC的顶级基类为BaseA、BaseB、BaseC且都有虚函数,所有DeriveABC有三个虚函数表)组成
ii.gdb
[redhat@localhost vtable]$ gdb vtable
Reading symbols from /home/redhat/code/cpp/vtable/vtable...done.
(gdb) start
Temporary breakpoint 1 at 0x804875a: file vtable.cpp, line 25.
Starting program: /home/redhat/code/cpp/vtable/vtable
Temporary breakpoint 1, main () at vtable.cpp:25
25 deriveab();
Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.25.el6.i686 libgcc-4.4.5-6.el6.i686 libstdc++-4.4.5-6.el6.i686
(gdb) print sizeof(BaseA)
$1 = 12
(gdb) print sizeof(int)*2
$2 = 8
(gdb) print sizeof(BaseB)
$3 = 12
(gdb) print sizeof(DeriveAB)
$4 = 36
(gdb) print sizeof(BaseA)+sizeof(BaseB)+sizeof(int)*3
$5 = 36
(gdb) print sizeof(void*)
$6 = 4
(gdb) print ab
$7 = {<BaseA> = {_vptr.BaseA = 0x8048d08, m_a = 0, m_b = 0}, <BaseB> = {_vptr.BaseB = 0x8048d1c, m_b = 0, m_c = 0}, m_a = 0, m_b = 0, m_c = 0}
(gdb) print &ab
$15 = (DeriveAB *) 0x804a560
(gdb) print (void *)&ab + sizeof(BaseA)
$16 = (void *) 0x804a56c
(gdb) print (BaseA *)&ab
$17 = (BaseA *) 0x804a560
(gdb) print (BaseB *)&ab
$18 = (BaseB *) 0x804a56c
(gdb) print &ab.m_a
$19 = (int *) 0x804a578
(gdb) print (void *)&ab + sizeof(BaseA) + sizeof(BaseB)
$20 = (void *) 0x804a578
由上可知BaseA与BaseB所占内存比他们的成员所占内存多4字节,这4字节实际是指向虚函数表的指针;
由于类在继承时,如果父类有虚函数表,则子类不用再添加额外的虚函数表,只是将虚函数添加到父类的第一个虚函数表中,所以DeriveAB的大小是BaseA+BaseB+成员大小。
在子类实例指针转父类指针时,只是对子类实例指针偏移了一定量;BaseA是DeriveAB的第一父类,所以排在最前,与子类实例指针相同;BaseB是DeriveAB的第二父类,排在BaseA之后,子类实例偏移BaseA大小即可;成员变量m_a排在父类之后,偏移BaseA、BaseB大小即为m_a的位置。
II.虚函数表
i.gdb
BaseA虚函数表:
(gdb) print a
$8 = {_vptr.BaseA = 0x8048d40, m_a = 0, m_b = 0}
(gdb) x/3x 0x8048d40
0x8048d40 <_ZTV5BaseA+8>: 0x08048818 0x08048844 0x72654439
(gdb) x 0x08048818
0x8048818 <BaseA::a()>: 0x83e58955
(gdb) x 0x08048844
0x8048844 <BaseA::b()>: 0x83e58955
(gdb) x 0x72654439
0x72654439: Cannot access memory at address 0x72654439
BaseB虚函数表:
(gdb) print b
$9 = {_vptr.BaseB = 0x8048d30, m_b = 0, m_c = 0}
(gdb) x/3x 0x8048d30
0x8048d30 <_ZTV5BaseB+8>: 0x08048870 0x080488c8 0x00000000
(gdb) x 0x08048870
0x8048870 <BaseB::a()>: 0x83e58955
(gdb) x 0x080488c8
0x80488c8 <BaseB::c()>: 0x83e58955
DeriveAB虚函数表(BaseA部分):
(gdb) x/4x 0x8048d08
0x8048d08 <_ZTV8DeriveAB+8>: 0x080488fc 0x08048928 0x08048954 0xfffffff4
(gdb) x 0x080488fc
0x80488fc <DeriveAB::a()>: 0x83e58955
(gdb) x 0x08048928
0x8048928 <DeriveAB::b()>: 0x83e58955
(gdb) x 0x08048954
0x8048954 <DeriveAB::xa()>: 0x83e58955
DeriveAB虚函数表(BaseB部分):
(gdb) x/3x 0x8048d1c
0x8048d1c <_ZTV8DeriveAB+28>: 0x080488f4 0x080488c8 0x00000000
(gdb) x 0x080488f4
0x80488f4 <_ZThn12_N8DeriveAB1aEv>: 0x04244483
(gdb) x 0x080488c8
0x80488c8 <BaseB::c()>: 0x83e58955
可以看出BaseB中的b函数并没有声明为虚函数,所以未出现在虚函数表中
ii.BaseA、BaseB、DeriveAB虚函数表关系如下图所示
iii.BaseA、BaseB、DeriveAB、BaseC、DeriveABC虚函数表关系如下图所示
上图进一步说明了,子类的虚函数会添加到第一个虚函数表,即第一个虚基类中;第一个虚函数表中没有的所有子类虚函数都会添加到第一个虚函数表中,尽管这个虚函数在其它父类虚函数表中存大且会覆盖该父类的虚函数;
如DeriveABC::xxa(),第一个虚函数表中没有xxa()函数,所以将xxa()添加到第一个虚函数表中;
再如DeriveABC::c(),尽管会覆盖BaseB和BaseC中的c函数,但是第一个虚函数表中没有,也会添加到第一个虚函数表中