关于虚函数

虚函数好像是一个比较喜欢考的东西

本文大部分转载于 这里 还有一部分是在网上拼凑出来的

虚函数的作用

虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数. 虚函数是实现多态(动态绑定)/接口函数的基础

虚函数的实现

编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表

虚函数表中存储的是中的虚函数的地址

如果不使用指针调用, 虚函数调用不会发生动态绑定

1
2
A a;
A* p=&a;

调用a.func()不会发生动态绑定

调用p->func() 会,因为p不知道所指向的对象到底是A还是继承于A的对象

虚函数的额外开销

使用虚函数时,对于内存和执行速度方面会有一定的成本:

每个对象都会变大,变大的量为存储虚函数表指针

对于每个类,编译器都会创建一个虚函数表

对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址

虚函数和纯虚函数

虚函数 纯虚函数
可以直接使用 必须在派生类中实现后才能使用
virtual 除了加上virtual 关键字还需要加上 =0
必须实现 不用实现

析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象

纯虚函数的作用

很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做

虚函数表

存在虚函数的类都有一个一维的虚函数表叫做虚表,每一个类的对象都有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的

虚函数表存放的内容:类的虚函数的地址
虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。

1
2
3
4
5
6
7
8
9
10
11
class Base1
{
public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}
};

Base1 b1,b2;
类对象的布局情况

虚表与继承

单继承无覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base1
{
public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2;

virtual void derive1_fun1() {}
};
子类的虚表会紧跟父类的虚表(虚函数按照声明顺序放于表中)

由于Base1只知道自己的两个虚函数索引[0]和[1], 所以就算在后面加上了[2], Base1根本不知情, 不会对它造成任何影响.

单继承有覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base1
{
public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2;

// 覆盖基类函数
virtual void base1_fun1() {}
};
覆盖的子类会替代原先父类虚表的位置,其余的子类虚表会紧跟父类

多继承无覆盖

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
class Base1
{
public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}
};

class Base2
{
public:
int base2_1;
int base2_2;

virtual void base2_fun1() {}
virtual void base2_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;

// 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
子类的虚表会紧跟第一个父类的虚表,其他的父类虚表中不会出现子类的虚表

多继承有覆盖

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
class Base1
{
public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}
};

class Base2
{
public:
int base2_1;
int base2_2;

virtual void base2_fun1() {}
virtual void base2_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;

// 基类虚函数覆盖
virtual void base1_fun1() {}
virtual void base2_fun2() {}

// 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
覆盖的子类会替代所有父类同名虚函数在虚表中的位置,其余的子类虚表会紧跟第一个父类的虚表

基类没有虚函数

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 Base1
{
public:
int base1_1;
int base1_2;
};

class Base2
{
public:
int base2_1;
int base2_2;
};

// 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;

// 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
子类的虚函数指针会被放在最前面

总结:

子类虚表跟在第一个父类后面,有覆盖则覆盖

有虚表的放前面

关于构造函数和析构函数与虚函数的问题

为什么构造函数不能定义为虚函数

如果父类的构造函数设置成虚函数,那么子类的构造函数会直接覆盖掉父类的构造函数。而父类的构造函数就失去了一些初始化的功能。这与子类的构造需要先完成父类的构造的流程相违背了。

虚函数的调用是需要通过“虚函数表”来进行的,而虚函数表也需要在对象实例化之后才能够进行调用。在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则。

虚函数是运行中的多态,而对象的构建是由编译器完成的

虚函数的调用是由父类指针进行完成的,而对象的构造则是由编译器完成的,由于在创建一个对象的过程中,涉及到资源的创建,类型的确定,而这些是无法在运行过程中确定的,需要在编译的过程中就确定下来。而多态是在运行过程中体现出来的,所以是不能够通过虚函数来创建构造函数的,与实例化的次序不同也有关系。

为什么基类的析构函数常定义为虚函数

在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数

由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。

虚函数的内联

ps:虚函数存在的意义是运行时的多态,所以虚函数的内联基本没有意义

虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联

内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联

inline virtual 唯一可以内联的时候

编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生