C++速成(2)

转载自:
http://www.cnblogs.com/kkdd-2013/p/5370094.html

  • C继承25h
    • 1 为什么要继承
    • 2 内存中的对象
    • 3 继承代码实践
    • 4 继承方式
      • 公有继承
    • 基类成员访问属性 继承方式 派生类成员访问属性 private成员 public 无法访问 protected成员 protected public成员 public
      • 保护继承和私有继承
    • 5 继承中的特殊关系
      • 隐藏
      • 隐藏代码实践
    • 6 is-a
      • 什么是is-a
      • 存储结构
      • is-a编码实践
    • 7 多继承与多重继承
      • 多重继承
      • 多继承
      • 多重继承编码实践
      • 多继承代码实践
    • 8 虚拟继承
      • 多继承多重继承的烦扰
      • 虚继承
      • 虚继承编码实践
    • 6 练习题
  • C多态3h
    • 1 多态概述
      • 静态多态早绑定
      • 动态多态晚绑定
    • 3 虚函数代码实践
    • 4 虚析构函数
    • 5虚析构函数代码实践
    • 6 虚函数与虚析构函数原理
      • 函数指针
      • 函数的覆盖与隐藏
      • 虚析构函数的实现原理
      • 虚函数表代码实践
    • 2 纯虚函数与抽象类
      • 纯虚函数的实现原理
      • 纯虚函数和抽象类的代码实践
    • 22 C接口类
    • 接口类
    • 接口类代码实践
    • RTTI运行时类型识别
    • RTTI代码实践
    • 异常处理
    • 异常处理代码实践
    • 5 练习题

5. C++继承2.5h

5.1 为什么要继承

我们先从下面这个例子开始讲起。

C++速成(2)_第1张图片

我们在这里定义了一个人的类(Person),其中有数据成员:姓名和年龄,还有一个成员函数吃饭(eat)。我们还定义了一个工人的类(Worker),我们知道工人是人类的一种,所以其肯定也有自己的姓名和年龄,肯定也要吃饭。当然,对于工人来说,其还要工作,因为工作才会有薪水,所以,我们发现,作为工人来说,其比人类具有更多的属性,并且具有所有的人类特性。那么既然人类中的所有数据成员和成员函数放在工人的类中都适用,那能不能在这种情况下,就让工人类和人类产生某种关系,从而在定义工人类的时候,不重复写这些代码,而减轻程序员的工作呢?其实,这在C++中是可以的,几遍如此,也是有前提条件的,其前提条件就是发生关系的这两个类必须具备包含关系(如下)

C++速成(2)_第2张图片

有了这样的概念关系,我们就可以将程序代码优化成如下形式:

C++速成(2)_第3张图片

这样在定义工人类的时候,就不需要再定义人类共有的属性,只需要定义自己特有的属性。这就是说工人类继承了人这个类。工人类就是人这个类的派生类,人这个类就是工人类的基类;也可以把人这个类和工人类分别称为父类和子类。

5.2 内存中的对象

在上面的内容中,我们已经初步讲了继承的语法,下面将为大家讲述在内存中作为对象来说,子类和父类(或者说派生类和基类)是怎样的关系。我们还是以刚才的人这个类和工人类为例,我们来看一下,在内存中人这个类如果实例化一个对象,那么它就会有两个数据成员(一个是姓名,一个是年龄),而工人类在实例化一个对象后,虽然我们在工人类中去定义姓名和年龄,但是因为工人类继承了人这个类,所以由工人类实例化出一个对象后,就已经包含了人这个类中的两个数据成员(姓名和年龄),同时还有一个自己特有的数据成员(薪水)。这就是人这个类和工人类实例化对象后在内存中的分布(如下)

C++速成(2)_第4张图片

5.3 继承代码实践

题目描述:

/* ************************************************************/

/* 继承

要求:

​ 1.定义Person类

​ 数据成员:姓名(m_strName)和年龄(m_iAge)

​ 成员函数:构造函数、析构函数、eat()函数

​ 2.定义Worker类

​ 公有继承Person类,特有数据成员(工资 m_iSalary)

​ 成员函数:构造函数、析构函数、work()函数

​ 目的:

​ 1.实例化Worker对象的时候,到底是先调用谁的构造函数,

​ 在销毁Worker这个对象的时候,又是先调用谁的析构函数

​ 2.子类继承了父类后,观察是否继承了父类的数据成员和成员函数

/***************************************************************/

程序框架:

C++速成(2)_第5张图片

