纯虚函数与抽象类

1. 多态

在面向对象语言中,接口的多种不同实现方式即为多态。多态是指,用父类的指针指向子类的实例(对象),然后通过父类的指针调用实际子类的成员函数

多态性就是允许将子类类型的指针赋值给父类类型的指针,多态是通过虚函数实现的

多态可以让父类的指针有“多种形态”,这是一种泛型技术。(所谓泛型技术,就是试图使用不变的代码来实现可变的算法)。

2. 虚函数

2.1虚函数定义

在基类的类定义中,定义虚函数的一般形式:

Virtual 函数返回值类型 虚函数名(形参表)
{函数体}

虚函数必须是类的非静态成员函数(且非构造函数),其访问权限是public。

2.2 虚函数的作用

虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数进行重新定义(形式同上)。在派生类中定义的函数应与虚函数具有相同的形参个数和形参类型(覆盖),以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

虚函数可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,基类虚成员函数调用基类指针,则会调用其真正指向的对象的成员函数,而不是基类中定义的成员函数(只要派生类改写了该成员函数)。若不是虚函数,则不管基类指针指向哪个派生类对象,调用时都会调用基类中定义的那个函数。

2.3 实现动态联编需要三个条件:

1)必须把需要动态联编的行为定义为类的公共属性的虚函数
2)类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来;
3)必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数。

2.4 定义虚函数的限制

1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数

2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。

3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名同返回值同参数个数同参数类型函数。

2.5 

1)为什么类的静态成员函数不能为虚函数: 

如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(在内存中只有一份拷贝,通过类名或对象引用访问静态成员)本身就是相矛盾的。

2)为什么构造函数不能为虚函数:

因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保vtbl的构建成功呢?

3)虚析构函数

C++开发的时候,用来做基类的类的析构函数一般都是虚函数。当基类中有虚函数的时候,析构函数也要定义为虚析构函数。如果不定义虚析构函数,当删除一个指向派生类对象的指针时,会调用基类的析构函数,派生类的析构函数未被调用,造成内存泄露。
虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这样,当删除指向派生类的指针时,就会首先调用派生类的析构函数,不会有内存泄露的问题了。
一般情况下,如果类中没有虚函数,就不用去声明虚析构函数。当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

2.6虚函数的实现——虚函数表

虚函数是通过一张虚函数表来实现的,简称V-Table。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享,类的每个虚函数成员占据虚函数表中的一行。
在这个表中,主要是一个类的虚函数的地址表。这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。在有虚函数的类的实例中,分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就指明了实际所应该调用的函数。

3. 纯虚函数

许多情况下,在基类中不能对虚函数给出有意义的实现,则把它声明为纯虚函数,它的实现留给该基类的派生类去做

纯虚函数的声明格式:virtual <函数返回类型说明符> <函数名> ( <参数表> )=0;

纯虚函数的作用是为派生类提供一个一致的接口

4.抽象类(abstract class)

抽象类是指含有纯虚函数的类(至少有一个纯虚函数),该类不能创建对象(抽象类不能实例化),但是可以声明指针和引用,用于基础类的接口声明和运行时的多态。

抽象类中,既可以有抽象方法,也可以有具体方法或者叫非抽象方法。抽象类中,既可以全是抽象方法,也可以全是非抽象方法。一个继承于抽象类的子类,只有实现了父类所有的抽象方法才能够是非抽象类。

5.接口

接口是一个概念。它在C++中用抽象类来实现,在C#和Java中用interface来实现。

接口是专门被继承的。接口存在的意义也是被继承。和C++里的抽象类里的纯虚函数是相同的。不能被实例化。
定义接口的关键字是interface,例如:   
public interface MyInterface{   
public void add(int x,int y);   
public void volume(int x,int y,int z);   
}  

继承接口的关键字是implements,相当于继承类的extends。需要注意的是,当继承一个接口时,接口里的所有函数必须全部被覆盖。
当想继承多个类时,开发程序不允许,报错。这样就要用到接口。因为接口允许多重继承,而类不允许(C++中可以多重继承)。所以就要用到接口。
 

6.虚基类

在派生类继承基类时,加上一个virtual关键词则为虚拟基类继承,如:
class derive : virtual public base
{
};

