一 多态概述

C++中的多态分为静态多态和动态多态。静态多态是函数重载,在编译阶段就能确定调用哪个函数。动态多态是由继承产生的,指同一个属性或行为在基类及其各派生类中具有不同的语义,不同的对象根据所接收的消息做出不同的响应,这种现象称为动态多态。例如,动物都能发出叫声,但不同的动物叫声不同,猫会“喵喵”、狗会“汪汪”,这就是多态的体现。面向对象程序设计中所说的多态通常指的是动态多态。

在C++中,“消息”就是对类的成员函数的调用,不同的行为代表函数的不同实现方式,因此,多态的本质是函数的多种实现形态。

多态的实现需要满足3个条件。

(1)基类声明虚函数。

(2)派生类重写基类的虚函数。

(3)将基类指针指向派生类对象,通过基类指针访问虚函数。

二 虚函数实现多态

如果基类与派生类中有同名成员函数,根据类型兼容规则,当使用基类指针或基类引用操作派生类对象时,只能调用基类的同名函数。如果想要使用基类指针或基类引用调用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。本节将针对虚函数实现多态进行详细讲解。

虚函数

虚函数的声明方式是在成员函数的返回值类型前添加virtual关键字,格式如下所示:

class 类名
{
权限控制符: virtual 函数返回值类型 函数名(参数列表); … //其他成员
};

声明虚函数时,有以下3点需要注意。

(1)构造函数不能声明为虚函数,但析构函数可以声明为虚函数。

(2)虚函数不能是静态成员函数。

(3)友元函数不能声明为虚函数,但虚函数可以作为另一个类的友元函数。

虚函数只能是类的成员函数,不能将类外的普通函数声明为虚函数,即virtual关键字只能修饰类中的成员函数,不能修饰类外的普通函数。因为虚函数的作用是让派生类对虚函数重新定义,它只能存在于类的继承层次结构中。

若类中声明了虚函数,并且派生类重新定义了虚函数,当使用基类指针或基类引用操作派生类对象调用函数时,系统会自动调用派生类中的虚函数代替基类虚函数。 下面通过案例演示虚函数实现多态的机制:

类的关系图:

实例代码:

#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>using namespace std;class Animal
{
public:virtual void speak(){cout << "animal speak ..." << endl;}
};
class Cat:public Animal
{
public:void speak(){cout << "Cat speak miaomiao" << endl;}
};
class Dog:public Animal
{
public:void speak(){cout << "Dog speak wangwang" << endl;}
};int main()
{Animal *pc = new Cat();pc->speak();Animal *pd = new Dog();pd->speak();return 0;
}

执行结果:

Cat speak miaomiao
Dog speak wangwang

pc指针调用的是Cat类的speak()函数,输出了miaomiao的叫声;pd调用的是Dog类的speak()函数,输出了wanwang的叫声。基类指针调用的永远都是派生类重写的虚函数,不同的派生类对象都有自己的表现形态。

这样效果不是很明显,我们将Animal类的定义修改为下面这样:就是去掉speak函数前面的virtual

class Animal
{
public:void speak(){cout << "animal speak ..." << endl;}
};

重新编译执行:发现,此时的pc指针和pd指针调用的都是Animal的speak函数。通过对比,才能体现出virtual动态多态的好处。

animal speak ...
animal speak ...

需要注意的是,派生类对基类虚函数重写时,必须与基类中虚函数的原型完全一致,派生类中重写的虚函数前是否添加virtual,均被视为虚函数。 也就是说Cat的speak函数前面加不加virtual。它都是virtual函数。

三 override和final(C++11新标准)

override和final关键字是C++11新标准提供的两个关键字,在类的继承中有着广泛应用,下面对这两个关键字进行简单介绍。

1.override

override关键字的作用是检查派生类中函数是否在重写基类虚函数,如果不是重写基类虚函数,编译器就会报错。示例代码如下:

