C++ 虚函数的神奇效用

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版》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值