一个例子彻底搞懂C++的虚函数和纯虚函数

学习C++的多态性,你必然听过虚函数的概念,你必然知道有关她的种种语法,但你未必了解她为什么要那样做,未必了解她种种行为背后的所思所想。深知你不想在流于表面语法上的蜻蜓点水似是而非,今天我们就一起来揭开挡在你和虚函数(女神)之间的这一层窗户纸。

首先,我们要搞清楚女神的所作所为,即语法规范。然后再去探究她背后的逻辑道理。她的语法说来也不复杂,概括起来就这么几条:

  1.在类成员方法的声明(不是定义)语句前面加个单词:virtual,她就会摇身一变成为虚函数;
  2.在虚函数的声明语句末尾中加个 =0 ,她就会摇身一变成为纯虚函数;
  3.子类可以重新定义基类的虚函数,我们把这个行为称之为复写(override);
  4.不管是虚函数还是纯虚函数,基类都可以为他们提供实现(implementation),如果有的话子类可以调用基类的这些实现;
  5.子类可自主选择是否要提供一份属于自己的个性化虚函数实现;
  6.子类必须提供一份属于自己的个性化纯虚函数实现。

语法都列出来了,背后的逻辑含义是什么呢?我们用一个生动的例子来说明,虚函数是如何实现多态性的。

假设我们要设计关于飞行器的类,并且提供类似加油、飞行的实现代码,考虑具体情况,飞行器多种多样,有民航客机、歼击机、轰炸机、直升机、热气球、火箭甚至窜天猴、孔明灯、纸飞机!

假设我们有一位牛得一比的飞行员,他能给各式各样的飞行器加充不同的燃料,也能驾驶各式各样的飞行器。下面我们来看看这些类可以怎么设计。

首先,飞行器。由于我们假设所有的飞行器都有两种行为:加油和飞行。因此我们可以将这两种行为抽象到一个基类中,并由它来派生具体的某款飞行器。

1 // 这是一个描述飞行器的基类,提供了两个基本的功能:加油和飞行。
2 class aircraft {
3     virtual void refuel();  // 加燃油,普通虚函数
4     virtual void fly()=0;   // 飞行,纯虚函数
5 };
1 // 这是一个普通虚函数,意味着基类希望子类提供自己的个性化实现代码,但基类
2 // 同时也提供一个缺省的虚函数实现版本,在子类不复写该虚函数的情况下作为备选方案。
3 void aircraft::refuel() {
4     // 加通用型燃油
5     std::cout << "加通用燃油" << std::endl;
6 }
1 // 这是一个纯虚函数,意味着基类强制子类必须提供自己的个性化版本,否则编译将失败。
2 void aircraft::fly() {
3     // 一种不应该被使用的缺省飞行方案
4     std::cout << "一种不应该被使用的缺省飞行方案" << std::endl;
5 }

但让人惊奇的是,C++仍然保留了基类提供该纯虚函数代码实现的权利,这也许是给千变万化的实际情况留下后路。

有了基类aircraft,我们就可以潇洒地派生出各式各样的飞行器了,比如轰炸机和直升机:

 1 // 轰炸机类定义,复写了加油和飞行
 2 class bomber : public aircraft {
 3     void refuel() {}  // 加轰炸机的特殊燃油
 4     void fly() {}     // 轰炸机实弹飞行
 5 };
 6
 7 // 直升机类定义,复写了飞行代码,但没有复写加油
 8 class copter : public aircraft {
 9     void fly() {}  // 直升机盘旋
10 };

以上代码可以看到,直升机类(copter)没有自己的加油方式,直接使用了基类提供的缺省加油的方式。此时我们来定义一个能驾驭多机型的王牌飞行员类:

1 // 一个能驾驶各种飞行器的王牌飞行员
2 class pilot {
3     void refuelPlane(aircraft *p);
4     void dirvePlane(aircraft *p);
5 };
1 // 给我什么飞机我就加什么油
2 void pilot::refuelPlane(aircraft *p) {
3     p->refuel();
4 }
5
6 // 给我什么飞机我就怎么飞
7 void pilot::dirvePlane(aircraft *p) {
8     p->fly();
9 }