class Base     //基类Base
{
public: virtual void func(); void show();
};
class Derive:public Base  //派生类Derive,公有继承Base类
{
public: void func() override;   //可通过编译 void show() override;   //不能通过编译
}; 

在上述代码中,派生类Derive中func()函数后面添加override关键字可以通过编译,而show()函数后面添加override关键字,编译器会报错,这是因为show()函数并不是重写基类虚函数。利用override关键字可以判断派生类是否准确地对基类虚函数进行重写,防止出现因书写错误而导致的基类虚函数重写失败。另外,在实际开发中,C++中的虚函数大多跨层继承,直接基类没有声明虚函数,但很可能会从“祖先”基类间接继承。如果类的继承层次较多或者类的定义比较复杂,那么在定义派生类时就会出现信息分散、难以阅读的问题,重写基类虚函数时,往往难以确定重写是否正确。此时,可以通过override关键字进行检查。

2.final

final关键字有两种用法:修饰类、修饰虚函数。当使用final关键字修饰类时,表示该类不可以被继承。示例代码如下:

class Base final    //final修饰类,Base类不能被继承
{
public: //...
};
class Derive :public Base     //编译错误
{
public: //...
}; 

在上述代码中,Base类被final关键字修饰,就不能作为基类派生新类,因此当Derive类继承Base类时,编译器会报错。除了修饰类,final关键字还可以修饰虚函数,当使用final关键字修饰虚函数时,虚函数不能在派生类中重写。示例代码如下:

class Base
{
public: virtual void func() final;
};
class Derive:public Base
{
public: void func();      //不能通过编译
}; 

在上述代码中,Derive类公有继承Base类,在Derive类中重写基类被final修饰的虚函数func()时,编译器会报“无法重写‘final’函数Base::func()”的错误。

四 虚函数实现多态的机制

在编写程序时,我们需要根据函数名、函数返回值类型、函数参数等信息正确调用函数,这个匹配过程通常称为绑定。C++提供了两种函数绑定机制:静态绑定和动态绑定。静态绑定也称为静态联编、早绑定,它是指编译器在编译时期就能确定要调用的函数。动态绑定也称为动态联编、迟绑定,它是指编译器在运行时期才能确定要调用的函数。

虚函数就是通过动态绑定实现多态的,当编译器在编译过程中遇到virtual关键字时,它不会对函数调用进行绑定,而是为包含虚函数的类建立一张虚函数表Vtable。在虚函数表中,编译器按照虚函数的声明顺序依次保存虚函数地址。同时,编译器会在类中添加一个隐藏的虚函数指针VPTR,指向虚函数表。在创建对象时,将虚函数指针VPTR放置在对象的起始位置,为其分配空间,并调用构造函数将其初始化为虚函数表地址。需要注意的是,虚函数表不占用对象空间。

派生类继承基类时,也继承了基类的虚函数指针。当创建派生类对象时,派生类对象中的虚函数指针指向自己的虚函数表。在派生类的虚函数表中,派生类虚函数会覆盖基类的同名虚函数。当通过基类指针或基类引用操作派生类对象时,以操作的对象内存为准,从对象中获取虚函数指针,通过虚函数指针找到虚函数表,调用对应的虚函数。

下面结合代码分析虚函数实现多态的机制,示例代码如下:

class Base1       //定义基类Base1
{
public: virtual void func();    //声明虚函数func() virtual void base1();    //声明虚函数base1()virtual void show1();    //声明虚函数show1()
};
class Base2       //定义基类Base2
{
public: virtual void func();    //声明虚函数func() virtual void base2();    //声明虚函数base2()virtual void show2();    //声明虚函数show2()
};
//定义Derive类,公有继承Base1和Base2
class Derive :public Base1, public Base2
{
public: virtual void func();    //声明虚函数func() virtual void base1();    //声明虚函数base1() virtual void show2();    //声明虚函数show2()
}; 