虚基类是相对于它的派生类而言的,它本身可以是一个普通的类。只有它的派生类虚继承它的时候,它才称作虚基类,如果没有虚继承的话,就称为基类。比如类B虚继承于类A,那类A就称作类B的虚基类,如果没有虚继承,那类B就只是类A的基类。
虚继承主要用于一个类继承多个类的情况,避免重复继承同一个类两次或多次。
例如 由类A派生类B和类C,类D又同时继承类B和类C,这时候类D就要用虚继承的方式避免重复继承类A两次。

7. 抽象类VS接口

一个类可以有多个接口,只能继承一个父类??

抽象类可以有构造方法,接口中不能有构造方法;

抽象类中可以有普通成员变量,接口中没有普通成员变量;

接口里边全部方法都必须是abstract的,抽象类的可以有实现了的方法;

抽象类中的抽象方法的访问类型可以是public,protected,但接口中的抽象方法只能是public类型的,并且默认即为public abstract类型;

抽象类中可以包含静态方法,接口中不能包含静态方法;

抽象类和接口中都可以包含静态成员变量,抽象类中的静态成员变量的访问类型可以任意,但接口中定义的变量只能是public static final类型,并且默认即为public static final类型。

8. 虚函数VS纯虚函数

虚函数
引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。
纯虚函数
引入原因:
1)同“虚函数”;
2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
纯虚函数就是基类只定义了函数体,没有实现过程。
纯虚函数相当于接口,不能直接实例话,需要派生类来实现函数定义;
有的人可能在想,定义这些有什么用?
比如你想描述一些事物的属性给别人,而自己不想去实现,就可以定义为纯虚函数。说的再透彻一些,比如盖楼房,你是老板,你给建筑公司描述清楚你的楼房的特性,多少层,楼顶要有个花园什么的,建筑公司就可以按照你的方法去实现了,如果你不说清楚这些,可能建筑公司不太了解你需要楼房的特性。用纯需函数就可以很好的分工合作了。

二者的区别:

1> 类里声明为虚函数的话,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被重载,这样的话,编译器就可以使用后期绑定来达到多态了;
纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。

2>虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现;

3>虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然我们也可以完成自己的实现。纯虚函数的类用于“介面继承”,主要用于通信协议方面。关注的是接口的统一性,实现由子类完成。一般来说,介面类中只有纯虚函数的;

4>带纯虚函数的类叫抽象类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。

一般而言纯虚函数的函数体是缺省的,但是也可以给出纯虚函数的函数体(此时纯虚函数变为虚函数),这一点经常被人们忽视,调用纯虚函数的方法为baseclass::virtual function.

出处:《C++Primer》中文版第五版541页第22行。

我们可以为纯虚函数提供定义,不过函数体必须定义在类的外部。若定义在类的内部,会出现错误:pure-specifier on function-definition

 
  1. class Dummy

  2. {

  3. //error:pure_specifier on function-definition.

  4. virtual void process()=0{};

  5. };

  6.  
  7.  
  8. class Dummy

  9. {

  10. virtual void process()=0;

  11. }

  12. void Dummy::process()

  13. {}

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

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

纯虚函数的实现原理

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

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

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

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

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

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

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

题目描述:

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

/* 纯虚函数和抽象类

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

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

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

*/

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

程序框架:

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

头文件(Person.h)

复制代码

#ifndef PERSON_H
#define PERSON_H

#include <string>
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 <stdlib.h>

#include "Person.h"

using namespace std;

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

    system("pause");
    return 0;
}

复制代码

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

我们可以看到错误提示:”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 <iostream>
#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 <iostream>
#include <stdlib.h>

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

using namespace std;

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

    system("pause");
    return 0;
}

复制代码

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

我们可以看到错误提示:”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 <iostream>
#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 <iostream>
#include <stdlib.h>

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

using namespace std;

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

    system("pause");
    return 0;
}

复制代码

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

从结果看到,此时计算机编译通过。这样,也就从另一个角度说明了,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 <iostream>
#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 <iostream>
using namespace std;
Dustman::Dustman(string name, int age):Worker(name,age)
{

}

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

复制代码

主调程序(demo.cpp)

复制代码

#include <stdlib.h>
#include "Dustman.h"
#include "Person.h"
#include "Worker.h"

using namespace std;

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

    system("pause");
    return 0;
}

复制代码

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

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

猜你喜欢

转载自blog.csdn.net/Scythe666/article/details/81301618