派生类继承了基类除构造函数和析构函数外的所有数据成员和函数成员。派生类和基类存在一种特殊关系:派生类是一种基类,具有基类的所有功能。面向对象的程序设计利用派生类和基类之间的特殊关系,常常将派生类对象当作基类对象使用,或者用基类来代表派生类,其目的是提高代码可重用性。由于C++对数据类型一致性要求比较严格,一般不能调用处理A类对象的函数afun(A x)来处理B类对象数据。
一、认识对象的替换和多态
通过一个例子更直观的理解对象的替换和多态:
class A
{
public:
void fun1() //普通函数成员fun1
{ cout<< "A::fun1 called\n"; }
virtual void fun2() //虚函数成员fun2
{ cout<< "Virtual A::fun2 called\n"; }
};
class B:public A //定义派生类B,公有继承A
{
public:
void fun1() //重写基类同名函数成员fun1
{ cout<< "B::fun1 called\n"; }
virtual void fun2() //重写基类同名虚函数成员fun2
{ cout<< "Virtual B::fun2 called\n"; }
};
void exfun(A &x) //类外函数处理类对象
{
cout<<"exfun call class A\n";
x.fun1();
}
void exfun1(A &x) //类外函数调用类虚函数,实现对象多态性
{
cout<<"exfun call class A\n";
x.fun2();
}
下面通过两个类分别定义对象和引用来观察它们所调用的函数是那些
A aobj;B bobj; //分别定义两个类的对象
A &ra1=aobj;
A &ra2=bobj; //通过基类对象引用两个对象
ra1.fun1(); //显示 A::fun1 called
ra2.fun1(); //显示 A::fun1 called
ra1.fun2(); //显示 Virtual A::fun2 called
ra2.fun2(); //显示 Virtual B::fun2 called
得到以下结论:
1)通过基类引用ra2来引用派生类对象bobj,相当于将派生类对象当作基类对象来使用,这被称作对象替换。
2)通过基类引用调用虚函数成员fun2,基类对象aobj和派生类对象bobj会显示不同信息。换言之,接收相同指令fun2,基类对象和派生类对象表现出不同行为,呈现多样化形态,这就是对象的多态性。
二、类型兼容语法规则(对象替换)
为了让派生类对象可以与基类对象一起共用算法代码,C++语言专门指定了如下的类型兼容语法规则:
1)派生类的对象可以赋值给基类对象。
2)派生类的对象可以初始化基类引用,或者说基类引用可以引用派生类对象。
3)派生类对象的地址可以赋值给基类指针,或者说基类的对象指针可指向派生类对象。
应用类型兼容语法规则有一个前提条件和一个使用限制。
前提条件:派生类必须公有继承基类。因为公有继承下,派生类拥有基类的全部功能,派生类对象可以当作基类对象使用。
使用限制:通过基类的对象、引用或对象指针访问派生类对象时,只能访问到基类成员,赋值和访问时只能接收基类成员。
示例:
A x1;B x2; A &x3 = x1; //定义类A,类B对象和类A的引用并初始化为类x1
x1 = x2; //派生类对象给基类对象赋值
x3 = x2; //基类引用可以引用派生类对象
A *p; p = &x2; //基类对象指针指向派生类对象
exfun(x2); //输出exfun call class A[换行] A::fun1 called
//此时通过类A的外部处理函数处理派生类B对象,完成代码重用
三、对象的多态性
通过上面示例思考,如何通过基类引用或指针访问派生类中与基类中同名的函数?我们将调用对象的某个函数成员称为向对象发送一条消息。将执行函数成员完成某种程序功能称为对象响应该消息。不同对象接收相同消息,但会表现除不同的行为,这就是对象的多态性。对象多态性就是:调用不同对象的同名函数成员,所执行的函数不同,完成的程序功能也不同。导致对象多态性的同名函数有以下三种不同形式:
1) 不同类之间的同名函数。类成员有作用域,不同类之间的函数成员可以同名互不干扰。
2)类中的重载函数。通过一类中的函数成员可以重名,只要他们的形参个数不同或类型不同。重载函数成员导致的多态本质上属于重载函数多态。
3)派生类中的同名覆盖。派生类中新增的函数成员可以与从基类继承来函数成员同名,但他们不是重载函数。
引入对象多态性的目的是为了让另外一些处理基类的代码也能够被派生类对象重用。“另外一些”代码的作用也就是通过基类引用或对象指针访问派生类代码时可以根据实际引用或指向的对象类型,自动调用该类同名函数中新增成员。这是需要将这些同名函数声明成虚函数。C++语言以虚函数的语法形式来实现对象多态性,例如示例中的exfun1(A &x)
函数。
四、虚函数
应用虚函数实现对象多态性的过程分为两步:先声明虚函数,在调用虚函数。
1、声明虚函数
在定义基类时使用关键字virtual
将函数声明成虚函数,然后通过公有继承定义派生类,并重写虚函数成员,也就是新增一个与虚函数同名的函数成员。此后使用基类或派生类定义对象,其函数成员中只有虚函数成员才会在调用时呈现出多态性。
为了更好的说明虚函数的声明和使用,我们编写一个简单示例代码如下:
class A //类声明
{
public:
virtual void fun1(); //声明虚函数成员fun1
void fun2(); //声明非虚函数成员fun2
};
//类实现
void A::fun1() { cout<<"Base class A:virtual fun1() called"<<endl; }
void A::fun2() { cout<<"Base class A:non-virtual fun2() called"<<endl; }
class B:public A //定义派生类B,公有继承A
{
public:
virtual void fun1(); //重写虚函数成员fun1
void fun2(); //重写非虚函数成员fun2
};
void B::fun1() { cout<<"Derived class B:virtual fun1() called"<<endl; }
void B::fun2() { cout<<"Derived class B:non-virtual fun2() called"<<endl; }
声明虚函数的语法细则:
1)只能在类声明部分声明虚函数。在类实现部分定义函数成员时不能在使用关键字virtual
。
2)基类中声明虚函数成员被继承到派生类后,自动成为派生类的虚函数成员。
3)派生类可以重写虚函数成员。如果重写后的函数原型与基类虚函数成员完全一致,则该函数自动成为派生类的虚函数成员,无论声明时加不加virtual
。
4)类函数成员中的静态函数、构造函数不能是虚函数。析构函数可以是虚函数。
2、调用虚函数
下面我们通过派生类对象、基类引用和基类对象指针分别调用派生类虚函数和非虚函数,得到的结果如下:
//通过对象名调用函数成员
A aObj; B bObj;
bObj.fun1(); //调用结果:调用派生类bObj的新增虚函数成员fun1
bObj.fun2(); //调用结果:调用派生类bObj的新增非虚函数成员fun2(同名覆盖)
//通过基类引用调用函数成员
A &raObj = aObj; //定义基类引用,引用基类对象
raObj.fun1(); //调用结果:调用基类对象aObj的虚函数成员fun1
raObj.fun2(); //调用结果:调用基类对象aObj的非虚函数成员fun2
A &rbObj = bObj; //定义基类引用,引用基类对象
rbObj.fun1(); //调用结果:调用派生类对象bObj的新增虚函数成员fun1
rbObj.fun2(); //调用结果:调用派生类对象bObj的基类非虚函数成员fun2(类型兼容规则)
//通过基类对象指针调用函数成员
A *paObj = &aObj;//定义基类对象指针paObj,指向基类对象aObj
paObj->fun1(); //调用结果:调用基类对象aObj的虚函数成员fun1
paObj->fun2(); //调用结果:调用基类对象aObj的非虚函数成员fun2
A *pbObj = &bObj;//定义基类对象指针paObj,指向基类对象aObj
pbObj->fun1(); //调用结果:调用派生类对象bObj的新增虚函数成员fun1
pbObj->fun2(); //调用结果:调用派生类对象bObj的基类非虚函数成员fun2(类型兼容规则)
总结:通过基类的引用或对象指针访问类族中对象的虚函数成员(例如:fun1),基类对象和派生类对象将分别调用各自的虚函数成员,呈现出多态性。如果访问的是非虚函数成员(例如:fun2),则访问的都是基类成员,不会呈现多态性。
实现基类对象与派生类对象之间的多态性要满足以下三个条件:
1)在基类中声明虚函数成员。
2)派生类需公有继承基类,并重写虚函数成员(属于新增成员)。
3)通过基类的引用或对象指针调用虚函数成员。
只有满足这三个条件,基类对象和派生类对象才会分别调用各自的虚函数,呈现出多态性。
将源程序中具有多态性的虚函数名转换成某个具体的函数存储地址,这种函数名到存储地址的转换被称为是对函数的绑定。通过基类的引用或对象指针调用虚函数成员,到底是调用基类成员还是新增成员,这在编译时还不能确定。其绑定过程需要在程序执行时才能完成。对象多态是一种执行时多态。
总结以下通过对象的多态性让类族共用算法代码需按以下步骤进行编程:
1)声明虚函数。定义基类时需要确定将那些函数成员声明成虚函数。一般将可能被派生类修改或扩充的函数成员声明成虚函数。
2)重写虚函数。定义派生类时公用继承基类,并重写那些从基类继承来的虚函数成员。主要是为了修改和扩充基类功能。
3)通过基类引用和对象指针访问对象。访问派生类对象,调用其中的虚函数成员将自动调用重写的虚函数,否则自动调用从基类继承来的函数成员(即类型兼容语法规则)。
3、虚析构函数
构造函数不能声明成虚函数。析构函数可以声明成虚函数,析构函数无形参,无函数类型。其声明语法形式如:virtual ~类名();
示例:
A *p1 = new A; //动态分配一个基类对象
A *p2 = new B; //动态分配一个派生类对象,使用基类的对象指针保存其地址
//使用对象略
delete p1; //自动调用基类析构函数
delete p2; //自动调用派生类析构函数
可以注意到,p1 和p2都是基类的对象指针,使用delete运算符删除对象时,将根据所指向对象的类型自动调用不同的析构函数,呈现出多态性。删除派生类对象时,先执行派生类析构函数来析构新增成员,再执行基类析构函数来析构基类成员。如果不采用虚析构函数,那么删除派生类对象时将只会调用基类析构函数。
五、抽象类和纯虚函数
1、纯虚函数
类定义中,“只声明,未定义”的函数成员被称为纯虚函数。纯虚函数的声明语法形式为: virtual 函数类型 函数名(形参列表)=0;
纯虚函数是一种虚函数,具有虚函数的特性,其中最重要的一条就是虚函数成员再调用时具有多态性。含有纯虚函数的类就是抽象类。
抽象类具有如下特性:
1)抽象类不能实例化
不能使用抽象类定义对象(即不能实例化),因为抽象类中含有未定义的纯虚函数,其类型定义不完整。但可以定义抽象类的引用或对象指针,所定义引用、对象指针可以引用其派生类的实例化对象。
2)抽象类可以作为基类定义派生类
抽象类可以作为基类定义派生类。此时的派生类也会继承抽象类的纯虚函数,由于抽象类只是声明了纯虚函数的函数原型,没有定义函数体代码,因此其派生类只是继承了其函数原型。派生类需要为纯虚函数成员编写函数体代码,这称为实现纯虚函数成员。派生类如果实现了所有的纯虚函数成员,那么它就变成了一个普通的类,可以实例化。只要派生类还有一个为实现的纯虚函数,那么它就还是一个抽象类,不能实例化,这是它还是只能作为基类继续往下派生,直到实现所有纯虚函数成员后才能实现化。
2、抽象类的应用
1)统一类族接口
派生类继承基类是为了重用基类的代码。如果基类时抽象类,纯虚函数成员只声明函数原型,这样类族中的所有派生类都具有相同的对外接口。统一接口可以方便类族的使用。
2)类族共用算法代码
抽象类中定义的纯虚函数具有虚函数特性,不同派生类中的虚函数实现和作用功能不同,调用时呈现多态性。类外函数可以通过抽象类(基类)引用和对象指针调用调用不同派生类对象,使得不同派生类对象公用该类外函数(算法)。
六、多继承、重复继承、虚基类(挖坑)