在上述代码中,基类Base1有func()、base1()和show1()三个虚函数;基类Base2有func()、base2()和show2()三个虚函数;派生类Derive公有继承Base1和Base2,Derive类声明了func()、base1()和show2()三个虚函数。Derive类与Base1类和Base2类的继承关系如图5-2所示。

图5-2 Derive类与Base1类和Base2类的继承关系

在编译时,编译器发现Base1类与Base2类有虚函数,就为两个类创建各自的虚函数表,并在两个类中添加虚函数指针。如果创建Base1类对象(如base1)和Base2类对象(如base2),则对象中的虚函数指针会被初始化为虚函数表的地址,即虚函数指针指向虚函数表。对象base1与对象base2的内存逻辑示意图如图5-3所示。

图5-3 对象base1与对象base2的内存逻辑示意图

Derive类继承自Base1类与Base2类,也会继承两个基类的虚函数指针。Derive类的虚函数func()、base1()和show2()会覆盖基类的同名虚函数。如果创建Derive类对象(如derive),则对象derive的内存逻辑示意图如图5-4所示。

图5-4对象derive的内存逻辑示意图

通过基类Base1、基类Base2的指针或引用操作Derive类对象,在程序运行时,编译器从Derive类对象内存中获取虚函数指针,通过指针找到虚函数表,调用相应的虚函数。不同的类,其函数实现都不一样,在调用时就实现了多态。

五 虚析构函数

在C++中不能声明虚构造函数,因为构造函数执行时,对象还没有创建,不能按照虚函数方式调用。但是,在C++中可以声明虚析构函数,虚析构函数的声明是在“~”符号前添加virtual关键字,格式如下所示:

virtual ~析构函数(); 

在基类中声明虚析构函数之后,基类的所有派生类的析构函数都自动成为虚析构函数。

在基类声明虚析构函数之后,使用基类指针或引用操作派生类对象,在析构派生类对象时,编译器会先调用派生类的析构函数释放派生类对象资源,然后再调用基类析构函数。如果基类没有声明虚析构函数,在析构派生类对象时,编译器只会调用基类析构函数,不会调用派生类析构函数,导致派生类对象申请的资源不能正确释放。下面通过案例演示虚析构函数的定义与调用

1 首先看看没有声明虚析构函数的情况:

实例代码:

#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>using namespace std;class Animal
{
public:~Animal(){cout << "Animal delete" << endl;}
};
class Cat:public Animal
{
public:char *str;Cat(){str = new char[10];memcpy(str,"hello",sizeof("hello"));cout << "str = " << str << endl;}~Cat(){delete str;cout << "Cat delete" << endl;}
};int main()
{Animal *p = new Cat;delete p;return 0;
}

执行结果:如下所示,只调用了父类中的析构函数,由于没有调用子类Cat中的析构函数,导致子类Cat的成员str占用的堆内存得不到释放,导致内存泄漏发生。

str = hello
Animal delete

2 声明虚析构函数的情况:

#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>using namespace std;class Animal
{
public:virtual ~Animal(){cout << "Animal delete" << endl;}
};
class Cat:public Animal
{
public:char *str;Cat(){str = new char[10];memcpy(str,"hello",sizeof("hello"));cout << "str = " << str << endl;}~Cat(){delete str;cout << "Cat delete" << endl;}
};int main()
{Animal *p = new Cat;delete p;return 0;
}

执行结果:此时,子类Cat的析构函数得到调用执行,str成员占用的堆内存被释放了,避免了内存泄漏。

str = hello
Cat delete
Animal delete

虚析构函数的定义与用法很简单,但在C++程序中却是非常重要的一个编程技巧。在编写C++程序时,最好把基类的析构函数声明为虚析构函数,即使基类不需要析构函数,也要显式定义一个函数体为空的虚析构函数,这样所有派生类的析构函数都会自动成为虚析构函数。如果程序中通过基类指针释放派生类对象,编译器能够调用派生类的析构函数完成派生类对象的释放。

六 纯虚函数和抽象类

