C++多态和虚函数(非常详细)
学习本文前,相信大家已经掌握了继承和封装的特征,下面我们将学习面向对象三大特征的最后一个特征——多态。本文主要针对虚函数实现多态进行讲解。
例如,动物都能发出叫声,但不同的动物叫声不同,猫会“喵喵”、狗会“汪汪”,这就是多态的体现。面向对象程序设计中所说的多态通常指的是动态多态。
在 C++ 中,“消息”就是对类的成员函数的调用,不同的行为代表函数的不同实现方式,因此,多态的本质是函数的多种实现形态。
多态的实现需要满足 3 个条件。
如果想要使用基类指针或基类引用调用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。
虚函数只能是类的成员函数,不能将类外的普通函数声明为虚函数,即
若类中声明了虚函数,并且派生类重新定义了虚函数,当使用基类指针或基类引用操作派生类对象调用函数时,系统会自动调用派生类中的虚函数代替基类虚函数。
【示例1】下面通过案例演示虚函数实现多态的机制,C++ 代码如下:
基类指针调用的永远都是派生类重写的虚函数,不同的派生类对象都有自己的表现形态。
需要注意的是,派生类对基类虚函数重写时,必须与基类中虚函数的原型完全一致,派生类中重写的虚函数前是否添加virtual,均被视为虚函数。
利用 override 关键字可以判断派生类是否准确地对基类虚函数进行重写,防止出现因书写错误而导致的基类虚函数重写失败。
另外,在实际开发中,C++ 中的虚函数大多跨层继承,直接基类没有声明虚函数,但很可能会从“祖先”基类间接继承。
如果类的继承层次较多或者类的定义比较复杂,那么在定义派生类时就会出现信息分散、难以阅读的问题,重写基类虚函数时,往往难以确定重写是否正确。此时,可以通过 override 关键字进行检查。
当使用 final 关键字修饰类时,表示该类不可以被继承。示例代码如下:
除了修饰类,final 关键字还可以修饰虚函数,当使用 final 关键字修饰虚函数时,虚函数不能在派生类中重写。示例代码如下:
C++ 提供了两种函数绑定机制:静态绑定和动态绑定。
虚函数就是通过动态绑定实现多态的,当编译器在编译过程中遇到 virtual 关键字时,它不会对函数调用进行绑定,而是为包含虚函数的类建立一张虚函数表Vtable。
在虚函数表中,编译器按照虚函数的声明顺序依次保存虚函数地址。同时,编译器会在类中添加一个隐藏的虚函数指针 VPTR,指向虚函数表。
在创建对象时,将虚函数指针 VPTR 放置在对象的起始位置,为其分配空间,并调用构造函数将其初始化为虚函数表地址。注意,虚函数表不占用对象空间。
派生类继承基类时,也继承了基类的虚函数指针。当创建派生类对象时,派生类对象中的虚函数指针指向自己的虚函数表。在派生类的虚函数表中,派生类虚函数会覆盖基类的同名虚函数。
当通过基类指针或基类引用操作派生类对象时,以操作的对象内存为准,从对象中获取虚函数指针,通过虚函数指针找到虚函数表,调用对应的虚函数。
【示例2】下面结合代码分析虚函数实现多态的机制,示例代码如下:
Derive 类与 Base1 类和 Base2 类的继承关系如图 1 所示。

图1 Derive类与Base1类和Base2类的继承关系
在编译时,编译器发现 Base1 类与 Base2 类有虚函数,就为两个类创建各自的虚函数表,并在两个类中添加虚函数指针。
如果创建 Base1 类对象(如 base1)和 Base2类对象(如 base2),则对象中的虚函数指针会被初始化为虚函数表的地址,即虚函数指针指向虚函数表。如下所示:

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

