1.引入虚函数
继承的成员函数的行为以及他们与派生类成员函数的关系。给CBox类添加一个输出CBox对象体积的函数。
1.新建win32控制台项目项目,右键添加新建项CBox.h类:
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class CBox
{
public:
//输出箱子体积函数
void showVolume() const{ cout << "CBox的体积是" << volume() << endl; }
//返回箱子体积函数
double volume()const{ return m_Length*m_Width*m_Height; }
//构造函数(在初始化列表中设定数据成员的值,因此其函数体中不需要任何语句)
CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0) :m_Length{ lv }, m_Width{ wv }, m_Height{ hv }{}
//声明箱子的成员变量(数据成员指定为protected,因此任何派生类的成员函数都可以访问它们)
protected:
double m_Length;
double m_Width;
double m_Height;
};
2.新建CGlassCBox类:
#pragma once
#include "CBox.h"
//创建另一个箱子类CGlassBox,体积是原来体积CBox容量的0.85倍
class CGlassBox :public CBox
{
public:
//用不同的volume()函数来计算体积
double volume() const
{
return 0.85*m_Length*m_Height*m_Width;
}
//构造函数(派生类中可能还有其他成员,派生类对象的构造函数只是在初始化列表中调用基类构造函数来设定数据成员的值)
CGlassBox(double lv, double wv, double hv):CBox{ lv, wv, hv }{}
};
//希望:当CGlassBox类的对象调用继承的showVolume()函数时,该函数能够调用派生类中新版本的volume()成员函数。
3.新建项目.cpp文件
#include "stdafx.h"
#include "CGlassBox.h" //引用CGlassBox头文件
void output(const CBox& aBox); //声明一个传引用函数
int _tmain(int argc, _TCHAR* argv[])
{
//声明一个CBox
CBox myBox{ 2.0, 3.0, 4.0 };
//声明一个派生类的箱子(成员变量大小相同)
CGlassBox myClassBox{ 2.0, 3.0, 4.0 };
//调用CBox的体积
myBox.showVolume();
//调用派生类的箱子体积
myClassBox.showVolume();
system("pause");
return 0;
}
运行结果:
CBox的体积是24
CBox的体积是24
该程序没有按照我们设想的方式工作,唯一令人感兴趣的是为什么会这样。显然,改程序没有考虑到第二次调用要处理CGlassBox这个派生类的对象,这一点可以从错误的输出中看出。
输出的错误原因在于,showVolume()函数中对volume()函数的调用被编译器一劳永逸地设定为基类中定义的版本。showVolume()是基类函数,在编译CBox时,编译器将volume()的调用解析为基类的volume()函数,编译器不知道有任何其他volume()函数。该过程称为函数调用的静态解析,因为函数调用是在程序执行之前确定的。也称为早期绑定,因为在程序编译过程(而不是执行过程)中将选中的特定volume()函数绑定到showVolume()函数包含的调用上。
而我们希望在程序执行时再解决特定的对象实例中使用哪个volume()函数的问题。这中操作称为动态链接或后期绑定。
C++提供了实现该功能的方法–需要使用虚函数。
2.虚函数的概念
虚函数是以Virtual关键字声明的基类函数。如果在基类中将某个函数指定为virtual,并且派生类中有该函数的另一个定义,则编译器将知道我们不想静态链接该函数。我们真正需要的是基于调用该函数的对象种类,在程序的特定位置选择调用哪一个函数。
修改上面的程序:
1.修改CBox.h类:
class CBox
{
public:
//输出箱子体积函数
void showVolume() const{ cout << "CBox的体积是" << volume() << endl; }
//虚函数
virtual double volume()const{ return m_Length*m_Width*m_Height; }
//构造函数(在初始化列表中设定数据成员的值,因此其函数体中不需要任何语句)
CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0) :m_Length{ lv }, m_Width{ wv }, m_Height{ hv }{}
//声明箱子的成员变量(数据成员指定为protected,因此任何派生类的成员函数都可以访问它们)
protected:
double m_Length;
double m_Width;
double m_Height;
};
2.修改CGlassCBox类文件:
//创建另一个箱子类CGlassBox,体积是原来体积CBox容量的0.85倍
class CGlassBox :public CBox
{
public:
//引用虚函数
virtual double volume() const
{
return 0.85*m_Length*m_Height*m_Width;
}
//构造函数(派生类中可能还有其他成员,派生类对象的构造函数只是在初始化列表中调用基类构造函数来设定数据成员的值)
CGlassBox(double lv, double wv, double hv):CBox{ lv, wv, hv }{}
};
运行结果如下:
CBox的体积是24
CBox的体积是20.4
现在程序做了我们原来希望它做的事情。
注意:虽然在派生类中的volume()函数定义了使用virtual关键字,但这样做不是必须的。在基类中将该函数定义为virtual已经足够了。不过建议在派生类中为虚函数指定virtual关键字,因为这样可以使阅读派生类定义的任何人都清楚地知道这些函数是动态选择的虚函数。
虚函数的运用是一种功能特别强大的机制。面向对象编程中的“多态性”指的就是虚函数功能。多态的某种事物能够以不同外观出现,如医生,警察,政治家。根据当前对象的种类,调用虚函数将产生不同的结果。
从派生类函数的观点来看,CBox派生类中Volume()函数实际上隐藏了该函数的基类版本。如果我们希望从某个派生类中调用基类的volume()版本,则需要以CBox::Volume()这样的形式使用作用域解析运算符来引用基类函数。
3.虚函数检查override
在派生类中添加override
virtual double volume() const override
{
return 0.85*m_Length*m_Height*m_Width;
}
编译器会检查基类中是否有相同签名的volume()函数。如果没有,就会收到一个错误信息。为了进行演示,可以通过天机override修饰符并省略const关键字来修改CGlassBox类中volume()的定义。
注意:与final修饰符一样,override也不是关键字,它也只在这里的上下文有特殊的含义。
4.禁止重写函数
有时可能希望禁止重写类的某个函数成员,这可能是因为希望保护某行为的特定方面。此时可以把成员函数指定为final。例如:
virtual double volume()const final
{
return m_Length*m_Width*m_Height;
}
final修饰符搞苏编译器volume()函数不能被重写。进行了这个修改后,编译器就把派生类中的volume()函数标记为一个错误。
5.使用指向类对象的指针处理虚函数
使用指针处理基类和派生类的对象时一项重要的技术。指向基类对象的指针不仅可以包含基类对象的地址,还可以包含派生类对象的地址。可以根据指向的对象种类,使用类型为”基类指针”的指针获得虚函数的不同行为。
仅修改新建项目名称.cpp文件:
int _tmain(int argc, _TCHAR* argv[])
{
//声明一个CBox
CBox myBox{ 2.0, 3.0, 4.0 };
//声明一个派生类的箱子(成员变量大小相同)
CGlassBox myClassBox{ 2.0, 3.0, 4.0 };
//创建一个基类CBox对象的指针
CBox* pBox{};
//指针指向基类
pBox = &myBox;
//调用函数showVolume
pBox->showVolume();
//指针指向派生类
pBox = &myClassBox;
pBox->showVolume();
system("pause");
return 0;
}
运行结果:
CBox的体积是24
CBox的体积是20.4
输出与上一个示例的输出完全相同,在上一个示例中,我们在函数调用中使用了显式的对象。从本示例可以得出这样的结论:虚函数机制借助于指向基类的指针同样能够正常工作,实际调用的函数是基于被指向的对象类型而选择的。
即使我们不知道程序中某个基类指针所指对象的准确类型(例如,某个指针作为实参传递给函数时),虚函数机制也能确保调用正确的函数(例如,某个指针作为实参传递给函数时)。这是一种特别强大的功能,因此,务必充分理解。多态性是一种我们将多次使用的基本C++机制。
6.使用引用处理虚函数
如果定义一个形参为基类引用的函数,则可以给该函数传递派生类的对象作为实参。该函数在执行时,将自动为传递进来的对象选择适当的虚函数。我们将上一个示例中的main()函数修改成调用一个形参为引用的函数,就可以看到这种情况。
同样仅修改 新建项目名称.cpp文件:
void output(const CBox& aBox); //声明一个传引用函数
int _tmain(int argc, _TCHAR* argv[])
{
//声明一个CBox
CBox myBox{ 2.0, 3.0, 4.0 };
//声明一个派生类的箱子(成员变量大小相同)
CGlassBox myClassBox{ 2.0, 3.0, 4.0 };
//调用函数
output(myBox);
output(myClassBox);
system("pause");
return 0;
}
void output(const CBox& aBox)
{
aBox.showVolume();
}
运行结果:
CBox的体积是24
CBox的体积是20.4
main()函数现在由两次对output()函数的调用组成,第一次用基类对象作为实参,第二次用派生类对象作为实参。因为形参是基类的引用,所以output()函数可以接受这两种类对象实参,并根据初始化引用的对象种类,调用适当的虚函数volume();
虚函数机制确实能够借助于引用形参起作用。多态机制可用于指针和引用。
这些大多是书上的原话,强烈推荐像我这样对C++基础不是特别好的,去读读这本书《VC++入门经典》专治各种概念定义拿捏不清楚的疑难咋整(1-10章)!哈哈。