有时候在基类中声明函数并不是基类本身的需要,而是考虑到派生类的需求,在基类中声明一个函数,函数的具体实现由派生类根据本类的需求定义。例如,动物都有叫声,但不同的动物叫声不同,因此基类(动物类)并不需要实现描述动物叫声的函数,只需要声明即可,函数的具体实现在各派生类中完成。在基类中,这样的函数可以声明为纯虚函数。

纯虚函数也通过virtual关键字声明,但是纯虚函数没有函数体。纯虚函数在声明时,需要在后面加上“=0”,格式如下所示:

virtual 函数返回值类型 函数名(参数列表) =  0;

上述格式中,纯虚函数后面“=0”并不是函数的返回值为0,它只是告诉编译器这是一个纯虚函数,在派生类中会完成具体的实现。

纯虚函数的作用是在基类中为派生类保留一个接口,方便派生类根据需要完成定义,实现多态。派生类都应该实现基类的纯虚函数,如果派生类没有实现基类的纯虚函数,则该函数在派生类中仍然是纯虚函数。

如果一个类中包含纯虚函数,这样的类称为抽象类。抽象类的作用主要是通过它为一个类群建立一个公共接口(纯虚函数),使它们能够更有效地发挥多态性。抽象类声明了公共接口,而接口的完整实现由派生类定义。

抽象类只能作为基类派生新类,不能创建抽象类的对象,但可以定义抽象类的指针或引用,通过指针或引用操作派生类对象。抽象类可以有多个纯虚函数,如果派生类需要实例化对象,则在派生类中需要全部实现基类的纯虚函数。如果派生类没有全部实现基类的纯虚函数,未实现的纯虚函数在派生类中仍然是纯虚函数,则派生类也是抽象类。 下面通过案例演示纯虚函数和抽象类的应用:

实例代码:

#include <iostream>
#include <iomanip>
#include <string.h>
#include <stdio.h>using namespace std;class Animal
{
public:virtual void speak() = 0;virtual ~Animal(){cout << "Animal delete" << endl;}
};
class Cat:public Animal
{
public:char *str;Cat(){cout << "Cat init" << endl;}void speak() override;~Cat(){delete str;cout << "Cat delete" << endl;}
};
void Cat::speak(){cout << "Cat miaomiao" << endl;
}class Dog:public Animal
{
public:Dog(){cout << "Dog init" << endl;}void speak() override;~Dog(){cout << "Dog delete" << endl;}
};
void Dog::speak(){cout << "Dog wangwang" << endl;
}int main()
{Animal *pc = new Cat;pc->speak();delete pc;Animal *pd = new Dog;pd->speak();delete pd;return 0;
}

执行结果:

Cat init
Cat miaomiao
Cat delete
Animal delete
Dog init
Dog wangwang
Dog delete
Animal delete

在上中,Animal类是抽象类,如果创建Anim al类对象,编译器会报错,例如在main()函数中添加如下代码:

Animal a;

报错:

error: variable type 'Animal' is an abstract class,unimplemented pure virtual method 'speak' in 'Animal'

如果Animal类的某个派生类没有全部实现纯虚函数,则派生类也是抽象类,不能创建该派生类的对象。

小结