图3 对象derive的内存逻辑示意图
通过基类 Base1、基类 Base2 的指针或引用操作 Derive 类对象,在程序运行时,编译器从 Derive 类对象内存中获取虚函数指针,通过指针找到虚函数表,调用相应的虚函数。不同的类,其函数实现都不一样,在调用时就实现了多态。
但是,在 C++ 中可以声明虚析构函数,虚析构函数的声明是在“~”符号前添加 virtual 关键字,格式如下所示:
在基类声明虚析构函数之后,使用基类指针或引用操作派生类对象,在析构派生类对象时,编译器会先调用派生类的析构函数释放派生类对象资源,然后再调用基类析构函数。
如果基类没有声明虚析构函数,在析构派生类对象时,编译器只会调用基类析构函数,不会调用派生类析构函数,导致派生类对象申请的资源不能正确释放。
【示例3】下面通过案例演示虚析构函数的定义与调用,C++ 代码如下:
由运行结果可知,程序先调用了 Derive 类析构函数,然后又调用了 Base 类析构函数。
虚析构函数的定义与用法很简单,但在 C++ 程序中却是非常重要的一个编程技巧。
在编写 C++ 程序时,最好把基类的析构函数声明为虚析构函数,即使基类不需要析构函数,也要显式定义一个函数体为空的虚析构函数,这样所有派生类的析构函数都会自动成为虚析构函数。
如果程序中通过基类指针释放派生类对象,编译器能够调用派生类的析构函数完成派生类对象的释放。
声明:《C++系列教程》为本站“54笨鸟”官方原创,由国家机构和地方版权局所签发的权威证书所保护。
多态概述
C++ 中的多态分为静态多态和动态多态。- 静态多态是函数重载,在编译阶段就能确定调用哪个函数。
- 动态多态是由继承产生的,指同一个属性或行为在基类及其各派生类中具有不同的语义,不同的对象根据所接收的消息做出不同的响应,这种现象称为动态多态。
例如,动物都能发出叫声,但不同的动物叫声不同,猫会“喵喵”、狗会“汪汪”,这就是多态的体现。面向对象程序设计中所说的多态通常指的是动态多态。
在 C++ 中,“消息”就是对类的成员函数的调用,不同的行为代表函数的不同实现方式,因此,多态的本质是函数的多种实现形态。
多态的实现需要满足 3 个条件。
- 基类声明虚函数。
- 派生类重写基类的虚函数。
- 将基类指针指向派生类对象,通过基类指针访问虚函数。
虚函数实现多态
如果基类与派生类中有同名成员函数,根据类型兼容规则,当使用基类指针或基类引用操作派生类对象时,只能调用基类的同名函数。如果想要使用基类指针或基类引用调用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。
虚函数
虚函数的声明方式是在成员函数的返回值类型前添加 virtual 关键字,格式如下所示:
class 类名
{
权限控制符:
virtual 函数返回值类型 函数名(参数列表);
… //其他成员
};
- 构造函数不能声明为虚函数,但析构函数可以声明为虚函数。
- 虚函数不能是静态成员函数。
- 友元函数不能声明为虚函数,但虚函数可以作为另一个类的友元函数。
虚函数只能是类的成员函数,不能将类外的普通函数声明为虚函数,即
virtual
关键字只能修饰类中的成员函数,不能修饰类外的普通函数。因为虚函数的作用是让派生类对虚函数重新定义,它只能存在于类的继承层次结构中。若类中声明了虚函数,并且派生类重新定义了虚函数,当使用基类指针或基类引用操作派生类对象调用函数时,系统会自动调用派生类中的虚函数代替基类虚函数。
【示例1】下面通过案例演示虚函数实现多态的机制,C++ 代码如下:
#include<iostream> using namespace std; class Animal //动物类Animal { public: virtual void speak(); //声明虚函数speak() }; void Animal::speak() //类外实现虚函数 { cout<<"动物叫声"<<endl; } class Cat:public Animal //猫类Cat,公有继承Animal类 { public: virtual void speak(); //声明虚函数speak() }; void Cat::speak() //类外实现虚函数 { cout<<"猫的叫声:喵喵"<<endl; } class Dog:public Animal //狗类Dog,公有继承Animal类 { public: virtual void speak(); //声明虚函数speak() }; void Dog::speak() //类外实现虚函数 { cout<<"狗的叫声:汪汪"<<endl; } int main() { Cat cat; //创建Cat类对象cat Animal *pA=&cat; //定义Animal类指针pA指向对象cat pA->speak(); //通过pA调用speak()函数 Dog dog; //创建Dog类对象dog Animal *pB=&dog; //定义Animal类指针pB指向对象dog pB->speak(); //通过pB调用speak()函数 return 0; }运行结果:
猫的叫声:喵喵
狗的叫声:汪汪
- 第 3~7 行代码定义了动物类 Animal,该类声明了虚函数 speak();
- 第 8~11 行代码在类外实现虚函数 speak()。需要注意的是,在类外实现虚函数时,返回值类型前不能添加 virtual 关键字;
- 第 12~16 行代码定义了猫类 Cat,公有继承 Animal。Cat 类也声明了虚函数 speak();
- 第 21~25 行代码定义了狗类 Dog,公有继承 Animal 类,Dog 类也声明了虚函数 speak();
- 第 32~34 行代码,在 main() 函数中创建 Cat 类对象 cat,定义 Animal 类指针 pA 指向对象 cat,然后通过 pA 调用 speak() 函数;
- 第 35~37 行代码创建 Dog 类对象 dog,定义 Animal 类指针 pB 指向对象 dog,然后通过 pB 调用 speak() 函数。
基类指针调用的永远都是派生类重写的虚函数,不同的派生类对象都有自己的表现形态。
需要注意的是,派生类对基类虚函数重写时,必须与基类中虚函数的原型完全一致,派生类中重写的虚函数前是否添加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: //... }; 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 放置在对象的起始位置,为其分配空间,并调用构造函数将其初始化为虚函数表地址。注意,虚函数表不占用对象空间。
派生类继承基类时,也继承了基类的虚函数指针。当创建派生类对象时,派生类对象中的虚函数指针指向自己的虚函数表。在派生类的虚函数表中,派生类虚函数会覆盖基类的同名虚函数。
当通过基类指针或基类引用操作派生类对象时,以操作的对象内存为准,从对象中获取虚函数指针,通过虚函数指针找到虚函数表,调用对应的虚函数。
【示例2】下面结合代码分析虚函数实现多态的机制,示例代码如下:
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 类的继承关系如图 1 所示。