很明显,我们接下来要给这位很浪的飞行员表演一下操纵各种飞行器的机会,我们来定义各种飞机然后丢给他去处理。

1 // 定义两架飞机,一架轰6K,一架武直10
2 aircraft *H6K = new bomber;
3 aircraft *WZ10 = new copter;
1 // 来一个王牌飞行员,给H6K加油(加的是轰炸机特殊燃油),并且按照H6K的特点飞行
2 pilot Jack;
3 Jack.refuelPlane(H6K);  // 加轰炸机燃油
4 Jack.flyPlane(H6K);     // 轰炸机实弹飞行
5
6 // 给WZ10加油(加的是基类提供的通用燃油),按照WZ10的特点飞行
7 Jack.refuelPlane(WZ10);  // 加通用型燃油
8 Jack.flyPlane(WZ10);     // 直升机盘旋

上述代码体现了最经典的所谓多态的场景,给Jack不同的飞机,就能表现不同的结果。虚函数和纯虚函数都能做到这一点,区别是,子类如果不提供虚函数的实现,那就会自动调用基类的缺省方案。而子类如果不提供纯虚函数的实现,则编译将会失败。基类提供的纯虚函数实现版本,无法通过指向子类对象的基类类型指针或引用来调用,因此不能作为子类相应虚函数的备选方案。

下面给出总结:

  1.当基类的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供一个备选方案的时候,请将其设计为虚函数。例如飞行器的加油动作,每种不同的飞行器原则上都应该有自己的个性化的加充燃油的方式,但也不免可以有一种通用的燃油加充方式;
  2.当基类的某个成员方法,必须由子类提供个性化实现的时候,请将其设计为纯虚函数。例如飞行器的飞行动作,逻辑上每种飞行器都必须提供为其特殊设计的个性化飞行行为,而不应该有任何一种"通用的飞行方式";
  3.使用一个基类类型的指针或者引用,来指向子类对象,进而调用经由子类复写了的个性化的虚函数,这是C++实现多态性的一个最经典的场景;
  4.基类提供的纯虚函数的实现版本,并非为了多态性考虑,因为指向子类对象的基类指针和引用无法调用该版本。纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路;
  5.虚函数和普通的函数实际上是存储在不同的区域的,虚函数所在的区域是可被覆盖(也称复写override)的,每当子类定义相同名称的虚函数时就将原来基类的版本给覆盖了,另一侧面也说明了为什么基类中声明的虚函数在后代类中不需要另加声明一律自动为虚函数,因为它所存储的位置不会发生改变。而普通函数的存储区域不会覆盖,每个类都有自己独立的区域互不相干。

转自https://blog.csdn.net/vincent040/article/details/78848322,并对代码做了小幅修正,在此感谢原作者。

原文地址:https://www.cnblogs.com/chwei2ch/p/10628608.html

时间: 2024-11-15 02:45:27

一个例子彻底搞懂C++的虚函数和纯虚函数的相关文章

一个例子搞懂C++的虚函数和纯虚函数

转自https://blog.csdn.net/vincent040/article/details/78848322,并对代码做了小幅修正,在此感谢原作者. 学习C++的多态性,你必然听过虚函数的概念,你必然知道有关她的种种语法,但你未必了解她为什么要那样做,未必了解她种种行为背后的所思所想.深知你不想在流于表面语法上的蜻蜓点水似是而非,今天我们就一起来揭开挡在你和虚函数(女神)之间的这一层窗户纸. 首先,我们要搞清楚女神的所作所为,即语法规范.然后再去探究她背后的逻辑道理.她的语法说来也不复

一口气搞懂《虚函数和纯虚函数》