C++学习12:C++多态、虚函数、虚析构函数、纯虚函数、抽象类相关推荐

  1. 析构函数可以为纯虚函数吗?纯虚函数可以有函数体吗?纯虚函数需要函数体吗?

    先回答标题中中的几个问题: 析构函数可以为纯虚函数吗? yes. 纯虚函数可以有函数体吗? yes. 纯虚函数需要函数体吗? 一般来讲,如果析构函数是纯虚函数,那么析构函数必须要有函数体,如果是其它函 ...

  2. 虚函数,虚基类 与纯虚函数 二

    虚函数    还是先看代码 class A { public:     void funPrint(){cout<<"funPrint of class A"<& ...

  3. c++多态--2(计算器,纯虚函数和抽象类)

    为什么要用多态 早期方法不利于扩展 开闭原则 开闭原则 对扩展开放 对修改关闭 利用多态实现-利于后期扩展,结构性非常好,可读性高,效率稍微低,发生多态内部结构复杂 多态成立的条件 又继承 子类重写父 ...

  4. C++编程进阶2(编译器在类内默认生成的函数讨论以及纯虚析构函数)

    三.编译器默认提供的类内函数讨论 1.当写下一个空类时,编译器会在必要的时候默认提供四个函数:构造.拷贝构造.operator=和析构函数,而且都是public的 class Empty{ }; 上述 ...

  5. C++ 虚函数成员和纯虚函数

    一般虚函数成员的声明语法是: virtual 函数类型 函数名(形参表); 在类的定义使用virtual关键字来限定成员函数,虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候出 ...

  6. 虚函数,虚基类 与纯虚函数 一

    正文 虚基类    在说明其作用前先看一段代码 class A { public:     int iValue; }; class B:public A { public:     void bPr ...

  7. 初入c++(六)虚函数实现多态,虚析构函数,虚函数表和多态实现机制,纯虚函数。

    1.c++多态的概念以及用途. 1.1虚函数实现多态 通过基类指针只能够访问派生类的成员变量,不能够访问派生类的成员函数. 解决问题的办法:使用虚函数(virtual function),只需要在函数 ...

  8. C++面试题:多态,虚函数,纯虚函数

    多态:是对于不同对象接收相同消息时产生不同的动作.C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现: 在程序编译时多态性体现在函数和运算符的重载上: 开课吧广场C ...

  9. 【C++学习笔记】 纯虚函数详解,绝不纯属虚构!

    首先:强调一个概念 定义一个函数为虚函数,不代表函数为不被实现的函数. 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数. 定义一个函数为纯虚函数,才代表函数没有被实现. 定义纯虚函数是为了实 ...

  10. C++多态,虚函数,纯虚函数

    多态:是对于不同对象接收相同消息时产生不同的动作. C++的多态性具体体现在运行和编译两个方面: 在程序运行时的多态性通过继承和虚函数来体现: 在程序编译时多态性体现在函数和运算符的重载上: 虚函数: ...

最新文章

  1. postgres数据库最大连接数
  2. python简单代码hello-[代码全屏查看]-python初学之helloworld
  3. Web 设计中的 5 个最具争议性的话题
  4. linux -- supervitord怎么杀出主进程派生出来的子进程。
  5. 201205阶段二FFmpeg编码
  6. Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)
  7. 分享10个2012年最新发布的jQuery插件
  8. 用户生命周期常用指标_生命周期管理工具如何使用指标
  9. oracle 删除已备份归档,通过RMAN联机全库备份,包括控制文件,归档日志文件,备份成功后,删除已备份的归档日志。...
  10. vue ---- 指令综合案例
  11. 01背包问题-一维数组实现原理
  12. _GNUC__宏函数
  13. 资源 | 邓力、刘洋等合著的这本NLP经典书籍之情感分析中文版
  14. 手机QQ空间装逼代码收集
  15. C++ 鼠标乱动整人代码
  16. 关于加速器加速后进入游戏显示WiFi功能丢失的解决方法(maybe)
  17. Windows 窗口层次关系及窗口层次说明
  18. shopnc 设置mysql_shopnc 手机网站配置
  19. Pocket PC模拟器设置上网
  20. DEBUG指示灯详细说明

热门文章

  1. 概率统计——基础运算法则
  2. 计算机课程设置图片,《计算机图像处理》课程教学大纲.doc
  3. lichee linux nfs,lichee开发板搭建nfs
  4. IDEA出现乱码的各种解决方法
  5. JUnit5学习之四:按条件执行
  6. rdkit 化学指纹(fingerprint)和相似性
  7. ai人工智能培训老师讲师NLP自然语言处理讲师叶梓:人工智能之最新NLP自然语言处理技术与实战-12
  8. 如何评价一个算法的好坏
  9. 函数和代码复用之实例解析:软文的诗词风
  10. 2022最新考研资料