图1 Derive类与Base1类和Base2类的继承关系
在编译时,编译器发现 Base1 类与 Base2 类有虚函数,就为两个类创建各自的虚函数表,并在两个类中添加虚函数指针。
如果创建 Base1 类对象(如 base1)和 Base2类对象(如 base2),则对象中的虚函数指针会被初始化为虚函数表的地址,即虚函数指针指向虚函数表。如下所示:

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

图3 对象derive的内存逻辑示意图
通过基类 Base1、基类 Base2 的指针或引用操作 Derive 类对象,在程序运行时,编译器从 Derive 类对象内存中获取虚函数指针,通过指针找到虚函数表,调用相应的虚函数。不同的类,其函数实现都不一样,在调用时就实现了多态。
虚析构函数
在 C++ 中不能声明虚构造函数,因为构造函数执行时,对象还没有创建,不能按照虚函数方式调用。但是,在 C++ 中可以声明虚析构函数,虚析构函数的声明是在“~”符号前添加 virtual 关键字,格式如下所示:
virtual ~析构函数();
在基类中声明虚析构函数之后,基类的所有派生类的析构函数都自动成为虚析构函数。在基类声明虚析构函数之后,使用基类指针或引用操作派生类对象,在析构派生类对象时,编译器会先调用派生类的析构函数释放派生类对象资源,然后再调用基类析构函数。
如果基类没有声明虚析构函数,在析构派生类对象时,编译器只会调用基类析构函数,不会调用派生类析构函数,导致派生类对象申请的资源不能正确释放。
【示例3】下面通过案例演示虚析构函数的定义与调用,C++ 代码如下:
#include<iostream> using namespace std; class Base //基类Base { public: virtual ~Base(); //虚析构函数 }; Base::~Base() { cout<<"Base类析构函数"<<endl; } class Derive:public Base //派生类Derive,公有继承Base类 { public: ~Derive(); //虚析构函数 }; Derive::~Derive() { cout<<"Derive类析构函数"<<endl; } int main() { Base *pb=new Derive; //基类指针指向派生类对象 delete pb; //释放基类指针 return 0; }运行结果:
Derive类析构函数
Base类析构函数
- 第 3~7 行代码定义了 Base 类,该类声明了虚析构函数;
- 第 12~16 行代码定义 Derive 类公有继承 Base 类。Derive 类中定义了析构函数,虽然析构函数前面没有添加关键字 virtual,但它仍然是虚析构函数;
- 第 23~24 行代码,定义了 Base 类指针 pb 指向一个 Derive 类对象,然后使用 delete 运算符释放 pb 指向的空间。
由运行结果可知,程序先调用了 Derive 类析构函数,然后又调用了 Base 类析构函数。
虚析构函数的定义与用法很简单,但在 C++ 程序中却是非常重要的一个编程技巧。
在编写 C++ 程序时,最好把基类的析构函数声明为虚析构函数,即使基类不需要析构函数,也要显式定义一个函数体为空的虚析构函数,这样所有派生类的析构函数都会自动成为虚析构函数。
如果程序中通过基类指针释放派生类对象,编译器能够调用派生类的析构函数完成派生类对象的释放。
声明:《C++系列教程》为本站“54笨鸟”官方原创,由国家机构和地方版权局所签发的权威证书所保护。