学习C++的多态性,你必然听过虚函数的概念,你必然知道有关她的种种语法,但你未必了解她为什么要那样做,未必了解她种种行为背后的所思所想.深知你不想在流于表面语法上的蜻蜓点水似是而非,今天我们就一起来揭开挡在你和虚函数(女神)之间的这一层窗户纸. 首先,我们要搞清楚女神的所作所为,即语法规范.然后再去探究她背后的逻辑道理.她的语法说来也不复杂,概括起来就这么几条: 在类成员方法的声明(不是定义)语句前面加个单词:virtual,她就会摇身一变成为虚函数. 在虚函数的声明语句末尾中加个 =0 ,她就

虚析构函数(√)、纯虚析构函数(√)、虚构造函数(X)

from:http://blog.csdn.net/fisher_jiang/article/details/2477577 一. 虚析构函数 我们知道,为了能够正确的调用对象的析构函数,一般要求具有层次结构的顶级类定义其析构函数为虚函数.因为在delete一个抽象类指针时候,必须要通过虚函数找到真正的析构函数. 如: class Base{public:   Base(){}   virtual ~Base(){}};class Derived: public Base{public:   D

C++中的虚析构函数、纯虚析构函数详解

C++中析构函数可以为纯虚函数吗? 众所周知,在实现多态的过程中,一般将基类的析构函数设为virtual,以便在delete的时候能够多态的链式调用.那么析构函数是否可以设为纯虚呢? class CBase { public: CBase() { printf("CBase()\n"); } virtual ~CBase() = 0; // 析构函数是纯虚函数 }; 答案是可以,那么这样实现的目的是什么呢?当然是避免实例化. 但因为派生类不可能来实现基类的析构函数,所以基类析构函数虽然

【C++】C++中的虚函数与纯虚函数

C++中的虚函数 先来看一下实际的场景,就很容易明白为什么要引入虚函数的概念.假设我们有一个基类Base,Base中有一个方法eat:有一个派生类Derived从基类继承来,并且覆盖(Override)了基类的eat:继承表明ISA(“是一个”)的关系,现在我们有一个基类的指针(引用)绑定到派生类对象(因为派生类对象是基类的一个特例,我们当然可以用基类指针指向派生类对象),当我们调用pBase->eat()的时候,我们希望调用的是Derived类的eat,而实际上调用的是Base类的eat,测试

C++学习笔记(十二):类继承、虚函数、纯虚函数、抽象类和嵌套类

类继承 在C++类继承中,一个派生类可以从一个基类派生,也可以从多个基类派生. 从一个基类派生的继承称为单继承:从多个基类派生的继承称为多继承. 1 //单继承的定义 2 class B:public A 3 { 4 < 派生类新定义成员> 5 }; 6 //多继承的定义 7 class C:public A,private B 8 { 9 < 派生类新定义成员> 10 }; 我们这篇主要说单继承. 派生类共有三种C++类继承方式: 公有继承(public) 基类的公有成员和保护成

虚函数和纯虚函数的区别

首先:强调一个概念定义一个函数为虚函数,不代表函数为不被实现的函数.定义他为虚函数是为了允许用基类的指针来调用子类的这个函数.定义一个函数为纯虚函数,才代表函数没有被实现.定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数.1.简介假设我们有下面的类层次: class A { public: virtual void foo() { cout<<"A::foo() is called"<<endl; } }; class

虚函数、纯虚函数、抽象类、接口 (Java_C++_C#)

在OOP编程语言中,多态是很重要的OOP思想.而多态的语法支持离不开虚函数.每次都把什么虚函数,抽象类搞混,这次好好整理下! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

抽象类,虚函数,纯虚函数的意义

C语言是面向过程的语言,C++是面向对象的语言,区分它们面向什么的重要区别在于C++比C多个类.那么在我看来,抽象就是类的升华. 一般刚学习C++的时候,抽象这个东西给人最大的感觉就是太抽象,很难理解.心里总是想着,其实这样或那样就能解决这个问题了,为什么要学这个?增加一个抽象类还增加一段代码,费事不说还不容易理解,所以当时我对抽象还是很抗拒的.但是当工作中真正用到这个的时候,就觉得这个东西真是太好了,任何其它的方案都无法代替抽象. 为什么这样说呢?首先C++是强类型语言,对于一个数组或链表来讲