头文件(*Person.h*

#include
using namespace std;

class Person
{
public:
    Person();
    ~Person();
    void eat();
    string m_strName;
    int  m_iAge;
};

源程序(*Person.cpp*

#include"Person.h"
#include
using namespace std;

Person::Person()
{
    cout <<"Person()"<< endl;
}
Person::~Person()
{
    cout <<"~erson()"<< endl;
}

voidPerson::eat()
{
    cout <<"eat"<< endl;
}

头文件(*Worker.h*

#include"Person.h"

class Worker:public Person//Worker公有继承Person类
{
public:
    Worker();
    ~Worker();
    void work();
    int m_iSalary;
};

源程序(*Worker.cpp*

#include"Worker.h"
#include
using namespace std;

Worker::Worker()
{
    cout <<"Worker()"<< endl;
}

Worker::~Worker()
{
    cout <<"~Worker()"<< endl;
}
void Worker::work()
{
    cout <<"work()"<< endl;
}

主调程序(*demo.cpp*):

根据程序的第1个目的是看实例化Worker对象的时候,到底是先调用谁的构造函数,在销毁Worker这个对象的时候,又是先调用谁的析构函数,所以我们在这里只在堆上实例化一个Worker对象,然后再销毁它

#include
#include
#include"Worker.h"

using namespace std;

int main()
{
    Worker *p = new Worker();
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

运行结果:

C++速成(2)_第6张图片

从运行结果来看,我们实例化了一个Worker类的对象(也就是子类或派生类),首先执行的是父类(也就是基类)的构造函数。可见,如果想要实例化一个派生类,必然先要实例化一个基类(当然,这种实例化是隐性的)。而在销毁的时候,先执行的是子类的析构函数,后执行的是父类的析构函数,可见析构函数的执行顺序与构造函数的执行顺序刚好相反。

根据第2个目的:子类继承了父类后,观察是否继承了父类的数据成员和成员函数

#include<iostream>
#include<stdlib.h>
#include"Worker.h"

using namespace std;

int main()
{
    Worker *p = new Worker();
    p->m_strName = "Keiven";
    p->m_iAge = 20;
    p->eat();
    p->m_iSalary = 5000;
    p->work();
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

运行结果:

C++速成(2)_第7张图片

从运行结果看到,除了打印出构造函数和析构函数的相关调用之外,还打印出eat()和work(),前者是父类的成员函数,可见子类可以访问父类的成员函数,此外也可以证明可以顺利访问自己的成员函数,同时,由于没有报语法错误,也可以证明子类访问父类的数据成员和自己的数据成员。


5.4 继承方式

这节课来学习继承的方式。既然有继承的关系,就一定会有以某种方式来继承的问题。C++中有三种继承方式,如下:

C++速成(2)_第8张图片

公有继承

当两个类具有继承关系,并且是以公有继承的方式来继承的话,那么基类中的public成员将会被继承到派生类中的public下面;基类中的protected成员也会被继承到派生类的protected下面,但基类的private成员将会无法访问,也就是说,没有在派生类的private下面继承基类的private成员。这样也就造成了派生类无法像访问自己的private成员那样去访问基类的private成员。

基类成员访问属性 继承方式 派生类成员访问属性
private成员 public 无法访问
protected成员 protected
public成员 public

保护继承和私有继承

当两个类具有继承关系,并且是以保护继承的方式来继承的话,那么基类中的public成员和protected成员都会被继承到派生类的protected下面,但基类的private成员会被继承,但也会无法访问。

基类成员访问属性 继承方式 派生类成员访问属性
private成员 protected 无法访问
protected成员 protected
public成员 protected

当两个类具有继承关系,并且是以私有继承的方式来继承的话,那么基类中的public成员和protected成员都会被继承到派生类的private下面,但基类的private成员会被继承,不过也会无法访问。

基类成员访问属性 继承方式 派生类成员访问属性
private成员 private 无法访问
protected成员 private
public成员 private

在这三种继承方式当中,以private方式最为特殊,为了说明这种特殊性,我们来看前面学习过的例子。

C++速成(2)_第9张图片

在这个例子中,我们可以看到,在线段类中有两个坐标类的对象(一个是A点m_coorA,一个是B点m_coorB)。那么这样的一种关系呢,大家会发现,线段类只能够访问到A点和B点这两个对象的公有数据成员和公有成员函数。说道这,我们再回过头来看一看刚刚前面学过的私有继承。在私有继承中,子类的对象也只能访问父类的公有数据成员和公有的成员函数,大家是不是也觉得这两点是不是很相似呢?在C++中,我们把这个例子中的线段类和坐标类的这种关系就称之为Has a关系,也就是说在线段中有一个坐标点,这种关系是一种包含关系。而在私有继承中也是一种包含关系,即当我们定义了一个子类的对象的时候,子类对象中就包含了一个父类对象(因为它只能访问父类中公有数据成员和公有的成员函数),所以,从本质上来说,私有继承也是一种Has a的关系。


5.5 继承中的特殊关系

在C++中,有两个非常重要的概念,那就是覆盖和隐藏。本节主要讲解隐藏。那么什么是隐藏呢?

隐藏

下面通过一个例子来说明隐藏这个概念。

C++速成(2)_第10张图片

假设我们先定义了一个父类A,并在父类A中定义了一个成员函数ABC(),然后,我们又定义了一个子类B,子类B公有继承了父类A,并且在子类B中也定义了一个同名的成员函数ABC()。这个时候,大家会发现,A中有一个ABC(),B中也有一个ABC(),而B继承了A之后,理论上在B中就应该拥有了A中所定义的成员函数ABC()。这时,子类B中的ABC()函数就会隐藏掉父类A中的ABC()函数。隐藏的特性也主要体现在,当实例化B的对象的时候,使用该对象只能够直接的访问到子类B中的ABC()成员函数,而无法访问父类A中的ABC()成员函数。从使用的体验上来说,父类A中的ABC()函数就似乎已经被隐藏以来了,但是实际上,父类A中的ABC()函数确实被继承到了子类B中,并且可以通过特殊的手段访问到父类A中的ABC()函数,所以,我们就将这种特性称为隐藏。当然,同名的隐藏不仅限于成员函数,从语法的角度来说,其实,同名的数据成员也具有隐藏的特征。比如说父类A中有一个数据成员x,子类B中也有一个数据成员x,那么,当它们具有继承关系后,父类中的x就会被隐藏起来,只不过因为存在父子关系的两个类之间的数据成员如果同名实在没有什么实际的意义,所以在实际的使用当中,十分罕见。综上所述,我们归纳为三个关键字:父子关系、成员同名、隐藏。那么这三个关键字呢就已经描述了隐藏的概念。接下来看一个代码的实例。

C++速成(2)_第11张图片

在这个例子中,我们定义了一个Person人这个类,在这个类中有一个成员函数play(),也就是玩耍的意思(作为人来说,都喜欢玩耍),另外还有一个数据成员叫做名字。另外,还定义了一个Soldier类(士兵),并且公有继承了Person这个类,在士兵这个类中,也定义了有一个同名的成员函数play()(注意,肯定士兵的玩法与普通人类的玩法会有所不同),当然,士兵这个类中还有其他的成员函数(work())以及数据成员(Code)(因为,对于士兵来说,都会有各自的编号)。接下来,我们看一下真正访问的时候,注意红色标记的代码。

C++速成(2)_第12张图片

当我们使用Soldier这个类去实例化一个soldier对象之后,那么我们使用这个对象直接去调用play()的时候,那么它将调用到Soldier这个类自己定义的play()函数。而如果我们想要通过soldier这个对象来调用到他的父类中的play()函数,我们就必须通过第二行红色代码方式(soldier.Person::play();)。如果我们在定义数据成员的时候,将父类的数据成员与子类的数据成员定义成同名的话,比如说在Person类中定义了一个string类型的编号(string code;),在Soldier类中也定义了一个int类型的编号(int code;),这种定义习惯非常不可取,在概念上容易混淆,如果想要去访问它呢,我们往往会在成员函数中去访问,因为这两个数据成员都定义在了protected限定符的下面,用实例化的对象无法直接访问到(五路子类还是父类),那它使用在各自的成员函数中。我们来看一下,如果在Soldie的成员函数中去使用code的话呢(code = “1234”;),这个code访问到的就是Soldier这个类本身定义的code,而如果要访问从Person这个类继承下来的这个code数据成员呢,就必须使用(Person::code = “5678”;)。


隐藏代码实践

题目描述:

/*******************************/

/* 继承关系中的隐藏

要求:

​ 1. Person类,数据成员:m_strName,成员函数:构造函数、play()

​ 2. Soldier类,数据成员:无,成员函数:构造函数、play()、work()

/*******************************/

程序框架:

C++速成(2)_第13张图片

头文件(*Person.h*

#include
usingnamespace std;

classPerson
{
public:
    Person();
    void play();
protected:
    string m_strName;
};

源程序(*Person.cpp*

#include"Person.h"
#include
usingnamespace std;

Person::Person()
{
    m_strName = "Mery";
}
voidPerson::play()
{
    cout <<"Person---play()"<< endl;
    cout << m_strName << endl;
}

头文件(*Soldier.h*

#include"Person.h"//这里如果不包含这个头文件,编译时就会出现“Person”未定义基类

classSoldier:publicPerson
{
public:
    Soldier();
    void play();
    void work();
protected:
};

源程序(*Soldier.cpp*

#include"Soldier.h"
#include
usingnamespace std;

Soldier::Soldier()
{
}
voidSoldier::play() 
{
    cout <<"Soldier---play()"<< endl;
}
voidSoldier::work()
{
    cout <<"work()"<< endl;
}

主调函数(*demo.cpp*

#include
#include
#include"Soldier.h"

usingnamespace std;

int main()
{
    Soldier soldier;
    soldier.play();
    soldier.work();
    system("pause");
    return 0;
}

运行结果:

img

从运行结果来看,当我们调用play()这个函数的时候,打印出来的是Soldier类的play()函数,当我们调用work()这个函数的时候,打印出来的是Soldier类的work()函数。如果我们想要去打印出Person这个类的play()函数怎么办呢?则就需要用这样的调用方式(soldier.Person::play()),我们将这个调用添加到main函数中,如下:

int main()
{
    Soldier soldier;
    soldier.play();
    soldier.work();
    soldier.Person::play();
    system("pause");
    return 0;
}

运行结果:

img

从运行结果来看,除了打印出我们之前的信息之外,还打印出了Person这个类中的play()函数,并且将Mery这个名字(Person的构造函数的原因)也一并打印出来了。此外,还要向大家说明另外一个问题,什么问题呢?我们来想一想,当前的play函数不仅同名,而且它的参数也是相同的(都是无參的),那如果当他的参数不同的时候,是不是也能形成隐藏的效果呢?我们来试一试。

首先,我们修改一下Soldier这个类下的play函数,我们给这个play函数传入一个参数(void play(int x)),这样Soldier类中的play函数与Person类中的play函数就变成了同名不同參的函数了。这样,我们修改一下main函数,如下:

int main()
{
    Soldier soldier;
    soldier.play(7);
    soldier.work();
    soldier.play();
    system("pause");
    return 0;
}

点击运行,报错如下:

C++速成(2)_第14张图片

原因是main函数中标红语句少一个参数,换句话说,我们实例化的soldier要调用play函数,只能调用Soldier这个类下面的play,而不能调用继承来的play函数,不管继承来的play与Soldier这个类下面的原有的play是不是参数相同,也就是说,它们之间无法形成重载,只能进行隐藏。如果想要调用基类的paly,必须使用刚才上面的调用方式(soldier.Person::play())。

下面我们继续一个例子,在这个例子当中,我们在Soldier.h中多声明一个数据成员,使这个数据成员与Person.h下面的数据成员同名,从而这两个同名的数据成员也有了继承关系。我们如果在work这个函数中去访问的时候,如果我们直接给m_strName赋值的话,它只能给Soldier这个类下面的m_strName赋值,而不能给基类Person下面的m_strName赋值,我们来试一试。修改Soldier.cpp如下:

#include"Soldier.h"
#include
using namespace std;

Soldier::Soldier()
{
}
void Soldier::play(intx) 
{
    cout << m_strName << endl;
    cout <<"Soldier---play()"<< endl;
}
void Soldier::work()
{
    m_strName = "Jim";
    cout <<"work()"<< endl;
}

主调函数:

int main()
{
    Soldier soldier;
    soldier.work();
    soldier.play(7);
    soldier.Person::play();
    system("pause");
    return 0;
}

运行结果:

C++速成(2)_第15张图片

从运行结果来看,打印出来的数据成员是Jim,这就意味着,在Soldier.cpp的work函数中给m_strName赋值的Jim,那么这个m_strName就是Soldier这个类中的数据成员,而不是基类Person中的数据成员。所以,当子类和父类的数据成员同名的时候,子类的数据成员直接使用也只能使用它自身的,而无法使用继承下来的同名的数据成员。如果想要使用继承下来的数据成员,怎么办呢?我们只能这样:(Person::m_strName = “Keiven”),在打印的时候也必须使用这种方式(cout << Person::m_strName << endl;)。我们将这两个代码添加到Soldier.cpp中,如下:

#include"Soldier.h"
#include
using namespace std;

Soldier::Soldier()
{
}
void Soldier::play(intx) 
{
    cout << m_strName << endl;
    cout <cout <<"Soldier---play()"<< endl;
}
void Soldier::work()
{
    m_strName = "Jim";
    Person::m_strName = "Keiven";
    cout <<"work()"<< endl;
}

然后运行,运行结果如下:

C++速成(2)_第16张图片

我们看到,当实例化的soldier调用了work函数后,那么,在work中已经给子类和父类的数据成员m_strName分别赋了值,于是又调用了Soldier中的play函数,就打印出了子类和父类的数据成员m_strName的值(Jim和Keiven)。


5.6 is-a

什么是is-a?

我们看下面两个例子:

隐形眼镜也是眼镜。如果眼镜是基类,那么隐形眼镜就可以是眼镜的派生类。于是,对于任何一个隐形眼镜的对象来说,我们都可以称之为眼镜,这是没有错误的。在C++中,我们就把这种关系称之为is-a。

再比如,我们定义一个人的类,再定义一个工人的类,再定义一个士兵的类,如果将人这个类作为基类,工人和士兵分别继承人这个类,那么我们就可以把每一个工人的对象称之为一个人的对象,也可以把每一个士兵的对象称之为一个人的对象。我们来看一个这样的例子。

C++速成(2)_第17张图片

例子分析:

在这个例子中,我们实例化了Soldier类的一个对象s1,然后,在实例化Person对象的时候,让Person的对象p1直接接收s1,也就是说,用s1去实例化p1。(这样做在语法上是正确的,因为一个士兵也是一个人,那么用士兵初始化人是可以的)。接着又定义了一个Person的指针p2,并且让p2指向Soldier对象s1。看到这里,前面三行代码是正确的。接着看下面的,将人的对象赋值给士兵(s1 = p1),同时,如果用一个士兵的指针去指向一个人的对象(Soldier *s2 = &p1),这两种写法就是有问题的。

那么在上面三行代码是正确的前提下,这就意味着,派生类的对象可以赋值给基类(也可以说,子类的对象可以赋值给父类),也可以说,用基类的指针指向派生类的对象。既然如此,我们就可以将基类的指针或者是基类的对象或者是基类的引用作为函数的参数来接收传入的子类的对象,当然也可以接收传入的基类的对象。看下面的一个例子。

C++速成(2)_第18张图片

当我们定义了fun1和fun2这两个函数的时候,我们注意到,这里用的是Person的指针和Person的引用来作为函数的参数的。那么在main函数中使用的时候,我们实例化了一个Person类的对象p1,还实例化了一个Soldier类的对象s1,从而我们就可以将这两个对象的地址传入进去,这个时候所使用的是fun1(因为fun1的参数是一个Person的指针,所以它既可以指向Person的对象,也可以指向Soldier的对象,因为Soldier继承了Person),同样,如果使用第二个函数fun2的话呢,也可以传入p1和s1,因为fun2所使用的参数是Person的引用,所以这里不需要加&符号,直接传对象本身就可以了。

相信,讲到这的时候,大家对Is-a的语法,使用方式上应该没有什么疑惑了,下面从内存的角度来说明Is-a的关系。


存储结构

首先,第一种情况,将子类的对象赋值给父类的对象,或者是用子类的对象初始化父类的变量。

C++速成(2)_第19张图片

如果父类中含有m_strName和m_iAge这两个数据成员的时候,那么子类在继承父类的时候一定也含有m_strName和m_iAge这两个数据成员,同时,子类应该还含有其自身的数据成员。当我们用子类的对象向父类的对象赋值或者是用子类的对象初始化父类的一个对象的时候,它的本质就是将子类当中从父类继承下来的数据成员赋值给父类的对象,那么子类中其他的数据成员此时就会被截断,因为,对于父类来说,它只能接收自己拥有的数据成员的数据,而无法接收其他数据。如果是用父类的指针指向一个子类对象,那么,父类的指针也只能够访问到父类所拥有的数据成员。而无法访问到子类所独有的数据成员。也就是说,如果我们用一个父类指针去指向一个子类对象的话,我们只能够通过这个父类指针去访问父类原有的数据成员和成员函数,而无法访问子类所独有的数据成员和成员函数。

C++速成(2)_第20张图片


is-a编码实践

题目描述:

/*******************************/

/* 继承关系中的隐藏

要求:

​ 1. Person类,数据成员:m_strName,成员函数:构造函数、play()

​ 2. Soldier类,数据成员:m_iAge,成员函数:构造函数、析构函数、work()

​ 3. 定义函数test1(Person p) test2(Person &p) test3(Person *p)

/*******************************/

程序框架:

C++速成(2)_第21张图片

头文件(*Person.h*

#include
using namespace std;

class Person
{
public:
    Person(string name = "Jim");//这里给定默认参数值
    ~Person();
    void play();
protected:
    string m_strName;
};

源程序(*Person.cpp*

#include"Person.h"
#include
using namespace std;

Person::Person(string name)
{
    m_strName = name;
    cout <<"Person()"<< endl;
}
Person::~Person()
{
    cout <<"~Person()"<< endl;
}
void Person::play()
{
    cout <<"Person---play()"<< endl;
    cout << m_strName << endl;
}

头文件(*Soldier.h*

#include"Person.h"//这里如果不包含这个头文件,编译时就会出现“Person”未定义基类

class Soldier:public Person
{
public:
    Soldier(string name = "James", int age = 20);//这里也给定默认参数值
    ~Soldier();
    void work();
protected:
    int m_iAge;
};

源程序(*Soldier.cpp*

#include"Soldier.h"
#include
using namespace std;

Soldier::Soldier(string name, intage)
{
    m_strName = name;
    m_iAge = age;
    cout <<"Soldier()"<< endl;
}
Soldier::~Soldier()
{
    cout <<"~Soldier()"<< endl;
}
void Soldier::work()
{
    cout << m_strName << endl;
    cout << m_iAge << endl;
    cout <<"Soldier--work()"<< endl;
}

主调程序(*demo.cpp*

#include
#include
#include"Soldier.h"

using namespace std;

int main()
{
    Soldier soldier;
    Person p = soldier;
    p.play();

    system("pause");
    return 0;
}

在这里我们实例化了一个Soldier对象soldier,并且用soldier去初始化Person的对象p,大家想一想,这里p中m_strName的值究竟是什么呢?那我们就来调用一下play函数来观察看一看,运行结果如下:

C++速成(2)_第22张图片

从运行结果来看,前面两行输出是因为,第一行代码我们实例化了Soldier对象,对象实例化时会自动调用其默认构造函数,这里由于Soldier是子类,所以先调用其父类的默认构造函数,再调用其自身的默认构造函数。后面两行输出是因为调用了play函数,打印出了m_strName是James,这就意味着,我们使用soldier去初始化Person的对象p,就使得p中的m_strName的值是James。这就意味着,这里做了截断(前面介绍过,即把soldier中的m_strName赋值给了p中的m_strName)。接下来,我们直接来实例化对象p,如下

int main()
{
    Soldier soldier;
    Person p;
    p.play();

    system("pause");
    return 0;
}

因为p有默认构造函数,并且有默认值“Jim”,当调用play函数时,一定打印出的是Jim,我们来看一下运行结果:

C++速成(2)_第23张图片

前面我使用的是soldier来实例化p,接下来我们让soldier直接赋值给p这个对象,如下:

int main()
{
    Soldier soldier;
    Person p;
    p = soldier;
    p.play();

    system("pause");
    return 0;
}

运行结果:

C++速成(2)_第24张图片

我们可以看到,当Soldier对象直接赋值给Person对象后,再用p去调用play函数,打印出的就是James,这就意味着,无论是用soldier去初始化p,还是将soldier直接赋值给p,那么soldier中的m_strName都可以赋值给其父类中队形对应的那个数据成员。接下来,我们使用指针来试一试效果如何(如下)。

int main()
{
    Soldier soldier;
    Person *p = &soldier;
    p->play();

    system("pause");
    return 0;
}

在这里我们定义了一个Person类的指针对象p,并且让p指向Soldier类的对象soldier,然后我们用p来调用play函数,看一看运行结果:

C++速成(2)_第25张图片

我们可以看到,效果是一样的,也打印出的是James。可见,无论是用对象赋值的方式,还是用指针指向的方式,如果用父类去指向或者接收子类对象的值,打印出的都是子类对象所拥有的那个值。

那么,使用父类的指针能不能调用子类的成员函数呢?我们来试一下(如下)

int main()
{
    Soldier soldier;
    Person *p = &soldier;
    p->play();
    p->work();

    system("pause");
    return 0;
}

运行如下:

C++速成(2)_第26张图片

我们看到程序报错,错误的提示是work函数不是Person类的成员,可见,我们使用Person类的对象或者指针,只能调用Person自己的数据成员和成员函数,无法调用其子类的成员和成员函数。

下面,我们来做另外一个实验,如果我们通过父类的指针指向子类的对象,那么指针销毁的时候,究竟执行的是父类的析构函数还是子类的析构函数呢?我们一起看一下(如下)

int main()
{

    Person *p = new Soldier;

    p->play();
    delete p;
    p = NULL;

    system("pause");
    return 0;
}

运行结果:

C++速成(2)_第27张图片

从运行结果来看,当我们用父类的指针去指向子类的一个对象的时候,那么子类的这个对象会去实例化,所以实例化的过程是先调用父类的构造函数,再调用子类的构造函数,但是当我们去销毁指针的时候,我们会发现,只执行了父类的析构函数,这意味着,子类的析构函数没有被执行,那就有可能造成内存的泄漏。

那这种情况下,我们如何来避免内存泄漏呢?我们是有办法能够解决这个问题的。

首先,我们需要学习一个新的知识点—-虚析构函数。

什么时候需要用到虚析构函数呢?当存在继承关系的时候,我们使用父类的指针去指向堆中的子类的对象,并且我们还想使用父类的指针去释放这块内存,这个时候就需要使用虚析构函数。写法很简单,只需要在析构函数前面加上关键字virtual即可。如果父类的析构函数是虚析构函数,那么其子类的析构函数无论前面加不加关键字virtual,其都是虚析构函数(这里,我们建议都加上,方便理解)。那么这里我们来修改一下析构函数使其变成虚析构函数,即分别在Person.h和Soldier.h中在析构函数的前面加上关键字virtual即可,如下:

virtual ~Person();
virtual ~Soldier();

main函数还是之前的main函数,如下

int main()
{

    Person *p = new Soldier;

    p->play();
delete p;
    p = NULL;

    system("pause");
    return 0;
}

现在再来看一看执行效果如何:

C++速成(2)_第28张图片

从运行结果,我们可以看到,其最后两行分别执行的是Soldier类的析构函数和Person类的析构函数,也就是说,它可以将soldier这个对象完全的释放掉。

接下来,我们来实现题目描述中的第3个要求。

下面我们在demo.cpp中来定义这3个函数(我们不关心其返回值),如下:

void test1(Personp)
{
    p.play();
}

void test2(Person&p)
{
    p.play();
}
void test3(Person *p)
{
    p->play();
}

然后在main函数中,我们分别实例化两个对象p和s。我们分别使用Person和Soldier的对象作为参数,分别传递给函数test1、test2和test3。注意观察三者的区别。

首先将p和s传递给test1,如下:

int main()
{    
    Person p;
    Soldier s; 

    test1(p);
    test1(s);

    system("pause");
    return 0;
}

我们来看一下运行结果:

C++速成(2)_第29张图片

从运行结果可以看到,由于实例化Person对象,所以打印出了Person的构造函数;接着由于实例化Soldier对象,这里由于Soldier是子类,所以先调用其父类的默认构造函数,再调用其自身的默认构造函数。然后,就需要注意的是:在我们调用test1函数的时候,因为我们在test1中所定义的参数是一个对象p,所以在传值的时候,先实例化一个临时对象p,这就使得它在接收参数的时候,会临时地实例化一个对象p,通过这个临时对象p来调用play函数,并且在函数test1执行完毕之后,p这个临时对象就会被销毁。这就是为什么在我们的执行结果中会打印出两次Person的析构函数来的原因。在销毁之前,它确实调用过play函数。我们看到,当我们传入的是Person的对象时,打印出的是Jim,而当我们传入的是Soldier对象时,打印出的是James。可见,如果函数的参数是基类的对象,那么基类的对象和派生类的对象都可以作为实参传递进来,并且能够正常的使用。

下面来看一下test2的执行情况,如下:

int main()
{    
    Person p;
    Soldier s; 

    test2(p);
    test2(s);

    system("pause");
    return 0;
}

我们来看一下运行结果:

C++速成(2)_第30张图片

从运行结果可以看到,前三行的打印结果跟之前一样(先实例化Person对象,再实例化子类Soldier对象),然后调用test2。大家注意,test2因为是一个引用,所以在传入参数的时候,会将这个参数起一个别名p,通过这个别名p来调用play函数,可见,这个过程中,并没有实例化临时对象,所以也没有销毁临时对象的痕迹。其他的打印结果与调用test1的打印结果是相同的。这也可以说明,使用基类的引用,也可以接收基类的对象以及派生类的对象。

下面来看一下test3的执行情况,如下:

int main()
{    
    Person p;
    Soldier s; 

//因为test3要求传入的是一个指针,所以这里需要传入的是p和s的地址
    test3(&p); 
    test3(&s);

    system("pause");
    return 0;
}

我们来看一下运行结果:

C++速成(2)_第31张图片

从运行结果可以看到,与调用test2的结果是一样的。这是因为test3的参数是一个指针,而这个指针是一个基类的指针,当我们使用基类或者是派生类的对象的地址传入之后,会使用指针p分别调用基类和派生类的play函数,所以打印出Jim和James。

通过对于test1、test2和test3三个函数的对比调用,我们可以发现,使用test2和test3并不会产生新的临时变量,所以效率更高。


5.7 多继承与多重继承

多重继承

什么是多重继承呢?如果有这样三个类:人类、士兵类、步兵类。其中,士兵类继承了人类,步兵类继承了士兵类,我们就称这三个类之间存在多重继承的关系(如下所示)。

C++速成(2)_第32张图片

如果这三个类在继承的时候,都使用的是public方式,那么它们也存在着以下关系:士兵是一个人,步兵是一个士兵,步兵也是一个人(如下)

C++速成(2)_第33张图片

那么,具体到代码上,我们可以这样来写:

C++速成(2)_第34张图片


多继承

知道了多重继承,那么多继承又是什么呢?

所谓多继承,我们先来看一个例子,先定义一个工人类,然后定义一个农民类。如果还有一个类,它不仅继承了工人类,而且还继承了农民类,在这里我们暂且叫这个类为农民工类。那么在这种情况下,我们发现,一个子类同时有两个父类(或者说一个派生类同时有两个基类),那么这样的关系,我们称之为多继承。它与多重继承是完全不同的。

C++速成(2)_第35张图片

在多继承的情况下,如果农民工在继承工人和农民的时候,都是以public方式继承的话,那么它们还存在着这样一种关系:农民工是一个工人,农民工是一个农民,但是,工人和农民这两个类本身是平行的,如下:

C++速成(2)_第36张图片

那么,具体到代码上,我们可以这样来写:

C++速成(2)_第37张图片


多重继承编码实践

题目描述:

/* ****************************************************************** */

/*

多重继承

要求:1.Person类,数据成员: m_strName,成员函数:构造函数,析构函数,play()

​ 2.Soldier类,数据成员:m_iAge,成员函数:构造函数,析构函数,work()

​ 3.Infantry类,数据成员:无,成员函数:构造函数,析构函数,attack()

​ 4.定义函数test1(Person p) test2(Person &p) test3(Person *p)

*/

/* ***************************************************************** */

程序框架:

C++速成(2)_第38张图片

头文件(*Person.h*

#include
using namespace std;

class Person
{
public:
    Person(string name = "Jim");
    virtual ~Person();
    void play();
protected:
    string m_strName;
};

源程序(*Person.cpp*

#include"Person.h"
#include
using namespace std;

Person::Person(string name)
{
    m_strName = name;
    cout <<"Person()"<< endl;
}
Person::~Person()
{
    cout <<"~Person()"<< endl;
}
void Person::play()
{
    cout <<"Person---play()"<< endl;
    cout << m_strName << endl;
}

头文件(*Soldier.h*

#include"Person.h"//这里如果不包含这个头文件,编译时就会出现“Person”未定义基类

class Soldier:public Person
{
public:
    Soldier(string name = "James", int age = 20); //这里也给定默认参数值
    ~Soldier();
    void work();
protected:
    int m_iAge;
};

源程序(*Soldier.cpp*

#include"Soldier.h"
#include
using namespace std;

Soldier::Soldier(string name, int age)
{
    m_strName = name;
    m_iAge = age;
    cout <<"Soldier()"<< endl;
}
Soldier::~Soldier()
{
    cout <<"~Soldier()"<< endl;
}
void Soldier::work()
{
    cout << m_strName << endl;
    cout << m_iAge << endl;
    cout <<"Soldier--work()"<< endl;
}

头文件(*Infantry.h*

//步兵类
#include"Soldier.h"

class Infantry:public Soldier
{
public:
    Infantry(string name = "Jack", int age = 30);
    ~Infantry();
    void attack();
};

源程序(*Infantry.cpp*

#include
#include"Infantry.h"
using namespace std;

Infantry::Infantry(string name, int age)
{
    m_strName = name;
    m_iAge = age;
    cout <<"Infantry()"<< endl;
}
Infantry::~Infantry()
{
    cout <<"~Infantry()"<< endl;
}
void Infantry::attack()
{
    cout << m_strName << endl;
    cout << m_iAge << endl;
    cout <<"Infantry--attack()"<< endl;
}
#include
#include
#include"Infantry.h"
using namespace std;

void test1(Personp)
{
    p.play();
}

void test2(Person &p)
{
    p.play();
}
void test3(Person *p)
{
    p->play();
}

int main()
{
    Infantry infantry;

    system("pause");
    return 0;
}

在main函数中,此时我们只实例化一个步兵对象,看一看程序结果:

C++速成(2)_第39张图片

程序打印出三行,分别是Person类的构造函数,Soldier类的构造函数,以及Infantry类的构造函数,这就意味着,我们在实例化步兵这个对象时,先执行了Person构造函数,又执行了Soldier构造函数,最后执行了Infantry本身的构造函数。为什么会是这样呢?其实作为最底层的子类,它如果想要实例化一个自己的对象,那它就必须执行其继承链上的每一个类,而在Infantry的继承链中有Person类还有Soldier类,所以要先执行Person的构造函数,再执行Soldier的构造函数,最后才执行自己本身的构造函数。当然,在销毁的时候,其析构函数会按照构造函数的逆序执行,也就是说,先执行Infantry的析构函数,再执行Soldier的析构函数,最后执行Person的析构函数。

接下来我们通过对test1、test2和test3这三个函数的调用来说明一个子类的对象可以作为函数参数传入这三个函数当中。大家注意,这三个函数要求的参数是Person的对象、引用、指针。那么,作为步兵这样的一个子类,传入进去之后呢,也应该是没有问题的,我们来一起实践一下。

在main函数中,分别调用test1、test2和test3函数,如下:

int main()
{
    Infantry infantry;
    test1(infantry);
    test2(infantry);
    test3(&infantry);

    system("pause");
    return 0;
}

我们来看一下运行结果:

C++速成(2)_第40张图片

从运行结果可以看到,前面三行是实例化步兵对象的结果。然后,由于test1、test2和test3这三个函数都在其中调用了play函数,所以,我们可以看到有三个”Person—play()”被打印出来,打印出来的同时,还将Jack也打印出来了。而Jack就来自于Infantry构造函数中的默认参数。通过这个打印结果,我们可以得到这样的结论:无论继承关系有多少层,它们只要保持着直接或者间接的继承关系,那么子类都可以与自己的直接父类或者间接父类称之为is-a的关系,并且能够通过父类的指针对直接子类或间接子类的对象进行相应的操作。


多继承代码实践

题目描述:

/* ****************************************************************** */

/*

多继承

要求:1.Farmer类,数据成员: m_strName【农民姓名】,成员函数:构造函数,析构函数,sow()【播种函数】

​ 2.Worker类,数据成员:m_strCode【工人工号】,成员函数:构造函数,析构函数,carry()【搬运函数】

​ 3.MIgrantWorker类,数据成员:无,成员函数:构造函数,析构函数

*/

/* ***************************************************************** */

程序框架:

C++速成(2)_第41张图片

头文件(*Worker.h*

#include
using namespace std;

class Worker
{
public:
    Worker(string code = "001");
    virtual ~Worker(); //虚析构函数
    void carry();
protected:
    string m_strCode;
};

源程序(*Worker.cpp*

#include"Worker.h"
#include
using namespace std;


Worker::Worker(string code)
{
    m_strCode = code;
    cout <<"Worker()"<< endl;
}
Worker::~Worker()
{
    cout <<"~Worker()"<< endl;
}
void Worker::carry()
{
    cout << m_strCode << endl;
    cout <<"Worker---carry()"<< endl;
}

头文件(*Farmer.h*

#include
using namespace std;

class Farmer
{
public:
    Farmer(string name = "Jack");
    virtual ~Farmer();
    void sow();
protected:
    string m_strName;
};

源程序(*Farmer.cpp*

#include"Farmer.h"
#include
using namespace std;

Farmer::Farmer(string name)
{
    m_strName = name;
    cout <<"Farmer()"<< endl;
}

Farmer::~Farmer()
{
    cout <<"~Farmer()"<< endl;
}
void Farmer::sow()
{
    cout << m_strName << endl;
    cout <<"Farmer---sow()"<< endl;
}

头文件(*MigrantWorker.h*

#include"Worker.h"
#include"Farmer.h"

class MigrantWorker:public Farmer, public Worker//农民工类继承农民类和工人类
{
public:
    MigrantWorker(string name,string code);
    ~MigrantWorker();
};

源程序(*MigrantWorker.cpp*

#include"MigrantWorker.h"
#include
using namespace std;

//采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode
MigrantWorker::MigrantWorker(string name, string code):Farmer(name),Worker(code)
{
    cout <<"MigrantWorker()"<< endl;
}
MigrantWorker::~MigrantWorker()
{
    cout <<"~MigrantWorker()"<< endl;
}

主调程序(*demo.cpp*

#include
#include
#include"MigrantWorker.h"
using namespace std;

int main()
{
    //采用堆的方式实例化农民工对象
    MigrantWorker *p = new MigrantWorker("Merry", "100"); //姓名将来传给农民类,工号传给工人类
    p->carry();
    p->sow();
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

我们来看一下运行结果:

C++速成(2)_第42张图片

从运行结果来看:前面有三个构造函数的执行,分别是农民的构造函数、工人的构造函数以及农民工的构造函数。这就是说,在实例化一个子类的对象时,它会先调用父类的构造函数,如果它有多个父类,那么它会依次调用每一个父类的构造函数,其调用顺序与初始化列表的顺序是一样的。再来看其他的打印结果,先是打印出工号100,接着打印出Worker中的carry函数(这是由于对象p调用了carry这个函数),然后打印出姓名Merry,接着打印出Farmer中的sow函数(这是由于对象p调用了sow这个函数)。最后三行打印出了析构函数,其打印顺序与构造函数的打印顺序正好相反。


5.8 虚拟继承

多继承+多重继承的烦扰

先来看一个例子,在这个例子中,类A是父类,类B和类C继承了类A,而类D既继承类B又继承类C(如下图所示),我们称这种继承关系又称之为菱形继承。在菱形继承中我们发现,既有多继承,也有多重继承

C++速成(2)_第43张图片

那么,现在问题来了……

当我们要实例化D的对象的时候呢,我们发现,D是从B继承来的,B又是从A继承来的;而D也是从C继承来的,C又是从A继承来的,这样一来,D中就含有两个一样的A的数据,而这种情况是我们不能容忍的。因为在一个对象当中有两份完全相同的数据,这种冗余数据我们没有办法来承担它的系统开销,所以我们必须要解决。也许,你会说在实际的工作中会遇到这么复杂的情况吗?我们来看一看,不用实际工作,我们只需要回顾一下前面举过的例子:之前我们讲过人这个类,也讲过工人,农民和农民工这三个类,那么,工人和农民可以继承人这个类,而农民工则既可以继承农民,也可以继承工人这两个类(如下)

C++速成(2)_第44张图片

那么,这种典型的菱形继承关系用什么办法刚才我们所说的数据冗余的问题呢?那就要用到接下来要讲到的虚继承相关知识。


虚继承

虚继承是继承的一种方式,其关键字是virtual,我们来看一看如何使用,请看下面这个例子。

在这个例子中,我们定义了一个工人的类,工人的类要去继承人这个类,而工人这个类将来也会被农民工这个类所继承,我们就把工人这个类就称之为虚基类。那么,在工人这个类去继承人这个类的时候,就需要加上关键字virtual。对于农民这个类写法上与工人这个类一样。(如下所示)。

C++速成(2)_第45张图片

在使用的时候,我们的农民工这个类,就可以继承工人和农民这两个类了。于是使用农民工这个类去实例化一个农民工的对象,那么,它当中就只含有一份Person的数据。

C++速成(2)_第46张图片


虚继承编码实践

题目描述:

/* ****************************************************************** */

/*

虚继承:

说明:通过这个例子学习虚继承的使用方法,及虚继承存在的必要性

要求:

1.Farmer类,数据成员: m_strName【农民姓名】,成员函数:构造函数,析构函数,sow()【播种函数】

​ 2.Worker类,数据成员:m_strCode【工人工号】,成员函数:构造函数,析构函数,carry()【搬运函数】

​ 3.MigrantWorker类,数据成员:无,成员函数:构造函数,析构函数

​ 4.Person类,数据成员:m_strColor【人的肤色】,成员函数:构造函数,析构函数,printColor()

*/

/* ***************************************************************** */

程序框架:

C++速成(2)_第47张图片

头文件(*Person.h*

#include
usingnamespace std;

classPerson
{
public:
    Person(string color = "blue");
    virtual ~Person();
    void printColor();
protected:
    string m_strColor;
};

源程序(*Person.cpp*

#include
#include"Person.h"
usingnamespace std;

Person::Person(stringcolor)
{
    m_strColor = color;
    cout <<"Person()"<< endl;
}

Person::~Person()
{
    cout <<"~Person()"<< endl;
}

voidPerson::printColor()
{
    cout << m_strColor << endl;
    cout <<"Person---printColor()"<< endl;
}

头文件(*Worker.h*

#include
#include"Person.h"
usingnamespace std;

classWorker:publicPerson
{
public:
    Worker(string code = "001", string coloe = "blue");//我们希望Worker可以传入参数“肤色”给Person类
    virtual ~Worker(); //虚析构函数
    void carry();
protected:
    string m_strCode;
};

源程序(Worker.cpp)

#include"Worker.h"
#include
usingnamespace std;


Worker::Worker(stringcode, stringcolor):Person(color)
{
    m_strCode = code;
    cout <<"Worker()"<< endl;
}
Worker::~Worker()
{
    cout <<"~Worker()"<< endl;
}
voidWorker::carry()
{
    cout << m_strCode << endl;
    cout <<"Worker---carry()"<< endl;
}

头文件(*Farmer.h*

#include
#include"Person.h"
usingnamespace std;

classFarmer:publicPerson
{
public:
    Farmer(string name = "Jack", string color = "blue");
    virtual ~Farmer();
    void sow();
protected:
    string m_strName;
};

源程序(Farmer.cpp)

#include"Farmer.h"
#include
usingnamespace std;

Farmer::Farmer(stringname, stringcolor):Person(color)
{
    m_strName = name;
    cout <<"Farmer()"<< endl;
}

Farmer::~Farmer()
{
    cout <<"~Farmer()"<< endl;
}
voidFarmer::sow()
{
    cout << m_strName << endl;
    cout <<"Farmer---sow()"<< endl;
}

头文件(*MigrantWorker.h*

#include"Worker.h"
#include"Farmer.h"

classMigrantWorker:publicFarmer, publicWorker//农民工类继承农民类和工人类
{
public:
    MigrantWorker(string name,string code, string color);
    ~MigrantWorker();
};

源程序(*MigrantWorker.cpp*

#include"MigrantWorker.h"
#include
usingnamespace std;

//采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode
MigrantWorker::MigrantWorker(stringname, stringcode, stringcolor):Farmer(name, color),Worker(code, color)
{
    cout <<"MigrantWorker()"<< endl;
}
MigrantWorker::~MigrantWorker()
{
    cout <<"~MigrantWorker()"<< endl;
}

主调程序(*demo.cpp*

#include
#include
#include"MigrantWorker.h"
usingnamespace std;

int main()
{

    system("pause");
    return 0;
}

如果我们现在在主调函数中什么都不写,直接按F5运行程序,我们会看见如下错误:

1>d:\vs_program\farmer_worker_migrantworker\farmer_worker_migrantworker\person.h(5): error C2011: “Person”:“class”类型重定义

为什么会这样呢?

我们看到,在Farmer.h和Worker.h中,我们都引用了”Person.h”,这样就对Person这个类进行了重定义,所以会报这个错误。那么,怎么解决这个错误呢?

下面介绍一种方法—-宏定义,来解决重定义的问题。

首先,我们在公共的被继承的这个类的.h文件当中,加上如下几行代码:

#ifndef PERSON_H  //如果没有定义 PERSON_H
#definePERSON_H//那么就定义 PESON_H
#endif//结束定义

加上这三行后,Person.h文件如下:

#ifndef PERSON_H  //如果没有定义 PERSON_H
#definePERSON_H//那么就定义 PESON_H

#include
usingnamespace std;

classPerson
{
public:
    Person(string color = "blue");
    virtual ~Person();
    void printColor();
protected:
    string m_strColor;
};

#endif//结束定义

这个时候,我们再按F5,运行程序,就不会再报错了

img

注意:用宏定义解决重定义的技巧,需要大家掌握,因为在工作中,大家肯定会遇到相同的问题。

下面,我们跳到demo.cpp文件中,在其中,我们先实例化一个农民工的对象(从堆中实例化),传入的参数有三个(姓名=”Merry”,工号=”200”,肤色=”Yellow”),然后直接删除这个对象,我们想要看的是:菱形继承过程当中,构造函数和析构函数的执行顺序。

int main()
{
    MigrantWorker *p = newMigrantWorker("Merry", "200", "Yellow");
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

我们来看一下运行结果:

C++速成(2)_第48张图片

我们来分析一下这个执行结果:

因为作为MigrantWorker这个类来说,它在实例化一个农民工对象的时候,必然要先实例化它的两个父类(Farmer类和Worker类),而在实例化Farmer类和Worker类的时候呢,由于这两个类的父类都是Person类,所以在实例化Farmer类之前必定先实例化Person类,同理,在实例化Worker类的时候也必定先实例化Person类,所以Person这个类就实例化了两次。在销毁的时候,析构函数的执行顺序与构造函数的执行顺序相反。

做完这样的实验之后,我们来想一想,其实在农民工这个类当中,已经存在了两份Person对象中的数据。那么,我们也可以通过调用的方式来向大家证明。为了证明这一点,我们需要对现在的程序做一些简单改造。

首先打开Worker.cpp文件,我们先来看一看Worker的构造函数是如何实现的.zai Worker的构造函数中,如果传入参数color,那么color就会直接传递给Person。我们希望在这个过程中,能够打上Worker的印记,所以我们这样来改造一下Worker的构造函数:将Worker作为字符串的一部分也传递给Person(字符串之间是可以进行拼接的)

Worker::Worker(stringcode, stringcolor):Person("Worker" + color)
{
    m_strCode = code;
    cout <<"Worker()"<< endl;
}

使用同样的方法来改造一下Farmer的构造函数,如下:

Farmer::Farmer(stringname, stringcolor):Person("Farmer" + color)
{
    m_strName = name;
    cout <<"Farmer()"<< endl;
}

最后,我们返回demo.cpp文件中,配合前面我们写的代码,要想证明Person在农民工这个对象中存在两份相同的数据成员,那么我们就要通过农民工这个对象指针来分别打印一下这两份数据成员的值。这两份数据成员呢,一份来自于Worker,一份来自于Farmer。

int main()
{
    MigrantWorker *p = newMigrantWorker("Merry", "200", "Yellow");
    p->Farmer::printColor();
    p->Worker::printColor();
    delete p;
    p = NULL;
    system("pause");
    return 0;
}

看看运行效果如何:

C++速成(2)_第49张图片

从运行结果,我们看到了“FarmerYellow”和“WorkerYellow”,可见,在农民工这个对象当中,实际上是存在着两个m_strColor这样的数据成员的。这种情况是我们不能容忍的,我们希望有一份就可以了。那么,怎么样才能保证使它只有一份数据成员呢?这个时候,就需要用到我们之前讲到的虚继承相关知识。写法上,将Worker类和Farmer类分别采用虚继承的方式继承Person类

classWorker:virtualpublicPerson
{
……
}
classFarmer:virtualpublicPerson
{
……
}

然后,我们返回到demo.cpp中,重新运行一下程序如下:

C++速成(2)_第50张图片

我们首先来观察一下它的构造函数和析构函数的运行情况:当我们加了virtual这个关键字以后,我们发现:Person这个构造函数和析构函数就执行了一次,这就说明,当我们采用了virtual关键字后就形成了虚继承,虚继承的好处就使得农民工这个类所实例化的对象当中只有一份Person的数据。

我们再来看一看中间部分所打印出来的数据。我们可以看到,原来是“FarmerYellow”和“WorkerYellow”的地方,现在变成了只是“blue”,这就说明,在虚继承的情况下,作为菱形继承最顶层的父类并没有进行参数的传递,也就是说,参数只使用了顶层父类的默认参数,而无法从子类当中获得传入的参数。

5.6 练习题


6. C++多态3h

6.1 多态概述

讲到多态,那什么是多态呢?

所谓多态,简单来说就是,当发出一条命令的时候,不同的对象接收到同样的命令之后,所做出的动作是不同的,那么我们就把这种情况称之为多态。

C++速成(2)_第51张图片

上一小节,我们笼统了解释了什么是多态,现在,我们来看一看书本上是如何来定义多态的,如下:指相同对象收到不同消息或不同对象收到相同消息时产生不同的动作

为了能够把这个概念说清楚,我们简而言之,其实就是在说两个概念:静态多态和动态多态。

静态多态(早绑定)

所谓静态多态,也称之为早绑定。在我们之前的课程中其实就已经涉及到并且已经应用了,只不过当时怕用这个概念把大家搞晕,就没有给大家提这个概念。那么,这节课就给大家讲一讲什么是静态多态,为什么把静态多态也叫做早绑定。接下来看下面这个例子。

C++速成(2)_第52张图片

在这个例子中,我们定义了一个矩形类(Rect),并且在这个类中,还定义了两个函数,然而这两个成员函数的名字是相同的,都是叫做calcArea(计算矩形面积),但是这两个函数的参数个数不同,那么对于这两个函数,我们之前称之为互为重载的函数。那么,作为互为重载的两个函数来说,当我们去实例化一个矩形的对象rect之后,我们就可以通过这个对象来分别调用这两个函数。

C++速成(2)_第53张图片

在调用时,对于第一个函数,我们传入一个参数,对于第二个函数,我们传入两个参数,那么,计算机在编译的时候就会自动的调用相应的函数,那么,大家会发现,程序在运行之前的编译阶段就已经确定下来,到底要使用哪个函数了。可见,很早的就已经将函数编译进去了,那么,我们就把这种情况叫做早绑定,也叫做静态多态。


动态多态(晚绑定)

那么什么是动态多态呢?我们还是从一个例子开始讲起。比如,当前我们下达一道指令—-计算面积。于是,就给圆形这个类下达了这道指令,让它来计算面积,同时,我们又向矩形这个类也下达了这道指令,也让它来计算面积。那么,对于圆形和矩形来说,它们分别有自己的计算面积的方法,可见,这两种方法肯定是不同的。

C++速成(2)_第54张图片

这种情况就是,对于不同的对象下达相同的指令,但是却做着不同的操作。而动态多态是有着前提的,它必须以封装和继承为基础。那么,动态多态起码是有两个类:一个是子类,一个是父类。当然,也可以有三个类,只有使用三个类的时候,动态多态才表现得更加明显。我们还是从代码的角度给大家讲解动态多态的例子。

我们先来看一看,适用我们之前所学习的一些代码是否能够实现动态多态呢?比如,我们在这里定义了一个形状类(Shape),在这个类当中,定义了一个成员函数叫做计算面积(calcArea)。

C++速成(2)_第55张图片

然后,我们再定义两个类:一个Circle类(圆),一个Rect类(矩形)。这两个类都以public方式继承Shape类,如下:

C++速成(2)_第56张图片

接下来我们所要讲的重点就是:在main函数中去使用的时候,我们可以使用父类的指针shape1去指向其中的一个子类对象Circle,并且用另外一个父类的指针shape2去指向一个矩形的对象。从而,这两个子类对象都被它的父类指针所指向,而我们进行操作的时候呢,是通过shape1和shape2分别调用计算面积的函数calcArea(),如下所示:

C++速成(2)_第57张图片

大家想一想,如果你去尝试一下的话,你肯定知道结果,而结果呢,其实并不是我们所想要的,因为要用到的都是父类的计算面积,也就是说,在屏幕上会打印出来年该行计算面积出来,即“calcArea()”的字样。那么,如果想要实现动态多态,看来以前我们所学习到的这些知识是没法实现的了。所以,接下来要学习一个新的知识:虚函数,即采用关键字*virtual*来修饰类的成员函数。实际当中,怎么来实现呢。我们还是以刚刚的例子来说明问题。我们在刚才的例子中,提到了计算面积(calcArea)这个函数,我们需要在父类(Shape)中去定义成员函数的时候,就把我们想要实现多态的成员函数前面加virtual关键字,使其成员虚函数,如下所示:

C++速成(2)_第58张图片

然后呢,我们在去定义它的子类的时候,给它计算面积的成员函数前面也要加上关键字virtual(此时这个关键字virtual不是必须要加上的,如果不加,系统会自动给你加上,如果加上了,我们会在后续的适用当中看得更加明显,所以,最好还是加上的好),使其成为虚函数,如下所示:

C++速成(2)_第59张图片

最后,就是关键的main函数中的使用了。

C++速成(2)_第60张图片

那么,在main函数当中,用父类指针再去指向子类对象的时候呢,如果用shape1调用calcArea()的时候呢,那么调用的就是Circle这个类自己的calcArea()函数,当然也会计算出实际的圆的面积出来。同理,用shape2调用calcArea()的时候呢,那么调用的就是Recr这个类自己的calcArea()函数,也会计算出矩形的面积来。


6.3 虚函数代码实践

题目描述:

/* **************************************** */

/* 动态多态、虚函数 */

/* 要求:

​ 1. 定义Shape类,成员函数:calcArea(),构造函数,析构函数

​ 2. 定义Rect类,成员函数:calcArea(),构造函数,析构函数

​ 数据成员:m_dWidth,m_dHeight

​ 3. 定义Circle类,成员函数:calcArea(),构造函数,析构函数

​ 数据成员:m_dR

/* **************************************** */

程序框架:

C++速成(2)_第61张图片

头文件(*Shape.h*

#ifndef SHAPE_H
#define SHAPE_H

#include 
using namespace std;

class Shape
{
public:
    Shape();
    ~Shape();
    double calcArea();
};

#endif

源程序(*Shape.cpp*

#include "Shape.h"
#include 
using namespace std;

Shape::Shape()
{
    cout << "Shape()" << endl;
}

Shape::~Shape()
{
    cout << "~Shape()" << endl;
}

double Shape::calcArea()
{
    cout << "Shape---calcArea()" << endl;
    return 0;
}

头文件(Circle.h)

#ifndef CIRCLE_H
#define CIRCLE_H

#include "Shape.h"

class Circle:public Shape
{
public:
    Circle(double r);
    ~Circle();
    double calcArea();
protected:
    double m_dR;
};

#endif

源程序(*Circle.cpp*

#include "Circle.h"
#include 
using namespace std;

Circle::Circle(double r)
{
    cout << "Circle()" << endl;
    m_dR = r;
}
Circle::~Circle()
{
    cout << "~Circle()" << endl;
}
double Circle::calcArea()
{
    cout << "Circle----calcArea()" << endl;
    return 3.14*m_dR*m_dR;
}
#include "Circle.h"
#include 
using namespace std;

Circle::Circle(double r)
{
    cout << "Circle()" << endl;
    m_dR = r;
}
Circle::~Circle()
{
    cout << "~Circle()" << endl;
}
double Circle::calcArea()
{
    cout << "Circle----calcArea()" << endl;
    return 3.14*m_dR*m_dR;
}

头文件(*Rect.h*

#ifndef RECT_H
#define RECT_H

#include "Shape.h"

class Rect:public Shape
{
public:
    Rect(double width, double height);
    ~Rect();
    double calcArea();
protected:
    double m_dWidth;
    double m_dHeight;
};

#endif

源程序(*Rect.cpp*

#include "Rect.h"

#include 
using namespace std;

Rect::Rect(double width, double height)
{
    cout << "Rect()" << endl;
    m_dWidth = width;
    m_dHeight = height;
}
Rect::~Rect()
{
    cout << "~Rect()" << endl;
}
double Rect::calcArea()
{
    cout << "Rect----calcArea()" << endl;
    return m_dWidth * m_dHeight;
}

主调程序(*demo.cpp*

在主调程序中定义两个指针,这两个指针都是Shape类的指针:一个指向Rect,一个指向Circle。然后使用指针shape1和shape2分别调用计算面积(calcArea())函数,目的是:看一看调用到的究竟是谁的计算面积的函数。最后,将两个指针对象销毁,目的是:看一看销毁父类指针的时候,能否销毁子类的对象呢?

#include 
#include 
#include "Circle.h"
#include "Rect.h"
using namespace std;

int main()
{
    Shape *shape1 = new Rect(3, 6); //传入宽和高
    Shape *shape2 = new Circle(5);  //传入半径 
    shape1 ->calcArea();
    shape2 ->calcArea();

    delete shape1;
    shape1 = NULL;
    delete shape2;
    shape2 = NULL;

    system("pause");
    return 0;
}

运行结果:

C++速成(2)_第62张图片

结果分析:

我们要实例化一个Rect的对象,显然就要先执行其父类的构造函数,然后再执行其本身的构造函数,同理,要实例化一个Circle的对象,首先也要执行其父类的构造函数,然后再执行其本身的构造函数,这就是前面四行代码的打印结果。接下来的两行就验证了我们之前所讲的一样,通过shape1和shape2去调用calcArea()函数的时候,调用到的都是Shape这个父类的calcArea()函数,并没有像我们想象中那样去调用Rect或Circle中的calcArea()函数,这个问题该如何来解决呢??。最后,在销毁Shape1和shape2的时候,只执行了Shape这个父类的析构函数,而并没有执行Rect和Circle本身的析构函数,这是为什么呢??(这个问题留待下一节课讲)。

针对执行结果中的第一个问题,我们前面已经学过,可以通过虚函数来达到多态的效果。也就是在计算面积(calcArea())函数前加上关键字virtual即可。当我们在三个头文件中的calcArea()函数前加上virtual后的运行结果如下:

C++速成(2)_第63张图片

这个时候,当用shape1和shape2去调用calcArea()函数的时候,调用到的就分别是Rect和Circle中的calcArea()函数了,这是我们想要的结果。


6.4 虚析构函数

之前我们已经重点学习了动态多态,那么在动态多态中还存在着问题—-内存泄漏。那么,怎么来解决多态中的内存泄漏问题呢?我们来通过一个例子来说明。

C++速成(2)_第64张图片

在这里我们定义了一个Shape的类,又定义了一个Circle的类,并且Circle类以public的方式继承Shape类。但是这个Circle类与我们在讲动态多态的时候讲到的略有不同。(如下所示)

C++速成(2)_第65张图片

最大的不同就是,多定义了一个数据成员,这个数据成员是一个坐标类(COordinate)的指针,这个指针代表的意思就是这个圆的圆心坐标,我们会在它的构造函数当中去实例化一个坐标对象,并且把这个坐标对象作为圆心坐标,并且适用m_pCenter去指向这个Coordinate对象。指向之后,我们会在析构函数执行的时候把这个对象再释放掉,这样就能够保证在实例化Circle后去使用它,使用完成之后还能将指针在堆中的内存释放掉,从而保证内存步泄漏。

可是在多态的使用当中,我们可以看一下:如果用父类指针去指向子类对象,并且通过父类指针去操作子类对象当中相应的虚函数,这个时候是没有问题的(这个前面已经讲过)。

C++速成(2)_第66张图片

可是后面的部分是有问题的,当我们使用delete去销毁对象,并且是借助于父类的指针想去销毁子类对象的时候,这个时候就出现问题了,为什么呢?我们在前面的课程给大家继承篇的时候,大家应该还记得:如果*delete*后边跟的是一个父类的指针,那么它只会执行父类的析构函数;如果跟着的是一个子类的指针,那么它既会执行子类的析构函数,也会执行父类的析构函数。可见,如果我们delete后面跟的是父类的指针(如上 *shape1),就只执行了父类的析构函数,又怎么能执行到Circle的析构函数呢?执行不到Circle的析构函数,那岂不是就造成了内存的泄漏了吗?因为我们在实例化Circle的对象的时候,是从堆中去申请的一段内存,并且把这段内存作为它的圆心坐标。从而可见,我们必须要解决这个问题,不解决的话,每次去使用的时候都会造成内存的泄漏问题。

讲到这里,我们可能会问:那之前的课程讲到这里,可没有说道有内存泄漏啊?那是有原因的!!!我们之前在给大家讲Circle这个类的时候,在Circle这个类当中一是没有指针型的数据成员*(*m_pCenter)*,二是没有在构造函数当中去申请内存。因为这两个原因,在这种情况下,它的构造函数当中其实什么也不做,既然什么也不做,那么我们执行它和步执行它区别就不大了,所以当时不会产生内存泄漏问题。但是,现在这种情况不一样了,那对于现在这种情况,我们如何来解决呢??我们必须要引入虚析构函数这个概念。

那么,什么是虚析构函数呢?

我们用关键字virtual去修饰析构函数,称此时的析构函数就叫做虚析构函数。从写法上来讲,也很简单,如下:

img

当我们用这种方式修饰了Shape的析构函数之后,那么Shape的子类,在这里就是Circle类中的析构函数前面既可以写上关键字virtual,也可以不写关键字virtual,如果不写,系统在编译时会自动加上。不过这里还是推荐大家写上,这样,将来再有类继承Circler的时候,也就知道Circle的析构函数是带有virtual的,那么它的子类的析构函数也就应该带有virtual了。当我们定义完成了虚析构函数之后呢我们在main函数当中,就可以适用之前的方式进行相应的操作了。这个时候我们再适用delete,如果此时在delete后面跟上父类指针的时候,那么父类指针指向的是哪个对象,那么这个对象的析构函数就会先可以执行,然后再执行它父类的析构函数,于是可以保证内存不被泄漏。

问题:关键字*virtual*既可以修饰普通的成员函数,也可以修饰析构函数,那它是不是就没有什么限制呢?

Virtual**的使用限制**

1) Virtual不能修饰普通的函数,必须是某个类的成员函数

2) Virtual不能修饰静态成员函数

如果用virtual去修饰一个静态成员函数的话,它不属于任何一个对象,它是和类是同生共死的,所以当用virtual去修饰的时候,也会造成编译错误

3) Virtual不能修饰内联函数

如果用viryual去修饰内联函数,那么,对于计算机来说,它会忽略掉inline关键字,而使它变成一个纯粹的虚函数。

4) Virtual不能修饰构造函数


6.5虚析构函数代码实践

题目描述:

/* **************************************** */

/* 虚析构函数 */

/* 要求:

​ 1. 定义Shape类,成员函数:calcArea(),构造函数,析构函数

​ 2. 定义Rect类,成员函数:calcArea(),构造函数,析构函数

​ 数据成员:m_dWidth,m_dHeight

​ 3. 定义Circle类,成员函数:calcArea(),构造函数,析构函数

​ 数据成员:m_dR, m_pCenter

​ 4. 定义Coordinate类,成员函数:构造函数,析构函数

​ 数据成员:m_iX,m_iY

/* **************************************** */

程序框架:

C++速成(2)_第67张图片

头文件(*Shape.h*

复制代码

#ifndef SHAPE_H
#define SHAPE_H

#include 
using namespace std;

class Shape
{
public:
    Shape();
    ~Shape();
    virtual double calcArea();
};

#endif

复制代码

源程序(*Shape.cpp*

复制代码

#include "Shape.h"
#include 
using namespace std;

Shape::Shape()
{
    cout << "Shape()" << endl;
}

Shape::~Shape()
{
    cout << "~Shape()" << endl;
}

double Shape::calcArea()
{
    cout << "Shape---calcArea()" << endl;
    return 0;
}

复制代码

头文件(*Circle.h*

复制代码

#ifndef CIRCLE_H
#define CIRCLE_H

#include "Shape.h"
#include "Coordinate.h" //为了使用这个类来定义一个指针,所以要包含进来

class Circle:public Shape
{
public:
    Circle(double r);
    ~Circle();
    virtual double calcArea();
protected:
    double m_dR;
    Coordinate *m_pCenter; //申明一个圆点类型的指针
};

#endif

复制代码

源程序(*Circle.cpp*

复制代码

#include "Circle.h"
#include 
using namespace std;

Circle::Circle(double r)
{
    cout << "Circle()" << endl;
    m_dR = r;
    m_pCenter = new Coordinate(3, 5); //实例化一个Coordinate对象作为圆心,并将m_pCenter指向这段内存
}
Circle::~Circle()
{
    cout << "~Circle()" << endl;
    delete m_pCenter;
    m_pCenter = NULL;

}
double Circle::calcArea()
{
    cout << "Circle----calcArea()" << endl;
    return 3.14*m_dR*m_dR;
}

复制代码

头文件(*Rect.h*

复制代码

#ifndef RECT_H
#define RECT_H

#include "Shape.h"

class Rect:public Shape
{
public:
    Rect(double width, double height);
    ~Rect();
    virtual double calcArea();
protected:
    double m_dWidth;
    double m_dHeight;
};

#endif

复制代码

源程序(*Rect.cpp*

复制代码

#include "Rect.h"

#include 
using namespace std;

Rect::Rect(double width, double height)
{
    cout << "Rect()" << endl;
    m_dWidth = width;
    m_dHeight = height;
}
Rect::~Rect()
{
    cout << "~Rect()" << endl;
}
double Rect::calcArea()
{
    cout << "Rect----calcArea()" << endl;
    return m_dWidth * m_dHeight;
}

复制代码

头文件(*Coordinate.h*

复制代码

#ifndef COORDINATE_H
#define COORDINATE_H

class Coordinate
{
public:
    Coordinate(int x, int y);
    ~Coordinate();
private:
    int m_iX;
    int m_iY;
};

#endif

复制代码

源程序(*Coordinate.cpp*

复制代码

 #include "Coordinate.h"
#include 
using namespace std;

Coordinate::Coordinate(int x, int y)
{
    cout <<"Coordinate()" << endl;
    m_iX = x;
    m_iY = y;
}
Coordinate::~Coordinate()
{
    cout <<"~Coordinate()" << endl;
}

复制代码

主调程序(*demo.cpp*

在这里我们还是先引用上一节课的demo.cpp,先不作任何修改如下:

复制代码

#include 
#include 
#include "Circle.h"
#include "Rect.h"
using namespace std;


int main()
{
    Shape *shape1 = new Rect(3, 6); //传入宽和高
    Shape *shape2 = new Circle(5);  //传入半径 
    shape1 ->calcArea();
    shape2 ->calcArea();

    delete shape1;
    shape1 = NULL;
    delete shape2;
    shape2 = NULL;

    system("pause");
    return 0;
}

复制代码

直接运行看结果:

C++速成(2)_第68张图片

我们此次要看的重点是构造函数和析构函数。首先,我们先去实例化Rect的时候,执行了Shape和Rect这两个构造函数;去实例化Circle的时候,执行了Shape和Circle这两个构造函数,;然后执行了Coordinate构造函数,这是因为我们在实例化Circle的时候,在Circle的构造函数当中,我们去实例化了一个Coordinate对象,这才使得Coordinate构造函数得以执行。但是,请大家注意:在后边所有打印出的内容当中,并没有去执行Circle的析构函数,这就意味着,在Circle的析构函数当中,去释放对象的过程没有得到执行。换句话说,就造成了内存的泄漏,泄露的是什么呢?泄漏的就是Coordinate这个对象。我们怎样才能够保证内存不被泄漏呢?我们需要加上关键字virtual,给谁加呢?给Shape的析构函数加就可以了,使Shape的析构函数变成一个虚析构函数,,于是继承Shape的其他类,比如Rect和Circle这两个类的析构函数也变成了虚析构函数,在这里我们最好将这两个子类的析构函数前面也加上virtual,看看现在的执行效果:

C++速成(2)_第69张图片

与之前的结果进行比较,主要看下半部分。当我们去销毁Shape1的时候,先执行的是Rect的析构函数,又执行了Shape的析构函数;当我们去销毁Shape2的时候,先执行的是Circle的析构函数,又执行了Coordinate的析构函数,最后执行的是Shape的析构函数。

6.6 虚函数与虚析构函数原理

前面我们学习了虚函数和虚析构函数,大家一定觉得很神奇,也很想知道,到底什么原因,采用了什么方法实现了虚函数和虚析构函数。这节课就为大家来揭晓虚函数和虚析构函数的实现原理。

关于实现原理的部分,因为涉及到函数指针,所以先给大家介绍一下函数指针的相关内容。

函数指针

我们在前面的课程已经学习过:如果通过一个指针指向一个对象,我们就叫它对象指针。那么指针除了可以指向对象之外,它也可以指向函数。那么,函数的本质是什么呢?函数的本质,其实就是一段写在内存中的二进制代码。我们可以通过指针来指向这段代码的开头,那么计算机就会从开头一直往下执行,直到函数的结尾,并且通过相关指令返回回来。如果我们有以下5个函数指针,这五个函数指针这里写的就是五个函数地址。

C++速成(2)_第70张图片

当我们使用的时候,比如如果我们适用Fun3_Ptr,那么就可以通过Fun3_Ptr拿到Fun3()这个函数的函数入口。当我们用指针指向到函数入口,并且命令计算机开始执行的时候,那么计算机就会使得Fun3中的二进制代码不断地得到执行,直到执行完毕为止。其他的函数也同样如此。那么,有的人会觉得函数指针很神奇,其实函数的指针与普通的指针本质上是一样的,也是由四个基本的内存单元组成,存储着一个内存的地址,这个内存地址就是这个函数的首地址

讲完了函数指针,我们就可以一起来学习多态的实现原理了。我们先来看一个例子。

C++速成(2)_第71张图片

在这个例子中,我们定义了一个Shape类,在这个Shape类中,还定义了一个虚函数和一个数据成员。然后,又定义了一个Circle子类(public继承自Shape)。大家请注意,我们在这里并没有给Circle定义一个计算面积的虚函数,也就是说,Circle这个子类所使用的也应该是Shape的虚函数来计算面积。当我们这样定义完成之后,我们来想一想,此时的虚函数如何来实现呢?

当我们去实例化一个Shape的对象的时候,在这个Shape对象当中,除了数据成员m_iEdge(表示边数)之外,还有另外一个数据成员—–虚函数表指针,它也是一个指针,占有4个基本内存单元。顾名思义,虚函数表指针就指向一个虚函数表。这个虚函数表会与Shape类的定义同时出现。在计算机中,虚函数表也是占有一定的内存空间的,这里假设虚函数表的起始位置是0xCCFF,那么这个虚函数表指针的值vftable_ptr就是0xCCFF。父类的虚函数表只有一个。通过父类实例化出来的所有的对象的虚函数表指针的值都是0xCCFF,以确保它的每一个对象的虚函数表指针都指向自己的虚函数表。在父类Shape的虚函数表当中,肯定定义了一个这样的函数指针,这个函数指针就是计算面积(calcArea())这个函数的入口地址。这里假设计算面积函数的入口地址是0x3355,那么虚函数表中的函数指针(calcArea_ptr)的值就是0x3355。调用的时候就可以先找到虚函数表指针,再通过虚函数指针找到虚函数表,再通过位置的偏移找到相应的虚函数的入口地址,从而最终找到当前定义的这个虚函数—-计算面积(calcArea())。整个过程如下所示:

C++速成(2)_第72张图片

当我们去实例化Circle的时候,又会是怎样的呢?如果我们实例化一个Circle对象,因为Circle当中并没有定义虚函数,但是它却从父类Shape当中继承了虚函数,所以我们在实例化Circle这个对象的时候,也会产生一个虚函数表。请大家注意,这个虚函数表是Circle自己的虚函数表,它的起始地址是0x6688。但是在Circle的虚函数表当中,它的计算面积的函数指针(calcArea_ptr)却是一样的,都是0x3355,这就能够保证:在Circle当中去访问父类的计算面积的函数,也能够通过虚函数表指针找到自己的虚函数表,在自己的虚函数表中找到的计算面积的函数指针也是指向父类的计算面积的函数入口的。整个过程如下所示:

C++速成(2)_第73张图片

那么,如果我们在Circle中定义了计算面积的函数(如下所示),又会是怎样的呢?

C++速成(2)_第74张图片

我们来看一看,对于Shape这个类来说,它的情况是不变的,有自己的虚函数表,并且在实例化一个Shape的对象之后,通过虚函数表指针指向自己的虚函数表,然后虚函数表当中有一个指向计算面积的函数,这样就Ok了。对于Circle来说,则有些变化。如下所示:

C++速成(2)_第75张图片

Circle的虚函数表与之前的虚函数表示一样的,但是,因为Circle此时自己已经定义了自己的计算面积的函数,所以它的虚函数表中关于计算面积的这个函数指针已经覆盖掉了父类当中的原有的指针的值。换句话说,0x6688当中的计算面积的函数指针的值变成了0x4B2C,而Shape当中的0xCCFF这个虚函数表中的所记录的计算面积的函数指针的值则是0x3355,这两者是不一样的。于是,我们如果用Shape的指针去指向Circle对象,那么,它就会通过Circle对象当中的虚函数表指针找到Circle的虚函数表,通过Circle的虚函数表(偏移量也是一样的),和父类一样,就能够找到Circle的虚函数的函数入口地址,从而执行子类当中的虚函数,这个就是多态的原理。

函数的覆盖与隐藏

在我们还没有学习多态的时候,如果定义了父类和子类。当父类和子类出现了同名函数,那么这时就称之为函数的隐藏。

函数的覆盖是今天要讲的知识。怎么就覆盖了呢?大家请注意:

如果我们没有在子类当中定义同名的虚函数,那么在子类虚函数表当中就会写上父类的相应的那个虚函数的函数入口地址。如果,在子类当中也定义了同名的虚函数,那么在子类的虚函数表当中就会把原来的父类的虚函数的函数入口地址覆盖一下,覆盖成子类的虚函数的函数地址,那么这种情况就称之为函数的覆盖。

虚析构函数的实现原理

虚析构函数的特点是:当我们在父类当中,通过virtual修饰析构函数之后,我们通过父类的指针再去指向子类的对象,然后通过delete接父类指针就可以释放掉子类的对象。

理论前提:*执行完子类的析构函数就会执行父类的析构函数。*

有了这个前提,我们想一想,如果有了父类的指针,通过delete的方式去释放子类的对象,那么只要能够实现通过父类的指针执行到子类的析构函数就可以实现了。我们来看一看例子:

C++速成(2)_第76张图片

在这个例子当中,我们给Shape类多加了一个函数:虚析构函数。在Circle类当中,我们也定义了它自己的虚析构函数。如果你不写,计算机会默认给你定义一个虚析构函数的,前提是,必须在父类中必须有关键字virtual修饰的析构函数。如果我们在main函数当中,通过父类的指针来指向子类的对象(如下)

C++速成(2)_第77张图片

然后,通过delete接父类的指针来释放子类的对象,那么这个时候虚函数表如何来工作呢?我们来看一看。

C++速成(2)_第78张图片

如果我们在父类当中定义了虚析构函数,那么,在父类当中的虚函数表中就会有一个父类的析构函数的函数指针,而在子类的虚函数表中也会产生一个子类的析构函数的函数指针,指向的是子类的析构函数。这个时候,如果我们使用父类的指针指向子类的对象,或者说,使用Shape的指针来指向Circle的对象,那么,通过deleite来接shape这样一个指针的时候,我们就可以同shape来找到子类的虚函数表指针,然后通过虚函数表指针找到虚函数表,再通过虚函数表找到子类的析构函数,从而使得子类的析构函数得以执行。子类的析构函数执行完毕之后,系统就会自动执行父类的析构函数,这个就是虚析构函数的实现原理。接下来向大家证明一下:虚函数表指针的存在,从而证明前面给大家讲的这一套理论的真实性。

虚函数表代码实践

题目描述:

/* ******************************************************* */

/* 证明虚函数表的存在

要求:

​ 1. 定义Shape类, 成员函数:calcArea(),构造函数,析构函数

​ 2. 定义Circle类,成员函数:构造函数,析构函数

​ 数据成员:m_iR

概念说明:

​ 1. 对象的大小

​ 2. 对象的地址

​ 3. 对象成员的地址

​ 4. 虚函数表指针

*/

/* ******************************************************* */

几个概念解释:

1. *什么是对象的大小呢?*

指在类实例化的对象当中,它的数据成员所占据的内存大小,而不包括成员函数。比如上面的Shape类来说,它没有定义任何数据成员,那么,理论上它应该是不占据内存的,但是呢?待会看一看程序结果。对于Circle类来说,它有一个数据成员m_iR,其实int类型,理论上它应该占据4个基本内存单元,所以每次实例化一个Circle类的对象,那么这个实例化的对象的大小就应该是4.

2. *什么是对象的地址?*

指通过一个类实例化一个对象,那么这个对象在内存当中就会占有一定大小的内存单元,那么这个内存单元的首地址就是这个对象的地址。

3. 什么是对象成员的地址?

指当用一个类去实例化一个对象之后,这个对象当中可能有一个或多个数据成员,那么每一个数据成员所占据的地址就是这个对象的成员地址。由于对象的每一个数据成员,因为它的数据类型不同,所以它占据的内存大小也有所不同,从而其地址也是不同的。

4. *什么是虚函数表指针?*

指的是在具有虚函数的情况下,实例化一个对象的时候,这个对象的第一块内存当中所存储的是一个指针,这个指针就是函数表的指针。因为其也是一个指针,所以其占据的内存大小也应该是4。那么就通过这个特点,我们就可以通过计算对象的大小来证明虚函数表指针的存在。

程序框架:

C++速成(2)_第79张图片

头文件(*Shape.h*

复制代码

#ifndef SHAPE_H
#define SHAPE_H

#include 
using namespace std;

class Shape
{
public:
    Shape();
    ~Shape();
    double calcArea();
};

#endif

复制代码

源程序(*Shape.cpp*

复制代码

#include "Shape.h"

Shape::Shape()
{
    //cout << "Shape()" << endl;
}

Shape::~Shape()
{
    //cout << "~Shape()" << endl;
}

double Shape::calcArea()
{
    cout << "Shape-->calcArea()" << endl;
    return 0; 
}

复制代码

头文件(*Circle.h*

复制代码

#ifndef CIRCLE_H
#define CIRCLE_H

#include "Shape.h"

class Circle:public Shape
{
public:
    Circle(int r);
    ~Circle();
protected:
    int m_iR;
};

#endif

复制代码

源程序(*Circle.cpp*

复制代码

#include "Circle.h"
Circle::Circle(int r)
{
    m_iR = r;
}
Circle::~Circle()
{

}

复制代码

主调程序(*demo.cpp*

复制代码

#include 
#include 
#include "Circle.h"
using namespace std;

int main()
{

    Shape shape;
    cout << sizeof(shape) << endl;

    Circle circle(100);
    cout << sizeof(circle) << endl;

    system("pause");
    return 0;
}

复制代码

在main函数中,首先我们先实例化了一个Shape类的对象shape,而我们注意到:在Shape.h中,Shape对象中没有任何的数据成员,那么理论上打印它的大小应该打印出的是0。那么真相是什么呢?一会揭晓!!!接着我们又实例化了一个Circle类对象circle,并且传入了参数100,这个100会传值给m_iR,然后我们打印circle这个对象的大小,理论上circle这个对象中含有一个int类型的数据成员,那么理论上circle对象的大小应该就是4。那么,我们现在来揭晓答案。

程序运行结果如下:

img

我们可以看到,在运行的结果当中,第一行打印出的是1,第二行打印出的是4。对于打印出的4来说,是符合我们预期的。那么,对于打印出的1如何来理解呢????我们来想一想:当Shape类没有任何数据成员的时候,而这个类也是可以实例化的,它实例化一个对象后,那么,作为一个对象来说,它必须要标明自己的存在。C++如何来完成这样的工作呢?C++*对于一个数据成员都没有的情况,用1*个内存单元去标定它,也就是说,这个内存单元只标定了这个对象的存在。如果这个对象里面有数据成员,那么这个1也就不存在了(比如Circle类的情况,它不会变成5,也就是说,它已经有了数据成员,能够标定它的存在了,那么就不需要额外的内存来标定它的存在了)。

* *然后我们再来看其他的。首先,在main函数中定义一个指针,并且通过这个指针来指向这个对象。请大家注意,这个指针比较奇特。这个指针是指向int类型的指针,而指向的这个对象shape是一个Shape类型,那么这种情况下可以指向吗?其实,直接指向是不可以的。所以,我们必须要使用强制类型转换。即将Shape类型的一个地址转换成一个int类型的地址,即:int p = (int )&shape;这个是不得已,否则我们没有办法进行后续的操作。我们指向之后,就可以通过cout语句将这个地址打印出来,则打印出的地址就是shape这个对象的地址。那么main函数如下:

复制代码

int main()
{

    Shape shape;
    //cout << sizeof(shape) << endl;

    int *p = (int *)&shape;
    cout << p << endl;

    Circle circle(100);
    //cout << sizeof(circle) << endl;

    system("pause");
    return 0;
}

复制代码

运行结果如下:

img

那么这个shape对象的地址与下面的circle对象的地址是不是同一个地址呢?我们来看一看。

复制代码

int main()
{
    Shape shape;
    //cout << sizeof(shape) << endl;
    int *p = (int *)&shape;
    cout << p << endl;

    Circle circle(100);
    //cout << sizeof(circle) << endl;
    int *q = (int *)&circle;
    cout << q << endl;

    system("pause");
    return 0;
}

复制代码

运行结果:

img

我们可以看到,shape这个对象与circle这个对象在内存中所占的地址是不同的。这个是顺理成章的事,毕竟是两个不同的对象。

接下来,我们讲一讲,作为指针p此时指向的shape这个对象,那么这个对象此时只有一个标识符来表明这个对象的存在。对于circle来说,指针q指向这个对象之后,那么这个对象的第一个位置就应该放的是circle的数据成员m_iR,我们来打印一下,验证一下是不是这样的。

复制代码

int main()
{
    Shape shape;
    //cout << sizeof(shape) << endl;
    int *p = (int *)&shape;
    cout << p << endl;

    Circle circle(100);
    //cout << sizeof(circle) << endl;
    int *q = (int *)&circle;
    cout << q << endl;
    cout << (unsigned int)(*q) << endl;

    system("pause");
    return 0;
}

复制代码

运行结果:

C++速成(2)_第80张图片

我们看到,第三行打印出的是100。那么这个100是怎么来的呢?这个100就是在我们实例化circle的时候,传入的参数100,而且这个传入的100赋值给了circle这个对象的数据成员m_iR。这个m_iR就处在circle这个对象地址的第一个位置。我们的指针q所指向的就是m_iR,当然,它也就是circle这个对象的地址。我们把这个地址的值打印出来,那就正好打印出来了m_iR。

讲到这为止,我们都是在讲虚函数前面的内容,只是对对象的理解。下面要讲的就是与虚函数相关的了。

首先,我们修改一下Shape.h头文件,在Shape这个类中的calcArea()函数前面加上关键字virtual。大家请注意,此时Shape的析构函数还是普通的析构函数,只不过它计算面积calcArea()这个成员函数变成了虚函数。那么在这种情况下,如果去实例化一个Shape的对象,就应该具有一个虚函数表指针。也就是说,如果原来它占的是一个内存的大小,那么,当我们已经拥有一个虚函数表指针的时候,那么这个Shape就应该占有4个内存的大小(因为一个指针所占有的内存单元的4)。我们来验证一下是不是这样的??

头文件(*Shape.h*

复制代码

#ifndef SHAPE_H
#define SHAPE_H

#include 
using namespace std;

class Shape
{
public:
    Shape();
    ~Shape();
    virtual double calcArea(); 
};

#endif

复制代码

主调程序(*demo.cpp*

复制代码

#include 
#include 
#include "Circle.h"
using namespace std;

int main()
{
    Shape shape;
    cout << sizeof(shape) << endl;

    system("pause");
    return 0;
}

复制代码

运行结果:

img

我们看到,打印出来的结果已经不是1了,而是我们期望看到的4。这就充分证明了,我们加了virtual关键字之后,在实例化Shape对象的时候,那么它实例化出来的对象当中就含有一个虚函数表指针。

接下来再来打开Shape.h文件,将计算面积的函数变为普通函数,同时在析构函数前面加上关键字virtual,使其变成虚析构函数。如下:

复制代码

#ifndef SHAPE_H
#define SHAPE_H

#include 
using namespace std;

class Shape
{
public:
    Shape();
    virtual ~Shape();
    double calcArea();
};

#endif

复制代码

这种情况下,我们可以看到,只有析构函数前面加了virtual关键字。那么在这种情况下,如果我们实例化一个Shape的对象,那么这个Shape的对象究竟占多少内存单元呢?是不是只有虚析构函数的情况下,作为Shape对象来说,也有一个虚函数表指针呢?我们就可以通过打印的方式来看一看shape这个对象的大小,如果结果是4,那么就说明:当我们去定义一个虚析构函数的时候,它同样会在实例化对象的时候,会产生一个虚函数表,并且在对象当中,产生一个虚函数表指针。

主调程序(*demo.cpp*

复制代码

#include 
#include 
#include "Circle.h"
using namespace std;

int main()
{
    Shape shape;
    cout << sizeof(shape) << endl;

    system("pause");
    return 0;
}

复制代码

运行结果:

img

在运行结果当中,我们看到,打印出来的结果也是4。那么,从这一点,我们就可以充分证明:虚析构函数同样能够使类在实例化对象的时候产生一个虚函数表,并且在实例化对象当中产生一个虚函数表指针。

那么,既然虚函数表指针会存在于父类对象当中,那么它也一样会存在于子类对象当中。

头文件保持之前的不变,主调程序如下:

复制代码

#include 
#include 
#include "Circle.h"
using namespace std;

int main()
{
    Shape shape;
    cout << sizeof(shape) << endl;

    Circle circle(100);
    cout << sizeof(circle) << endl;


    system("pause");
    return 0;
}

复制代码

运行结果:

C++速成(2)_第81张图片

通过打印,我们可以看到,第一行是4,也就是说shape的大小为4。第二行是8,这个8是怎么来的呢?其中的4个内存单元是由circle对象的数据成员m_iR所占据的(因为m_iR是int类型),另外4个正如大家所想,它就应该是虚函数表指针所占据的。因为父类当中定义了虚析构函数,这个虚析构函数能够传给子类,子类也就因为有虚析构函数的原因,在实例化的时候产生了虚函数表,并且在对象当中产生了虚函数表指针。那么,这个虚函数表指针在哪个位置呢?我们说其在对象的前4个内存单元。如何而来证明这一点呢,我们可以这样:

首先,我们使用指针p去指向shape对象,用指针q去指向circle对象。这种指向结束之后,我们就可以通过*p来打印shape对象当中的前4个基本单元的值。那么这个被打印出来的值是什么值呢?这个值就是Shape这个类的虚函数表的地址。同时,我们也可以在打印*q的值,那么*q就是指当前的指针q所指向的circle这个对象的地址。那么,作为circle这个对象来说,circle这个对象的前4个基本内存单元就是它的虚函数表指针,那么这个虚函数表指针究竟是什么值呢?我们就可以通过*q来打印出来。

主调程序(*demo.cpp*

复制代码

#include 
#include 
#include "Circle.h"
using namespace std;

int main()
{
    Shape shape;
    //cout << sizeof(shape) << endl;

    int *p = (int *)&shape;
    //cout << p << endl;
    cout << (unsigned int)(*p) << endl;

    Circle circle(100);
    //cout << sizeof(circle) << endl;

    int *q = (int *)&circle;
    //cout << q << endl;
    cout << (unsigned int)(*q) << endl;

    system("pause");
    return 0;
}

复制代码

运行结果:

img

我们可以看到,作为shape来说,它的虚函数表的地址是13229484,对于circle来说,它的虚函数表的地址是13229204。对于circle来说,它的前4个内存单元是虚函数表的地址,那么接下来的4个内存单元就应该是m_iR的地址了。为了能够表明这一点,我们让指针q实行++操作,于是这个q指针就往后指向了一个位置。那么指向的这个位置一共跳过了4个基本内存单元,正好指向了m_iR,此时再来打印*q,应该就能打印出100这个值来了。我们来看一看:

复制代码

#include 
#include 
#include "Circle.h"
using namespace std;

int main()
{
    Shape shape;
    //cout << sizeof(shape) << endl;

    int *p = (int *)&shape;
    //cout << p << endl;
    cout << (unsigned int)(*p) << endl;

    Circle circle(100);
    //cout << sizeof(circle) << endl;

    int *q = (int *)&circle;
    //cout << q << endl;
    cout << (unsigned int)(*q) << endl;
    q++;
    cout << (unsigned int)(*q) << endl;
    system("pause");
    return 0;
}

    int *p = (int *)&shape;
    cout << p << endl;

    Circle circle(100);
    //cout << sizeof(circle) << endl;
    int *q = (int *)&circle;
    cout << q << endl;
    cout << (unsigned int)(*q) << endl;

复制代码

运行结果:

C++速成(2)_第82张图片

我们可以看到,第三行打印出来的值就是100,这充分说明了,在多态的情况下,虚函数表指针在对象当中所占据的内存位置是每个对象的前4个基本内存单元,后面依次排列的才是这个对象的其他的数据成员。

6.2 纯虚函数与抽象类

下面通过一个例子来说明纯虚函数的定义方法

img

在这个类当中,我们定义了一个普通的虚函数,并且也定义了一个纯虚函数。那么,纯虚函数是什么呢??从上面的定义可以看到,纯虚函数就是没有函数体,同时在定义的时候,其函数名后面要加上“*= 0***”**。

纯虚函数的实现原理

本节从虚函数表的角度来说明纯虚函数的实现原理。

img

上面就是我们在前面课程讲到的多态的实现原理,在讲这一部分的时候,讲到了虚函数表以及虚函数表指针。如果我们定义了Shape这样的类,那么,Shape类当中,因为有虚函数和纯虚函数,所以,它一定有一个虚函数表,当然,也就一定有一个虚函数表指针。如果是一个普通的虚函数,那么,在虚函数表中,其函数指针就是一个有意义的值;如果是一个纯虚函数,那么,在虚函数表中,其函数指针的值就是0。也就是说,在虚函数表当中,如果是纯虚函数,那么就实实在在的写上0,如果是普通的虚函数,那就肯定是一个有意义的值。通过对纯虚函数的讲解,大家也一定会发现:纯虚函数也一定是某个类的成员函数,那么,包含纯虚函数的类也叫作什么呢?我们把包含纯虚函数的类称之为抽象类。比如刚刚举的Shape类当中就含有一个计算周长的纯虚函数,那么,我们就说这个Shape类是一个抽象类。大家可以想一想,如果我们使用Shape这个类去实例化一个对象,那么这个对象实例化之后,如果想要去调用纯虚函数(比如要去调用这个计算周长的纯虚函数),那怎么去调用呢???我们说,显然是无法调用的。所以,我们得出一个结论:对于抽象类来说,*C++*是不允许它去实例化对象的。也就是说,抽象类无法实例化对象。那么,如果我们强行写成如下形式:

img

比如上面的,从栈中或者堆中去实例化一个对象,此时,如果我们去运行程序的话,计算机就会报错。而且,不仅如此,对于抽象类的子类也可以是抽象类。比如:我们如果定义一个Person的类如下:

img

因为人是要工作的,所以定义了一个work()函数,同时还定义了一个打印信息的函数。由于人比较抽象,所以也不知道工作要做啥,所以就定义work()为纯虚函数,同时,也不知道该打印啥信息,所以也定义成了纯虚函数。当我们使用Worker这个类去继承Person类的时候,我们可以想象一下,对于工人来说,其工种是非常多的,单单一个工人,我们倒是可以一些他的信息(比如:这个工人的名字,工号等等),但是,这个工人是什么工作,具体是做什么的,我们也没有办法清晰明了的描述出来,所以这个时候,我们可以也把它定义成一个纯虚函数,如下所示。此时,这个Worker类作为Person的子类来说,它也是一个抽象类。

img

当我们明确了这个工人是什么工种(比如他是一名清洁工),清洁工这个类继承了Worker类(清洁工也是工人的一种),那么work()这个函数就有了一个明确的定义了(比如:他的工作就是扫地,我们可以将其打印出来),如下图所示。那么,此时,我们就可以使用清洁工(Dustman)这个类去实例化对

img

到此,我们需要强调说明一点的是:对于抽象类来说,它无法实例化对象,而对于抽象类的子类来说,只有把抽象类中的纯虚函数全部实现之后,那么这个子类才可以实例化对象

纯虚函数和抽象类的代码实践

题目描述:

/* ************************************** */

/* 纯虚函数和抽象类

​ 1. Person类,成员函数:构造函数,虚析构函数,纯虚函数work(),数据成员:名字 m_strName

​ 2. Worker类,成员函数:构造函数,work(),数据成员:年龄m_iAge

​ 3. Dustman类,成员函数:构造函数,work()

*/

/* ************************************** */

程序框架:

img

首先,我们来验证一下,含有纯虚函数的类,即抽象类能否实例化对象

头文件(*Person.h*

复制代码

#ifndef PERSON_H
#define PERSON_H

#include 
using namespace std;
class Person
{
public:
    Person(string name);
    virtual void work() = 0; //定义函数work()为纯虚函数
    virtual ~Person() { }
private:
    string m_strName;
};

#endif

复制代码

源程序(*Person.cpp*

复制代码

#include "Person.h"

Person::Person(string name)
{
    m_strName = name;
}

复制代码

我们看到Person.h文件中定义了一个纯虚函数work(),即此时Person这个类是一个抽象类。接下来我们在main函数中去实例化一个Person类的对象看看情况如何?

主调程序(*demo.cpp*

复制代码

#include 

#include "Person.h"

using namespace std;

int main()
{
    Person person("Zhangsan");

    system("pause");
    return 0;
}

复制代码

此时,我们调试一下程序(F7)看一看结果如何?运行结果如下:

img

我们可以看到错误提示:”Person”是一个抽象类,不能实例化。当我们双击这一行错误提示行的时候,箭头就会指向程序代码中的main函数中的实例化语句。

接着,我们看一看抽象类的子类是否能够实例化?

我们使用*Worker*这个类来继承*Person*类,如下:

头文件(*Worker.h*

复制代码

#ifndef WORKER_H
#define WORKER_H

#include "Person.h"
class Worker:public Person
{
public:
    Worker(string name, int age);
private:
    int m_iAge;
};

#endif

复制代码

源程序(*Worker.cpp*

复制代码

#include 
#include "Worker.h"

using namespace std;

Worker::Worker(string name, int age):Person(name)
{
    m_iAge = age;
}

复制代码

我们看到,在Worker这个类当中,我们并没有对work()这个函数做特别的处理,而是从Person类中完全继承下来了。由于Person类中的work()函数是一个纯虚函数,那么这就导致Worker这个类也变成了一个抽象类。那么,此时我们在main函数中去实例化一个Worker类的对象的话,结果如何呢?

主调程序(*demo.cpp*

复制代码

#include 
#include 

#include "Person.h"
#include "Worker.h"

using namespace std;

int main()
{
    Worker worker("ZhangSan", 30);

    system("pause");
    return 0;
}

复制代码

此时,我们调试一下程序(F7)看一看结果如何?运行结果如下:

img

我们可以看到错误提示:”Worker”是一个抽象类,不能实例化。当我们双击这一行错误提示行的时候,箭头就会指向程序代码中的main函数中的实例化语句。

接下来,我们在Worker这个类中,对work()函数进行实现,然后再实例化一个Worker类对象,看一看结果又是如何?

头文件(*Worker.h*

复制代码

#ifndef WORKER_H
#define WORKER_H

#include "Person.h"
class Worker:public Person
{
public:
    Worker(string name, int age);
    virtual void work();
private:
    int m_iAge;
};

#endif

复制代码

我们看到对于Worker这个类来说,其中也有一个work()函数。我们来看一看Worker.cpp文件

源程序(*Worker.cpp*

复制代码

#include 
#include "Worker.h"

using namespace std;

Worker::Worker(string name, int age):Person(name)
{
    m_iAge = age;
}

void Worker::work()
{
    cout << "work()" << endl;
}

复制代码

在这里,我们看到,在Worker.cpp中对work()这个函数进行了实现,此时我们再来实例化Worker对象如下:

主调程序(*demo.cpp*

复制代码

#include 
#include 

#include "Person.h"
#include "Worker.h"

using namespace std;

int main()
{
    Worker worker("ZhangSan", 30);

    system("pause");
    return 0;
}

复制代码

此时,我们调试一下程序(F7)看一看结果如何?运行结果如下:

img

从结果看到,此时计算机编译通过。这样,也就从另一个角度说明了,Worker这个类虽然继承自抽象类Person类,但此时Worker类中已经没有了纯虚函数,但凡是虚函数,也已经被实现了。

从上面我们可以看到:如果Worker类中的work()函数也不进行实现,那么Worker这个类仍然是一个抽象类,也就不能进行实例化,此时,就待依赖Worker的子类来实现纯虚函数,从而就将此重任交给了清洁工(Dustman)这个类。那么,往往这种情况是存在的,因为对于人类来说,劳动这个函数很抽象,不知道该如何劳动,而到了Worker这个类中,劳动仍然比较抽象,因为我们虽然知道其是一个工人,但工人的工种有很多,工种不同,其具体劳动也不是不一样的,所以在Worker这个类当中,对work进行实现,往往显得也不太合适。那么,更多情况下,是在更具体的类中去实现work这个函数。比如说,在清洁工(Dustman)这个类中,工作就已经很明确了,他的工作就是扫地,所以对work进行实现的时候,就打印出“扫地”就行了,如下:

头文件(*Worker.h*

复制代码

#ifndef WORKER_H
#define WORKER_H

#include "Person.h"
class Worker:public Person
{
public:
    Worker(string name, int age);
private:
    int m_iAge;
};

#endif

复制代码

源程序(*Worker.cpp*

复制代码

#include 
#include "Worker.h"

using namespace std;

Worker::Worker(string name, int age):Person(name)
{
    m_iAge = age;
}

复制代码

头文件(*Dustman.h*

复制代码

#ifndef DUSTMAN_H
#define DUSTMAN_H

#include "Worker.h"

class Dustman:public Worker
{
public:
    Dustman(string name, int age);
    virtual void work();
};
#endif

复制代码

源程序(*Dustman.cpp*

复制代码

#include "Dustman.h"
#include 
using namespace std;
Dustman::Dustman(string name, int age):Worker(name,age)
{

}

void Dustman::work()
{
    cout << "扫地" << endl;
}

复制代码

主调程序(*demo.cpp*

复制代码

#include 
#include "Dustman.h"
#include "Person.h"
#include "Worker.h"

using namespace std;

int main()
{
    Dustman dustman("ZhangSan", 30);

    system("pause");
    return 0;
}

复制代码

此时,我们调试一下程序(F7)看一看结果如何?运行结果如下:

img

从结果看到,此时计算机编译通过。从而我们可以得到如下结论:对于一个含有纯虚函数的类(抽象类)来说,其无法进行实例化

6.2.2 C++接口类

接口类

问题:什么是接口类??

如果在一个抽象类中,仅含有纯虚函数,而不含有其他的任何东西,那么我们就称之为接口类。即:仅含有纯虚函数的类称为接口类

那么,我们如何理解接口类的定义呢?

也就是说,在类当中,没有任何的数据成员,只有成员函数,而这仅有的成员函数当中,其又都是纯虚函数,此时,我们就把这样的类称之为接口类。

下面通过一个例子来说明接口类的定义方法。如下:

C++速成(2)_第83张图片

Shape这个类在之前的课程中已经讲过,此时,如果我们将Shape类中的计算面积和计算周长这两个成员函数都定义成纯虚函数,并且Shape类此时还不含有别的成员函数以及数据成员,那么,此时我们就称Shape这个类为接口类。

在实际的使用过程中,接口类更多的是用来表达一种能力或协议。这句话又该如何理解呢?我们还是通过一个例子来进行说明。

img

比如,我们有上面的一个类,这个类的意思就是“会飞的”。如果我们要是有会飞这种能力,那么我们就应该事先以下两个函数:起飞和降落。在这里,我们可以看到,起飞和降落这两个函数都是虚函数,那么继承Flyable这个类的子类就必须要实现在Flyable这个类当中所定义的起飞和降落这两个纯虚函数,实现之后,它就具有了“会飞”这种能力。我们来看一下:如果我们定义了鸟(Bird)这个类,并且Bird去继承Flyable这个类(如下)。

C++速成(2)_第84张图片

当形成了这种继承关系之后,如果我们要实例化Bird,那么我们就必须要在Bird这个类当中去实现起飞和降落这两个函数(如上面省略号表示的函数体)。大家可以想一想,如果我们在使用的时候,有如下一个函数flyMatch()。

C++速成(2)_第85张图片

我们看到,flyMatch这个函数所要求传入的指针是“会飞”的,也就是说,任何会飞的对象的指针都可以传入进来。Bird这个类实现了Flyable,即Bird是一个子类。前面我们讲过,当我们用一个子类去继承父类的时候,就形成了一种is-a的关系。当形成了这种is-a的关系之后,我们就可以在flyMatch,也就是飞行比赛当中传入两个指针,这两个指针要求传入的类只要是Flyable的子类就可以了。那么这个时候,我们知道Bird是Flyable的子类,那么在flyMatch中就可以传入Bird类的对象指针。传入进来的对象指针就可以调用Flyable类中所要求必须实现的起飞和降落这两个函数了。这个时候,大家应该隐隐的感觉到,其实Flyable这个类就相当于是一种协议,你如果想要参加飞行比赛,那么你就一定要会飞;那么如果你会飞,你一定实现了起飞和降落这两个函数;那么你实现了这两个函数,那么我们就可以再flyMatch(飞行比赛)中去调用了。同样的道理,如果我们有如下一个类,这个类叫做CanShot(能够射击,即一种具有射击的能力)。

C++速成(2)_第86张图片

在这个类当中,我们定义了两个纯虚函数:瞄准和装弹。此时,如果我们再定义一个Plane(飞机类),飞机可以进行多继承,其继承了Flyable(会飞的)和CanShot(可以射击的)这两个类,如下所示:

C++速成(2)_第87张图片

那么这个时候,想要实例化Plane,那么,它就要必须实现Flyable中的起飞(takeoff)和降落(land)以及CanShot中的瞄准(aim)和装弹(reload)。如果我们把这些都实现了,那么,假设我们有如下一个函数fight(战斗)。战斗的时候,我们要求,只需要具有能够射击这种能力就可以了,如下所示:

C++速成(2)_第88张图片

那么,作为Plane(飞机)这个二类来说,它既是Flyable的子类也是CanShot(射击)这个类的子类。Fight这个函数要求只要是CanShot的子类就可以了。如果我们此时传入Plane这个类的对象指针给fight,其就是满足fight函数参数要求的。那么传入进来的对象指针必定是CanShot这个类的对象指针,呢么它就一定实现了瞄准和装弹这两个函数,实现之后,我们就可以再fight战术中去调用者两个函数了。

对于接口类来说,更为复杂的情况如下所示:

C++速成(2)_第89张图片

当我们定义一个Plane(飞机)这样的一个类的时候,对于飞机来说,他一定是能够会飞的,所以我们继承Flyable这个类,这样飞机就有了好“会飞”的能力。如果想要去实例化飞机,,那么此时我们就必须要实现起飞和降落这两个函数。而战斗机是可以继承飞机的,同时战斗机还应该具有射击的能力,这个时候它也是一种多继承,如下所示:

C++速成(2)_第90张图片

这种多继承请大家注意:它的第一个父类(Plane)并不是一个接口类,它的第二个父类(CanShot)则是一个接口类。这种情况下,我们从逻辑上可以理解为:战斗机是继承了飞机的绝大部分属性,同时还具有能够射击这样的功能,那么我们就需要在战斗机中去实现CanShot这个类当中的瞄准和装弹这两个函数。实现完成之后,如果我们有一个函数airBattle(空战),而空战的时候就需要传入两个战斗机的对象指针,如下所示:

C++速成(2)_第91张图片

因为此时我们传入的是战斗机的对象指针,那么战斗机对象当中一定实现了CanShot中瞄准和装弹这两个函数,同时,也肯定实现了Flyable中的起飞和降落这两个函数,于是我们就可以放心地在airBattle(空战)这个函数中去调用Flyable和CanShot所约定的函数了。

接口类代码实践

题目描述:

/* ********************************** */

/* 接口类

​ 1. Flyable类,成员函数:takeoff(起飞)、land(降落)

​ 2. Plane类,成员函数:takeoff、land、printCode,数据成员:m_strCode

​ 3. FighterPlane类,成员函数:构造函数、takeoff、land

​ 4. 全局函数flyMatch(Flyable *f1, Flyable *f2)

*/

/* ********************************** */

程序框架:

C++速成(2)_第92张图片

头文件(*Flyable.h*

复制代码

#ifndef FLYABLE_H
#define FLYABLE_H

//Flyable只含有纯虚函数,没有其他的成员函数,也没有任何的数据成员,
//所以不需要.cpp文件,这样的类就称之为接口类
class Flyable 
{
public:
    virtual void takeoff() = 0;
    virtual void land() = 0;
};

#endif

复制代码

头文件(*Plane.h*

复制代码

#ifndef PLANE_H
#define PLANE_H

#include 
#include "Flyable.h"
using namespace std;

class Plane:public Flyable
{
public:
    Plane(string code);
    virtual void takeoff();
    virtual void land();
    void printCode();
private:
    string m_strCode;
};

#endif

复制代码

源程序(*Plane.cpp*

复制代码

#include 
#include "Plane.h"

using namespace std;

Plane::Plane(string code)
{
    m_strCode = code;
}
void Plane::takeoff()
{
    cout << "Plane --> takeoff()" << endl;
}
void Plane::land()
{
    cout << "Plane --> land()" << endl;
}
void Plane::printCode()
{
    cout << m_strCode << endl;
}

复制代码

头文件(*FighterPlane.h*

复制代码

#ifndef FIGHTERPLANE_H
#define FIGHTERPLANE_H

#include "Plane.h"
#include 
using namespace std;

class FighterPlane:public Plane
{
public:
    FighterPlane(string code);
    virtual void takeoff();
    virtual void land();
};
#endif

复制代码

源程序(*FighterPlane.cpp*

复制代码

#include 
#include "FighterPlane.h"

using namespace std;

FighterPlane::FighterPlane(string code):Plane(code)
{
}
void FighterPlane::takeoff()
{
    cout << "FighterPlane --> takeoff()" << endl;
}
void FighterPlane::land()
{
    cout << "FighterPlane --> land()" << endl;
}

复制代码

主调程序(*demo.cpp*

复制代码

#include <iostream>
#include "stdlib.h"
#include <string>
#include "FighterPlane.h"

using namespace std;

void flyMatch(Flyable *f1, Flyable *f2)
{
    f1->takeoff();
    f1->land();
    f2->takeoff();
    f2->land();
}

int main()
{
    Plane p1("001");
    Plane p2("002");
    p1.printCode();
    p2.printCode();
    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

复制代码

我们来看一看运行结果(按F5)

C++速成(2)_第93张图片

从运行结果,我们可以看到,首先打印出来两行”001”和”002”,这个毫无疑问是通过printCode函数打印出来的;接下来打印出来的四行,分别是f1的起飞和降落,f2的起飞和降落,这个也说明了Plane可以正确的作为参数传递给flyMatch。通过打印结果,我们可以看到,对于flyMatch这个函数来说,它相当于限制了传入参数的参数类型,并且可以在函数体中放心地去调用接口类当中所定义的纯虚函数,这个就是接口类最为常见的用法。接下来我们使用FighterPlane这个类来试一试,看看其能不能作为参数传入到flyMatch当中,修改main函数如下:

复制代码

int main()
{
    FighterPlane p1("001");
    FighterPlane p2("002");
    p1.printCode();
    p2.printCode();
    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

复制代码

我们来看一看运行结果:

C++速成(2)_第94张图片

通过运行效果,我们可以看到,打印飞机编号跟之前是一样的,但后面四行是不一样的。我们可以看到,通过f1和f2调用起飞和降落函数呢,实际上调用的是FighterPlane(战斗机)这个类当中的起飞和降落函数。此外,我们再做一个小小的修改:要求在FighterPlane中不仅要继承Plane这个类,而且还要继承Flyable这个类,同时,不让Plane继承Flyable这个类(注意此时就要去掉之前实现的纯虚函数takeoff和land)。这个时候,我们发现此时是一个多继承:FighterPlane既继承了Plane这个类,也继承了Flyable这个类。在这种情况下,对于FighterPlane来说,它就有了两个父类,这就意味着:FighterPlane既是一个Plane,也是一个Flyable。

修改后的头文件(*Plane.h*

复制代码

#ifndef PLANE_H
#define PLANE_H

#include 
#include "Flyable.h"
using namespace std;

class Plane
{
public:
    Plane(string code); 
    void printCode(); 
private:
    string m_strCode;
};

#endif

复制代码

修改后的源程序(Plane.cpp)

复制代码

#include 
#include "Plane.h"

using namespace std;

Plane::Plane(string code)
{
    m_strCode = code;
}

void Plane::printCode()
{
    cout << m_strCode << endl;
}

复制代码

修改后的头文件(*FighterPlane.h*

复制代码

#ifndef FIGHTERPLANE_H
#define FIGHTERPLANE_H

#include "Plane.h"
#include 
using namespace std;

class FighterPlane:public Plane,public Flyable
{
public:
    FighterPlane(string code);
    virtual void takeoff();
    virtual void land();
};
#endif

复制代码

main**函数不变,如下:**

复制代码

int main()
{
    FighterPlane p1("001");
    FighterPlane p2("002");
    p1.printCode();
    p2.printCode();
    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

复制代码

此时我们来看一看运行结果:

C++速成(2)_第95张图片

从运行结果可以看到跟之前一样,但是意义却不同了。此时的FighterPlane既继承了Plane这个类,也继承了Flyable这个类。这就意味着,如果有另外一个函数,要求传入的是Plane而不是FighterPlane,如下:

void flyMatch(Plane *f1, Plane *f2)
{
    f1->printCode();
    f2->printCode();    
}

而我们在main函数中:

复制代码

int main()
{
    FighterPlane p1("001");
    FighterPlane p2("002");

    flyMatch(&p1, &p2);

    system("pause");
    return 0;
}

复制代码

我们实例化的是FighterPlane,但在flyMatch这个函数中,我们仍然传入的是FighterPlane(p1和p2),这样写是合法的,因为flyMatch要求传入的类是FighterPlane的父类,所以这样写是合法的。此时,我们按F5来看一看运行的结果如何:

img

我们可以看到,打印出来的结果只有两行”001”和”002”。通过这两个实验,我们可以更进一步的体会到接口类给我们带来的好处,以及多继承给编程带来的灵活性。

RTTI—运行时类型识别

RTTI:Run-Time Type Identification。

那么RTTI如何来体现呢?这就要涉及到typeid和dynamic_cast这两个知识点了。为了更好的去理解,那么我们就通过一个例子来说明。这个例子大家已经非常熟悉了,如下:

C++速成(2)_第96张图片

首先定义一个Flyable类,在这个类当中有两个纯虚函数:takeoff(起飞)和land(降落)。我们又定义了一个鸟类,并且公有继承了Flyable类,既然public继承了Flyable,就要去实现起飞和降落这两个函数,此外,作为鸟类来说,还有一个自己特有的函数foraging(觅食)。同时,我们还定义了另外一个类Plane,其也以public方式继承了Flyable,并且也实现了起飞和降落这两个函数,此外,作为飞机类来说,其还有一个自己特有的函数Carry(运输)。

在使用的时候,我们假设有如下一个函数dosomething,它的传入参数是Flyable的一个指针,如下:

C++速成(2)_第97张图片

在这个函数当中,我们可以使用obj这个指针去调用起飞和降落这两个函数。同时,我们可以想一想,如果我们能够对传入的这个指针再做进一步的判断,如如说,我们判断出如果它是一个Bird对象指针,那么我们是不是就可以用这个指针去调用觅食这个函数呢?同理,如果我们判断出它是一个Plane对象指针,那么我们是不是也就可以用这个指针去调用运输这个函数呢?如果想要做到这样的效果,那么就要用到这节课开始提到的知识:运行时类型识别(RTTI)。

我们可以看到,当我们去实现dosomething这个函数的时候,如下:

C++速成(2)_第98张图片

我们在这调用了起飞函数,最后一行代码调用了降落函数。我们在调用完起飞这个函数之后,我们通过typeid(*obj).name()这样的方法就可以将当前的obj这个指针指向的实际的对象类型打印出来了(比如传入的是飞机,打印出来的就是Plane;如果传入的是Bird,那么打印出来的就是Bird)。当然,我们可以还可以通过if语句对类型进行比对,如果我们想要判断当前的obj是不是一个Bird类型,我们就可以通过上面的if判断语句的方法进行比对,比对完成之后,我们就可以将obj通过dynamic_cast的方式,将其转换为Bird指针。转换的时候,需要注意的是,dynamic_cast

RTTI代码实践

/* *************************************** */

/* RTTI

​ 1. Flyable类,成员函数:takeoff()和land()

​ 2. Plane类,成员函数:takeoff()、land()和carry()

​ 3. Bird类,成员函数:takeoff()、land()和foraging()

​ 4. 全局函数dosomething(Flyable *obj)

*/

/* *************************************** */

程序结构:

C++速成(2)_第99张图片

头文件(*Flyable.h*

复制代码

#ifndef FLYABLE_H
#define FLYABLE_H

//在Flyable这个类中定义两个纯虚函数takeoff()和land()
class Flyable
{
public:
    virtual void takeoff() = 0;
    virtual void land() = 0;
};

#endif

复制代码

头文件(*Bird.h*

复制代码

#ifndef Bird_H
#define Bird_H

#include "Flyable.h"
#include 
using namespace std;

class Bird:public Flyable //公有继承了Flyable
{
public:
    void foraging();//对于Bird类来说,其具有一个特有的成员函数foraging(觅食)
    virtual void takeoff(); //实现了Flyable中的虚函数takeoff和land
    virtual void land();
};

#endif

复制代码

源程序(*Bird.cpp*

复制代码

#include 
#include "Bird.h"

using namespace std;

void Bird::foraging()
{
    cout << "Bird --> foraging()" << endl;
}

void Bird::takeoff()
{
    cout << "Bird --> takeoff()" << endl;
}

void Bird::land()
{
    cout << "Bird --> land()" << endl;
}

复制代码

头文件(*Plane.h*

复制代码

#ifndef PLANE_H
#define PLANE_H

#include "Flyable.h"
#include 
using namespace std;

class Plane:public Flyable  //公有继承了Flyable
{
public:
    void carry(); //Plane具有一个特有的成员函数carry(运输)
    virtual void takeoff(); //实现了Flyable中的虚函数takeoff和land
    virtual void land();

};

#endif

复制代码

源程序(*Plane.cpp*

复制代码

#include 
#include "Plane.h"

using namespace std;

void Plane::carry()
{
    cout << "Plane --> carry()" << endl;
}

void Plane::takeoff()
{
    cout << "Plane --> takeoff()" << endl;
}

void Plane::land()
{
    cout << "Plane --> land()" << endl;
}

复制代码

主调程序(*demo.cpp*

复制代码

#include 
#include "stdlib.h"
#include "Bird.h"
#include "Plane.h"

using namespace std;
void dosomething(Flyable *obj)
{
    cout << typeid(*obj).name() << endl;  //打印传入的对象指针究竟是什么类型的对象
    obj->takeoff();
    if(typeid(*obj) == typeid(Bird)) //这里判断obj这个指针所指向的对象是不是Bird类型
    {
        Bird *bird = dynamic_cast(obj); //将obj这个指针通过dynamic_cast强制转换为Bird指针,并且将这个指针赋值给一个新的指针bird
        bird->foraging(); //通过这个bird指针来调用foraging(觅食)这个成员函数
    }
    if(typeid(*obj) == typeid(Plane)) //这里判断obj这个指针所指向的对象是不是Bird类型
    {
        Plane *plane = dynamic_cast(obj); //将obj这个指针通过dynamic_cast强制转换为Plane指针,并且将这个指针赋值给一个新的指针plane
        plane->carry(); //通过这个plane指针来调用carry(运输)这个成员函数
    }
    obj->land();
}

复制代码

我们来到主调函数main()下面,先实例化一个Bird对象b,然后通过调用dosomething函数来传入Bird这个对象b,由于dosoething这个函数传入的参数是一个独享指针,所以这里传入的应该是对象b的地址(&b),如下:

复制代码

int main()
{
    Bird b;
    dosomething(&b);

    system("pause");
    return 0;
}

复制代码

我们按一下F5,看一下运行结果:

C++速成(2)_第100张图片

通过运行的结果来比较相应的程序,看一看是如何来运行的。

首先打印出的第一行是 “class Bird”,其是通过dosomething函数中的cout语句打印出来的;接下来打印出的是“Bird –> takeoff()”,其是通过代码obj->takeoff();实现的;第三行打印出的是“Bird –> foraging()”,其运行的一定时dosomething函数中的第一个if判断语句,因为其通过bird这个指针调用了foraging(觅食)这个函数;可见,当前传入的这个obj指针所指向的对象就是一个Bird对象(如果这里我们指向的是一个Plane对象,那么显而易见就会执行第二个if判断语句,从而就会通过plane指针去调用carry(运输)这个函数);最后一行打印出的是“Bird –> land()”,其是通过代码obj->land();实现的。

这里,如果我们实例化一个Plane对象,并将对象指针传入dosomething函数,如下:

复制代码

int main()
{
    Plane p;
    dosomething(&p);

    system("pause");
    return 0;
}

复制代码

运行结果:

C++速成(2)_第101张图片

从我们的打印结果就可以反推出RTTI所做的这些工作。

接下来,再通过一些代码再来展示一下关于typeid以及dynamic_cast使用的注意事项。

我们先来看一看typeid,对于typeid来说,它能够看任何一个对象或者指针的类型(包括基本的数据成员的类型)。比如,我们定义一个变量i,就可以通过cout来看一看我们定义的i究竟是什么类型,如下:

复制代码

int main()
{
    int i = 0;
    cout << typeid(i).name() << endl;

    system("pause");
    return 0;
}

复制代码

我们按F5来看一下运行结果:

img

打印结果就是int,这就说明i这个变量的数据类型就是int类型(如果我们写成double i;),那么打印出来的就是double类型,如下:

img

那么,对于*typeid*来说,它能够打印的指针是指针本身的类型。我们再来看一看typeid打印指针和打印对象的不同之处。

首先,我们用Flyable去定义一个指针p,并且用指针p去指向Bird这样的一个对象(Flyable *p = new Bird();),指向这个对象之后,我们分别来看一看p和*p通过typeid所打印出来的结果如何,看如下代码:

复制代码

int main()
{
    Flyable *p = new Bird();
    cout << typeid(p).name() << endl;
    cout << typeid(*p).name() << endl;

    system("pause");
    return 0;
}

复制代码

按一下F5,看一看运行结果:

C++速成(2)_第102张图片

我们看到,p通过typeid打印出来的结果是“class Flyable ”,也就是说,p是一个Flyable 的数据类型,而对于*p来说,它打印出来的则是“class Bird”,也就是说*p是一个Bird对象。

接着我们再来看一看dynamic_cast有什么使用限制。

为了看到这些限制,我们需要改造一下前面的代码。

修改后的Flyable.h文件如下:

复制代码

#ifndef FLYABLE_H
#define FLYABLE_H

//在Flyable这个类中定义两个纯虚函数takeoff()和land()
class Flyable
{
public:
    void takeoff(){}
    void land(){}
};

#endif

复制代码

修改后的Bird.h文件如下:

复制代码

#ifndef Bird_H
#define Bird_H

#include "Flyable.h"
#include 
using namespace std;

class Bird:public Flyable //公有继承了Flyable
{
public:
    void foraging();//对于Bird类来说,其具有一个特有的成员函数foraging(觅食)
    void takeoff();
    void land();
};

#endif

复制代码

此时,对于Bird和Flyable来说,它们之间只是一种普通的子类和父类的关系

那么,当我们用父类的指针去指向一个子类的对象(Flyable *p = new Bird();)也是可以的。那么,我们还能不能通过dynamic_cast来进行指针的转换呢?我们一起来看一看:

复制代码

int main()
{
    Flyable *p = new Bird();
    Bird *b = dynamic_castp; //将Flyable的指针转换为Bird指针,并且将转换完的指针赋值给Bird的一个指针b

    system("pause");
    return 0;
}

复制代码

此时,我们按F7看一看编译是否通过:

C++速成(2)_第103张图片

我们看到系统提示“dynamic_cast”:“Flyable”不是多态类型,也就是说,对于Flyable来说,它要求转换的目标类型以及被转换的数据类型都应该是具有虚函数的,如果没有就会报错;当然也不能直接转对象这样的类型,比如说,将Flyable的对象p直接转换为Bird的对象b,如下:

复制代码

int main()
{
    Flyable p;
    Bird b = dynamic_castp;

    system("pause");
    return 0;
}

复制代码

我们看一看这样是否可行,按F5:

C++速成(2)_第104张图片

我们看到,这样依然会报错,报错提示依然是“dynamic_cast”:“Flyable”不是多态类型的原因,当然它也不是一个正常的数据类型,因为必须待是引用和指针才可能进行转换,其次还要加上一个条件:这个类当中必须含有虚函数。

异常处理

所谓异常:程序运行期出现的错误

异常处理:对有可能发生异常的地方做出预见性的安排

如果我们做出的安排合理,并且能够给出人性化的提示,那么使用者就不会觉得突兀,使用者就会根据我们给出的提示做相应的操作。比如说,告诉使用者网线没有插,待插上网线才可以使用,否则没法联网;再比如,告诉使用者内存太小(内存不足),必须释放掉一些内存,程序才可以正常往下运行。这样,使用者就可以非常容易的去接收所给出的提示,并且可以根据相应的提示能够使得程序顺利的继续进行下去。如果有些异常,我们没有预料到,那么就会直接报给系统,系统则是非常粗暴的,它会将我们的程序直接杀死,对于用户来说,所看到的现象就是我们的程序直接崩溃掉,而程序崩溃是我们最不愿意看到的。程序崩溃对于开发者来说简直就是一场噩梦。那么,如何来进行一场处理呢?

关键字:

Try(*尝试*)…catch(*捕获*)…:即尝试运行正常的逻辑,如果在运行正常逻辑的时候出现异常,那么就会通过catch将其捕获,捕获之后再去对出现的异常进行处理

Throw(*抛出异常*):抛出异常之后,被catch进行捕获,捕获之后再进行处理。

基本思想:主逻辑与异常处理分离

接下来我们看一看异常处理在C++中是如何工作的??

如果我们定义三个函数:f1、f2、f3,我们用f2来调用f1,用f3来调用f2。如果f1在运行过程中出现了异常,那么它就会把出现的异常向上抛,抛给它的调用者f2,如果f2可以处理,那么就可以处理完成,如果f2处理不了,那就继续向上抛,抛给它的调用者f3,那么作为更上层的f3就会捕获到异常,捕获到之后就会进行相应的处理,如果处理不了就继续向上抛,直到有函数进行处理,如果所有的调用函数都不能处理,那么最后就会抛给操作系统,操作系统就会粗暴的进行干预。下面我们就通过例子进行进一步的说明。

我们定义了一个函数fun1,在fun1当中我们写出了”throw 1”(只是简单的抛出了一个数字1,实际的程序肯定不能这样写,一定会写出正常的逻辑),在main函数中,我们通过try…catch…来进行异常的捕获,我们将fun1这个函数的调用方在try块中,如果它能够正常的运行完成,那么catch块就不会执行,如果fun1不能够正常的完成调用,而在运行的过程中不幸出现问题而抛出数字1,那么此时就必须用catch块来捕获它,捕获之后,就可以在catch语句块中作相应的处理。请大家注意,我们在fun1中所抛出的是一个数字1,其是一个int类型,所以我们可以通过catch(int)才能够捕获到,如果我们所抛出的不是1,而是0.1,那么catch中就应该用double,即catch(double)来进行捕获。如下所示:

C++速成(2)_第105张图片

下面我们来看另外一种情况,对于try…catch…来说,它可以不是一对一的,它可以是一对多的,如下:

C++速成(2)_第106张图片

对于一个try来说,其里面有相应的主逻辑,主逻辑在运行的过程中,可能在第一行代码就抛出异常,也可能在第三行代码抛出异常,也可能在第四行代码抛出异常,所抛出的异常可能是int类型,也有可能是double类型,也有可能是其他类型的,那么这个时候我们就要根据不同的异常来做相应的处理。如果以上catch块都不能捕获到相应的异常,那么最后的一个catch块可以为大家兜底,请注意写法catach( … ),这种写法就是说我可以捕获所有的异常,所有的异常统统可以处理。可是这种处理是很野蛮的,因为我们不分青红皂白,没有细致划分,就一刀切的在catch块中写相应的代码,无非就是告诉用户:“你出错了,只能关闭”。所以我们并不建议大家这样写:直接try后跟一个catch( … ),而应该在前面所有的情况下都处理不了了,万般无奈下才使用。

在刚才的例子中,我们发现一个特点,我们所抛出的异常虽然是一个值,但是我们在捕获的时候只是一种数据类型。如果我们想要捕获这个值怎么办呢?我们来看一看下面这个例子。

C++速成(2)_第107张图片

在这个例子中,我们的函数名叫做getChar,是要获取一个字符,可是传入的两个参数呢,一个是字符串,一个则是下标。我们想要根据字符串并且通过下标拿到字符串中所对应下标的字符。可是你无法保证传入进来的下标就一定比字符串短,什么意思呢?比如你传入的字符串一共就3个字符,而你传入的下标是4,显然这是不符合逻辑的,那么既然不符合逻辑,我们就应该通过throw将这种异常跑出去,告诉外面的用户:“你当前传入的下标是非法的”。我们更希望把这段字符拿到,所以我们采用如下方式就可以拿到了。

C++速成(2)_第108张图片

大家请看,此时在catch中,写的是(catch(string& aval)),这个时候如果我们传入的字符串是“hello world”,而传入的下标是100,它必然会抛出string,然后告诉我们,传入的是一个非法下标(string(“invalid index”))。那么这个时候,我们就可以通过catch块拿到相应的值,并且把它打印出来,这样就清晰的告诉用户:“你的下标传错了”。

常见异常:

  • 数组下标越界
  • 除数为0
  • 内存不足

异常处理与多态的关系

比如我们定义一个异常类,叫做Exception,这个异常类假设是一个借口类,在它其中,我们定义一些打印的方法(异常处理的方法),然后我们通过细分的子类来继承借口类(Exception),那么,当我们抛出这些子类的对象的时候,就都可以用这个父类(Exception)去捕获,如下:

C++速成(2)_第109张图片

我们来看一个例子:

C++速成(2)_第110张图片

如果我们定义一个fun1的函数,在fun1中我们进行了相应的逻辑处理,在处理的过程中,不幸的跑出了一个SizeErr的异常(其是Exception的一个子类),在fun2函数中,也写了一些逻辑,如果这些逻辑出现异常,比如抛出了一个MemmoryErr的异常(其也是Exception的一个子类),这个时候,我们怎么去捕获它们呢??无论是fun1还是fun2,我们都可以通过try…catch…来捕获它们。如下:

C++速成(2)_第111张图片

这里的关键是,catch中我们用到了父类Exception,这个时候我们就可以捕获到fun1和fun2所抛出的子类对象,并且通过子类对象去调用相应的虚函数。

异常处理代码实践

题目描述:

/* ************************** */

/* 异常处理

​ 1. 定义一个Exception类,成员函数:printException,析构函数

​ 2. 定义一个IndexException类,成员函数printException

​ Note: Exception类是异常类,作为父类

​ IndexException类是下标索引异常类,是Exception类的子类

​ 如果这两个类具有继承关系,我们需将父类的析构函数定义为虚析构函数

​ 父类和子类中的printException()都是虚函数

*/

/* ************************** */

程序框架:

C++速成(2)_第112张图片

头文件(*Exception.h*

复制代码

#ifndef EXCEPTION_H
#define EXCEPTION_H

class Exception
{
public:
    virtual void printException();
    virtual ~Exception(){} //虚析构函数
};

#endif

复制代码

源程序(*Exception.cpp*

复制代码

#include"Exception.h"
#include
using namespace std;

void Exception::printException()
{
    cout <<"Exception --> printException()"<< endl;
}

复制代码

头文件(*IndexException.h*

复制代码

#ifndef INDEXEXCEPTION_H
#define INDEXEXCEPTION_H

#include "Exception.h"
class IndexException : public Exception
{
public:
    virtual void printException();
};

#endif

复制代码

源程序(IndexException.cpp)

复制代码

#include "IndexException.h"
#include
using namespace std;

void IndexException::printException()
{
    cout <<"提示:下标越界"<< endl;
}

复制代码

主调程序(*demo.cpp*

首先演示一下对于普通的数据类型,如何使用try…catch…和throw的方式来进行异常处理。我们先来定义一个函数:test(),在这个test函数当中,我们不做其他逻辑,只通过throw将异常抛出,抛出的时候我们可以抛出一个数字10;然后,我们可以在main函数当中去调用test这个函数,调用的时候,我们用try…catch…来进行调用。如下所示:

复制代码

#include
#include"stdlib.h"
#include "IndexException.h"

using namespace std;

void test()
{
    throw 10;//抛出异常 10

}
int main()
{
    try
    {
        test();
    }
    catch(int)
    {
        cout <<"exception"<< endl;
    }
    system("pause");
    return 0;
}

复制代码

我们来看一看有什么效果,按一下F5,看运行结果如下:

img

我们看到,打印出了“exception”的字样,可见,对于抛出来的10,我们是可以捕获到的。为了能够让大家看到有捕获不到的情况,我们在这可以不抛出10,可以抛出其他的东西,比如说,我们抛出一个0.1,我们来看一看现在可不可以捕获到,按F5运行如下:

C++速成(2)_第113张图片

可见,如果我们抛出的是0.1的话,计算机就无法捕获到,那么程序就会产生崩溃的状况。如果我们仍然抛出的是0.1,而把catch后面括号中的int改成double,这样再按F5,看看运行结果如下:

img

我们看到,此时程序没有崩溃,并且还打印出了“exception”的字样。从这就能够充分说明一个问题:如果你在抛出异常的时候,你抛出了如果你能看得到,那么你就能够进行合理的处理,或者说,你有能力对其进行处理;如果抛出的异常你捕获不到,等待计算机替你处理,那么系统就会崩溃。

接下来,我们通过引用来获取抛出来的异常值,给这个异常值所取的引用名为e,如下:

复制代码

#include
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw 0.1; //抛出异常 10

}
int main()
{
    try
    {
        test();
    }
    catch(double&e)
    {
        cout << e << endl;
    }
    system("pause");
    return 0;
}

复制代码

我们来看一看,能不能把抛出的异常值0.1打印出来,我们按F5看一看运行结果:

img

我们看到,屏幕上打印出了0.1。可见,通过引用的方法式可以获取到抛出来的异常值的。如果我们在这里抛出来的不是一个数值,而是一个字符串,那就会直接打印出来一个字符串来了。在实际的项目当中,我们往往抛出来的是一个错误编号,catch到错误编号后,我们就可以根据错误编号找到相应的错误提示。

下面为大家展示一下多个Exception类之间所形成的继承关系在异常处理党章所展示出来的优越性。

首先,我们需要对程序改造一下,如下:

复制代码

#include
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw IndexException();

}
int main()
{
    try
    {
        test();
    }
    catch(IndexException &e)
    { 
        e.printException();
    }
    system("pause");
    return 0;
}

复制代码

我们按F5看一下运行结果,如下:

img

通过运行结果,我们可以看到,我们捕获到了IndexException,并且打印出了下标越界的提示来。这样,用户就能知道自己犯了什么错误,可以通过程序的输入来弥补这样的错误。如果我们在这捕获的不是IndexException,而是Exception,能不能捕获到IndexException呢?我们来看一看,

复制代码

#include
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw IndexException();

}
int main()
{
    try
    {
        test();
    }
    catch(Exception&e)
    { 
        e.printException();
    }
    system("pause");
    return 0;
}

复制代码

按一下F5,看一看运行结果,如下:

img

通过打印结果我们可以看到,通过Exception来捕获IndexException也是可行的。捕获到之后,打印出来的也是IndexException中定义的printException函数。如果我们在catch这部分中捕获的不是Exception,或者说,我们也不知道test函数会抛出什么样的异常,这就要catch后面括号中携程“…”的形式了,这样是否能够捕获到IndexException的异常呢?其实是可以捕获到的,但是我们没有办法调用异常中的成员函数了,因为这样的捕获实在是太笼统,我们没有办法获得更多的线索,通过这些线索来给用户更为精准的提示。我们来试一试,在这里我们只能打印“Exception”的字样,因为没有其他线索了,如下:

复制代码

#include
#include"stdlib.h"
#include"IndexException.h"

using namespace std;

void test()
{
    throw IndexException();

}
int main()
{
    try
    {
        test();
    }
    catch(...)
    { 
        cout <<"Exception"<< endl;
    }
    system("pause");
    return 0;
}

复制代码

按F5来看一看运行结果:

C++速成(2)_第114张图片

大家可以看到,通过catch(…)也是可以捕获到异常的,只不过这时候捕获到的异常很无力,给出的提示很笼统。

6.5 练习题

你可能感兴趣的:(C++速成(2))