虚函数原理

定义

如果没有使用关键字 virtual ,程序将根据引用类型或指针类型选择方法;如果使用了 virtual 关键字,程序将根据引用或指针指向的对象的类型来选择方法(C++ Primer Plus)

例如 不使用关键字 virtual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Father
{
void func1(){};
void func2(){};
};

class Son:public Father
{
void func1(){};
void func2(){};
};

void main()
{
Father father;
Son son;
Father *pFather1 = &father;
Father *pFather2 = &son;
pFather1->func1(); //call Father::func1()
pFather2->func1(); //call Father::func1()
}

使用关键字 virtual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Father
{
virtual void func1(){};
virtual void func2(){};
};

class Son:public Father
{
virtual void func1(){}; //virtual可省略
virtual void func2(){}; //同上
};

void main()
{
Father father;
Son son;
Father *pFather1 = &father;
Father *pFather2 = &son;
pFather1->func1(); //call Father::func1()
pFather2->func1(); //call Son::func1()
}

所以如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的

这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本

虚函数的原理

虚函数表

只有在拥有关键字 virtual 修饰的方法才会产生虚函数表

  • 虚函数表的地址存放在对象的首地址中
  • 存放虚函数地址的数组称之为虚表,对象中指向虚表的地址称之为虚表指针
  • 一般虚表放在全局数据区
  • 含有虚表指针的类比没有虚表指针的类大4个字节
  • 虚函数调用,首先拿到对象的首地址,从对象的首地址取出虚表指针,然后从虚表取出对应的虚函数的地址,调用虚函数,这种调用方式称之为虚调用(间接调用)。
  • 每个类都有自己的虚表,每个类都在自己的构造函数中填入自己的虚表指针,这个实际构造函数执行时间早
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father
{
virtual void func1(){};
virtual void func2(){};
};

class Son:public Father
{
virtual void func3(){};
virtual void func4(){};
};

void main()
{
Son son;
}

创建 Son 实例,打开内存窗口查看 son 内存

1
2
0x0019FF0C  6c 68 41 00  lhA.
0x0019FF10 cc cc cc cc ????

首地址为虚表指针并指向 0x0041686c

打开 0x0041686c

以下为虚函数表

1
2
3
4
5
0x0041686C  2d 10 41 00  -.A.
0x00416870 77 11 41 00 w.A.
0x00416874 cd 10 41 00 ?.A.
0x00416878 6e 10 41 00 n.A.
0x0041687C 00 00 00 00 ....

最后的 0x00000000 为虚函数表终止符

打开反汇编窗口输入地址,分别找到它们对应基类和子类的成员函数

0x0041102d 0x00411177 0x004110cd 0x0041106e
Father:func1() Father:func2() Son::func3() Son::func4()

如果将类 Son 中的成员函数 func3() 改为 func1()

1
2
3
4
5
6
7
8
9
10
11
class Father
{
virtual void func1(){};
virtual void func2(){};
};

class Son:Father
{
virtual void func1(){};
virtual void func3(){};
};

此时虚函数表的内容为

1
2
3
4
0x0041686C  17 12 41 00  ..A.
0x00416870 77 11 41 00 w.A.
0x00416874 6e 10 41 00 n.A.
0x00416878 00 00 00 00 ....

对应的方法为

0x00411217 0x00411177 0x0041106e
Son::func1() Father:func2() Son::func3()

可以看到 Son 的虚函数表中,此时的 fun1() 不再是 Father:func1()

而是 Son::func1() 对其进行了覆盖

为何需要设置虚析构函数

在使用delete释放由new分配的对象代码时,如果析构函数不是虚的,则将只调用对应于指针类型的析构函数

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Father
{
public:
~Father(){};
}
class Son : public Father
{
public:
~Son(){};
};

void main
{
Son *son = new Son();
Father *pFather = son;
delete(pFather); //call Father::~Father()
};

此时仅调用了父类 Father 中的析构函数

如果在析构函数中添加了 virtual 关键字,则析构将正常进行,顺序为先子类后父类

语法细节

  • 在子类的一般成员函数中调用虚函数,也存在多态效果

  • 在父类的一般成员函数中调用虚函数,存在多态效果

  • 在构造成员函数中调用虚函数,没有多态效果

  • 在析构成员函数中调用虚函数,没有多态效果

  • 构造成员函数不能设置为虚函数

  • 析构成员函数必须是虚函数