C++ 虚函数的神奇效用
0X00 前言
可能大多数人开始学C++和我一样,对于虚函数(virtual)这个词,有点似懂非懂、云里雾里的感觉。
今天我们就把这个虚函数好好唠清楚
0X10 何为虚函数?
其实虚函数没有那么大假大空的定义,就一句话:
在类定义的函数中,如果用限定词
vitual
修饰的就是虚函数
怎么样,是不是简单利索不墨迹
不过还是来一点更实在的,直接上代码
我们先定义一个类,类名是Animal,将其作为基类
class Animal
{
public:
Animal(){
cout<<"I'm an animal!"<<endl;}
void sleep(){
cout<<"I'm sleep"<<endl;}
virtual eat(){
cout<<"I'm eating"<<endl;}
virtual ~Animal(){
cout<<"Remember me,I'm an animal! Bye!"<<endl;}
}
从上面的类方法可以看到,定义了构造函数,一个普通的成员函数 sleep(),一个虚函数 eat(),并且一个析构函数,至于为什么析构函数也是虚函数,这个要等到后面才能揭晓。
现在大家只需要好好的看看我们这个虚函数,记住它的模样
0X20 虚函数何用?
上面认识了虚函数了,那有人要问了:要这玩样有啥用呢?
别急,要想说清这个,我们还需要做一件事。
既然有基类那么是不是就应该有派生类?没错滴
下面我们还需要新建一个派生类Dog,它继承了我们前面的Animal类
class Dog:public Animal
{
public:
Dog(){
cout<<"I'm a dog"<<endl;}
void sleep(){
cout<<"I'm a dog and I'm sleep"<<endl;}
virtual void eat(){
cout<<"I'm eating bones"<<endl;}
virtual ~Dog(){
cout<<"Remember me,I'm a dog! Bye!"<<endl;}
}
好了,继承完毕,可以看到有一个Dog构造函数,并且重写了Animal的函数sleep()和eat(),以及后面的析构函数
Dog类写好了,我们就需讲 虚函数 的作用到底是干嘛的了
首先,我们知道(默认知道233)基类的指针指向派生类,基类的引用可以对派生类的对象进行引用
因此我可以有如下代码:
Animal animal;
Dog dog;
Animal * fa1=&base;
Animal * fa2=&dog;
上述代码定义了一个Animal类对象animal,定义了一个Dog类对象dog。
并且定义了两个基类的指针 fa1,fa2分别指向了animal和dog
现在我们分别通过指针来调用对象的方法:
cout<<"animal:"<<endl;
fa1->sleep();
fal->eat();
cout<<"dog:"<<endl;
fa2->sleep();
fa2->eat();
通过两个指针来调用两个对象的方法,那么结果到底如何呢?
是不是有点奇怪呢?
明明两个函数都进行了重写,为什么fa2调用的是Animal::sleep()
而不是Dog::sleep()
原因很简单:一个是虚函数而另一个不是!
没错了,这就是虚函数的功用:
当指向派生类的指针(或引用)调用类函数时,对于虚函数,会调用派生类的虚函数,而一般的成员函数只会调用基类的函数
可能乍听起来有点绕,但是通过以上的例子,想必大家应该是清楚了。
0X30 虚函数究竟何用?
但是看了上面解释,有些同学又该问了,听是听懂了,但这感觉并没有啥用啊。似乎有点鸡肋。
这样想你就错了
1、为了多态
其实在C++中,很多东西都是为了多态而设计的。
那么这个虚函数究竟是为了哪门子多态呢?
好,我们加入设计一个函数,在这个函数里面需要调用"动物"的动作。当然这里的动物包括我们的 Animal 也包括Dog。也就是说我们需要传递一个类的引用到函数中去
现在假设,我们从来都没有虚函数这个东西(或者说我们现在要调用 sleep 这个函数)
那我们的函数应该怎么设计了?
我想应该是这样的:
void action(Animal &animal)
{
//.....
animal.sleep();
//....
}
void action(Dog &dog)
{
//.....
dog.sleep();
//....
}
我们假设省略的东西是一样的,那得多麻烦啊是不?
就一句话不一样就需要写一个函数,又浪费空间又浪费时间。
那要是我们现在有虚函数了呢?(也就是调用eat函数)
这个时候只需要一个函数就可以了
void action(Animal &fa)
{
//.....
fa.sleep();
//....
}
为什么呢?
既然基类引用既可以对基类进行引用也可以对派生类引用,那么传递进来的不管是基类对象还是派生类对象,都是可以的。
并且刚才都说了,对于虚函数,如果是基类的引用就是调用基类方法,如果是派生类引用就调用派生类方法
那一个函数就可以啦,这比没有虚函数时好多了,这是啥——多态啊!
2、为了避免内存泄露
等等,咋扯扯扯,扯到内存泄露来了?
如果你有疑惑了,那就对了,这正是最容易出错的地方!
我们再回到前面,是不是有一个坑没填上?没错,为什么析构函数也用了 virtual
来修饰?
我们知道(再次强行知道 (狗头)),析构函数一般是用来释放之前申请的堆空间的。虽然我这里没有,但是一般来讲析构函数就是用来做这个事的。我现在用cout来区分两个类而已
我们现在执行以下函数,但这次我们将不用virtual
来修饰析构函数
Animal* fa=new Dog();
//...
delete fa;
这次我们用Animal指针来new了一个Dog对象,然后执行一系列操作之后就将其delete掉
最后的执行结果如下:
发现只出现了这一句话呢,也就是说只调用了 Animal 的析构函数
那这问题就严重了,我定义的是一个Dog对象,要是我在Dog中申请了一大堆的堆内存,那么岂不是没有释放了,这是什么——内存泄露啊!
怎么解决呢?——虚函数
我们将析构函数设置为虚函数再运行一遍看看
现在舒服了,Dog的析构函数也调用了,不用怕内存泄露了
原因很简单,因为是虚函数,所以此时会调用派生类的析构函数,然后在调用基类的析构函数,而不是只调用基类的。
0X40 虚函数小总结
有以下几个要点:
- 在基类方法声明中使用关键字
virtual
可使得该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的(也就是说,其实我们派生类中对用的虚函数其实可以不用写virtual,因为会自动成为虚函数,但是我们还是习惯上写上用以区分 ) - 如果使用指向对象的引用或者指针来调用虚函数,程序将使用对象类型定义的方法,而不是使用为引用或指针类型定义的方法。这称为动态联编。这种行为十分重要,因为这样基类指针或者引用可以指向派生类对象
- 如果定义的类将被作为基类,则应该将那些要在派生类中重新定义的类方法定义为虚的
还有几点需要注意:
1、构造函数
构造函数不能是虚函数,因为构造函数的调用顺序不用于继承机制。派生类并不会继承基类的构造函数,所以将构造函数定义为虚函数没有任何意义
2、析构函数
析构函数应该是虚函数。原因刚才讨论过了,为了避免内存泄漏
3、友元函数
友元函数不能是虚函数,因为友元函数不是类成员,只有类成员才能是虚函数
4、没有重新定义
如果派生类没有重新定义函数,将使用该函数的基类版本
0X50 后言
关于虚函数的内容就到这里啦,只要大家能够理解它的作用以及设计者的初心,应该就能容易记住并且运用起来得心运手了。
最后祝大家天天向上,好好学习~
下次见!
——————————————————————————————————————————————————————
参考:《C++ primer plus 第6版》