一、虚函数

前面已经简单介绍过虚函数了,这里再稍微带下:虚函数就是那些被声明为virtual,并在派生类中重新定义的成员函数。

1.1、虚函数的作用

虚函数让派生类可以对基类中的函数进行动态覆盖,是实现c++多态性的一大工程

1.2、虚函数是怎么实现的

虚函数的实现是依赖虚拟表。用比较通俗的话解释,就是有一个隐藏的指向基类的指针,我们称为vptr,vptr在创建类实例时自动设置,以便指向该类的虚拟表。这里需要特别注意的是:vptr是一个真正的指针,这代表它会占一个指针大小的内存,也意味着vptr会被派生类继承。 而虚拟表中,原理是一个静态数组,保存着一个虚函数对应的其他实例的地址。

虚拟表的实现原理:如果你创建一个类,并且类中有定义了虚函数,那么编译器编译的时候就会生成该类的虚函数表,并且为它分配指针,而虚函数表的大小,取决于你定义了多少个虚函数,定义得越多,占用内存越大。不管是基类还是派生类,每个类都有一个虚函数表,并且这个虚函数表中指向的函数都是自己类的函数位置。对于派生类来说,会复制一份基类的虚函数表,如果其中某个虚函数重写了,则会使用新的地址替换旧地址。

1.3、虚函数传参

先看看下面例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class Base {
public:
virtual void fun(int x = 10) { cout << "Base::fun(), x = " << x << endl; }
};

class Derived : public Base {
public:
virtual void fun(int x = 20) { cout << "Derived::fun(), x = " << x << endl; }
};

int main() {
Derived d1;//定义派生类
Base *bp = &d1;//定义一个Base类型的指针,然后指向Derived
bp->fun(); // 结果输出为10
return 0;
}


默认参数是静态绑定的,虚函数的是动态绑定的,所以调用的是派生类的fun,但是打印出来的值是基类上fun的参数值

  • main() 中,bpBase* 类型的指针,尽管它指向的是 Derived 类型的对象,但它的静态类型是 Base*
  • 因此,编译器会使用 Base::fun() 的默认参数,而不是 Derived::fun() 的默认参数。编译器选择 Base::fun() 时,默认参数 x = 10 会被采用。

1.4、虚函数与其他函数的关系

1.4.1、静态函数可以声明为虚函数吗?

不行
之所以虚函数不可以为static函数,是因为虚函数可以实现多态性,也就是同一个函数可能有不同的实现,这个是在调用时才决定调用哪个类的函数,但是如果定义为static,那么在调用类的时候就已经分配好内存了,无论怎么调用都是调用的同一个函数

1.4.2、构造函数可以是虚函数吗?

不行
构造函数不可以声明为虚函数。同时除了inline和explicit之外,构造函数不允许使用其它任何关键字。
如果类中有虚函数,那么编译器会在构造函数中添加代码来创建vptr。如果构造函数时虚的,那么它也需要vptr来访问vtable,可这个时候vptr还没有产生。

1.4.3、析构函数可以是虚函数吗?

可以
析构函数可以声明为虚函数。甚至可以说,析构函数最好都定义为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。事实上,只要一个类有可能会被其他类继承,就应该声明虚析构函数。为什么??
先看下面这个例子来帮助理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class Base {
public:
~Base() {
cout << "Base destroyed" << endl;
}
};

class Derived : public Base {
public:
~Derived() {
cout << "Derived destoyed" << endl;
}
};

int main() {
Base *bp = new Derived;
delete bp; //使用基类指针删除派生类对象
}

输入结果如下:只调用到了基类的析构函数去释放资源,而没有调用派生类的析构函数
Base destroyed

只有将基类的析构函数定位为虚函数,才可以同时调用到基类和派生类的析构函数。

这里衍生出一个题外话:那为什么默认的析构函数并没有定义为虚函数呢?
前面我们已经介绍过,一个类有虚函数时,在构造函数中会生成一个vptr指针和vtable,而vptr的一个真实存在的指针类型,会占用内存,如果默认析构函数就定义为虚函数,会造成额外的内存损耗。

1.4.4、友元函数可以是虚函数吗?

不行
因为友元函数不属于类函数,只有类函数才可以为虚函数。