文章目录
第六章 多态与虚函数
第一节 多态的基本概念
- 1、多态分为编译时多态和运行时多态。
- 编译时多态(静态多态、早绑定):主要是指函数的重载(包括运算符的重载)。对重载函数的调用,在编译时就可以根据实参确定应该调用哪个函数。指调用同名函数,根据参数的不同而调用不同的同名函数。
- 运行时多态(动态多态、晚绑定):和继承、虚函数等概念有关。指不同对象调用同名函数时,由于对象不同而调用不同的同名函数。
- 2、多态的实现原理:多态的关键在于通过基类指针或引用调用一个虚函数时,编译阶段不能确定到底调用的是基类还是派生类的函数,运行时才能确定。
- 3、静态多态和动态多态的区别:在什么时候将函数实现和函数调用关联起来,是在编译阶段还是在运行阶段,即函数地址是早绑定的还是晚绑定的。
- 4、实现动态绑定必须满足的两个条件(在类之间满足赋值兼容的前提下):
- ① 必须声明虚函数。
- ② 通过基类类型的引用或指针调用虚函数。
- 5、虚函数:在函数声明时前面加了virtual关键字的成员函数。
- ① virtual关键字只在类定义中的成员函数声明处使用,不能在类外部写成员函数体时使用。如果虚函数的定义是在类体外,则只需在声明函数时添加virtual关键字,定义时不加virtual关键字。
- ② 只有类的非静态成员函数才能定义为虚函数。静态成员函数、友元函数、构造函数不能定义为虚函数。
- ③ 不在构造函数和析构函数中调用虚函数。在构造函数和析构函数中,对象是不完整的,可能会出现未定义的行为。
- ④ 虚函数一般不声明为内联函数:因内联会在编译阶段进行静态处理,而虚函数的调用是动态的。
- ⑤ 最好将基类的析构函数声明为虚函数。
- ⑥ 包含虚函数的类称为“多态类”。
动态多态:通过基类指针实现多态
- 声明虚函数后,派生类对象的地址可以赋值给基类指针,也就是基类指针可以指向派生类对象。
- 对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时系统并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则调用基类的虚函数;如果基类指针指向的是一个派生类对象,则调用派生类的虚函数。
#include <iostream>
using namespace std;
class A
{
public:
//定义虚函数(声明和定义必须在类内)
virtual void Print(){ cout<<"A::Print"<<endl; };
};
//公有继承
class B:public A
{
public:
virtual void Print(){ cout<<"B::Print"<<endl; }
};
//公有继承
class D:public A
{
virtual void Print(){ cout<<"D::Print"<<endl; }
};
//公有继承
class E:public B
{
virtual void Print(){ cout<<"E::Print"<<endl; }
};
int main()
{
A a;B b;D d;E e;
A *pa=&a; //基类pa指针 指向基类对象a
B *pb=&b; //派生类指针pa指向派生类对象b
//多态,目前指向基类对象,调用a.Print(),输出A::Print
pa->Print();
/** 注:但即使基类指针指向的是一个派生类的对象,
也不能通过基类指针访问基类中没有而仅在派生类中定义的成员函数。*/
pa = pb; //派生类指针赋值给基类指针,pa指向派生类对象b
//多态,目前指向派生类对象,调用b.Print(),输出B::Print
pa->Print();
pa=&d; //基类指针pa指向派生类对象d
//多态,目前指向派生类对象,调用d.Print(),输出D::Print
pa->Print();
return 0;
}
动态多态:通过基类引用实现多态
- 通过基类指针调用虚函数时可以实现多态,通过基类的引用调用虚函数的语句也是多态的。即通过基类的引用调用基类和派生类中同名、同参数表的虚函数时,若其引用的是一个基类的对象,则调用的是基类的虚函数;若其引用的是一个派生类的对象,则调用的是派生类的虚函数。
#include <iostream>
using namespace std;
class A
{
public:
virtual void Print(){ cout<<"A::Print"<<endl; };
};
class B:public A
{
public:
virtual void Print(){ cout<<"B::Print"<<endl; };
}
//多态,使用基类引用,调用哪个Print()取决于r引用了哪个类的对象
void PrintInfo(A &r){ r.Print(); }
int main()
{
A a;B b;
PrintInfo(a); //使用基类对象,调用基类中的函数,输出A::Print
PrintInfo(b); //使用派生类对象,调用派生类类中的函数,输出B::Print
return 0;
}
第二节 多态实例
- 定义一个基类CShape表示一般图形,然后派生3个子类分别表示矩形类、圆形类和三角形类。基类中定义了计算图形面积和输出信息的虚函数,3个派生类中均继承了这两个虚函数,可以针对具体的图形计算各自的面积并输出结果。
#include <iostream>
using namespace std;
const double PI=3.14159;
class Point
{
private:
double x,y;
public:
Point(double i,double j):x(i),y(j){};
virtual double area(){return 0;}
};
class Circle:public Point
{
private:
double radius;
public:
Circle(double a,double b,double r):Point(a,b),radius(r){};
double area(){ return PI*radius*radius; };
};
void display(Point *p){ cout<<p->area()<<endl; };
void display(Point &a){ cout<<a.area()<<endl; };
void main()
{
Point a(1.5,6.7);
Circle c(1.5,6.7,2.5);
Point *p=&c; //派生类对象的地址赋给基类指针;
Point &rc=c; //派生类对象初始化基类引用;
display(a); //基类对象调用基类虚函数area,输出0
display(p); //指针调用派生类虚函数area,输出19.6349
display(rc); //指针调用派生类虚函数area,输出19.6349
}
第三节 多态的使用
- 在普通成员函数(静态成员函数、构造函数和析构函数除外)中调用其他虚成员函数也是允许的,并且是多态的。
- 不仅能在成员函数中调用虚函数,还可以在构造函数和析构函数中调用虚函数,但这样调用的虚函数不是多态的。
- 在构造函数中调用的,编译系统可以据此决定调用哪个类中的版本,所以它不是多态的;
- 在析构函数中调用的,所以也不是多态的;
- 实现多态时,必须满足的条件是:使用基类指针或引用来调用基类中声明的虚函数。
- 派生类中继承自基类的虚函数,可以写virtual关键字,也可以省略这个关键字,这不影响派生类中的函数也是虚函数。
#include <iostream>
using namespace std;
class A
{
public:
void func1(){ cout<<"A::func1"<<endl; };
virtual void func2(){ cout<<"A:func2"<<endl; };
};
class B:public A
{
public:
virtual void func1(){ cout<<"B::func1"<<endl; };
//注:func2自动成为虚函数
void func2(){ cout<<"B:func2"<<endl; };
};
//类C以类A未间接基类
class C:public B
{
public:
//注:func1、func2自动成为虚函数
void func1(){ cout<<"C::func1"<<endl; };
void func2(){ cout<<"C:func2"<<endl; };
};
int main()
{
C obj;
A *pa=&obj;
B *pb=&obj;
//注:实现多态的条件:使用 基类指针或引用 来调用基类中声明的虚函数
pa->func2(); //多态
pa->func1(); //不是多态
pb->func1(); //多态
pb->func2(); //多态
return 0;
};
第四节 虚析构函数
- 如果一个基类指针指向的对象是用new运算符动态生成的派生类对象,那么释放该对象所占用的空间时,如果仅调用基类的析构函数,则只会完成该析构函数内的空间释放,不会涉及派生类析构函数内的空间释放,容易造成内存泄漏。
- 声明虚析构函数的一般格式如下: virtual 〜类名( );
- 虚析构函数没有返回值类型,没有参数,所以它的格式非常简单。
- 如果一个类的析构函数是虚函数,则由它派生的所有子类的析构函数也是虚析构函数。使用虚析构函数的目的是为了在对象消亡时实现多态。
#include <iostream>
using namespace std;
class ABase
{
public:
ABase(){ cout<<"ABase构造函数"<<endl; };
/** 如果一个类的析构函数是虚函数,
则由它派生的所有子类的析构函数也是虚析构函数。
使用虚析构函数的目的是为了在对象消亡时实现多态。
例: ~ABase(){ cout<<"ABase析构函数"<<endl; };
在delete基类时只会调用基类的析构函数,不会调用派生类的析构函数; **/
virtual ~ABase(){ cout<<"ABase析构函数"<<endl; };
};
class Derived:public ABase
{
public:
int w,h;
Derived():w(4),h(7){ cout<<"Derived构造函数"<<endl; };
~Derived(){ cout<<"Derived析构函数"<<endl; };
};
int main()
{
//使用基类指针指向new创建的派生类对象
ABase *p = new Derived();
delete p;
return 0;
}
- 可以看出,这次不仅调用了基类的析构函数,也调用了派生类的析构函数。
- 只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字声明,都自动成为虚析构函数。
- 一般来说,一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。不过切记,构造函数不能是虚函数。
第五节 纯虚函数
- 纯虚函数的作用相当于一个统一的接口形式,表明在基类的各派生类中应该有这样的一个操作,然后在各派生类中具体实现与本派生类相关的操作。
- 纯虚函数是声明在基类中的虚函数,没有具体的定义,而由各派生类根据实际需要给出各自的定义。
- 声明纯虚函数的一般格式如下:
- virtual 函数类型 函数名(参数表) =0; 例如:virtual void fun( )=0;
- 纯虚函数没有函数体,参数表后要写“=0”。派生类中必须重写这个函数。按照纯虚函数名调用时,执行的是派生类中重写的语句,即调用的是派生类中的版本。
第六节 抽象类
- 包含纯虚函数的类称为 抽象类。因为抽象类中有尚未完成的函数定义,所以它不能实例化一个对象。
- 抽象类的派生类中,如果没有给出全部纯虚函数的定义,则派生类继续是抽象类。直到派生类中给出全部纯虚函数定义后,它才不再是抽象类,也才能实例化一个对象。虽然不能创建抽象类的对象,但可以定义抽象类的指针和引用。这样的指针和引用可以指向并访问派生类的成员,这种访问具有多态性。
- 纯虚函数不同于函数体为空的虚函数,它们的不同之处如下:
- ① 纯虚函数没有函数体,而空的虚函数的函数体为空。
- ② 纯虚函数所在的类是抽象类,不能直接进行实例化;而空的虚函数所在的类是可以实例化的。
- 它们共同的特点是:
- 纯虚函数与函数体为空的虚函数都可以派生出新的类,然后在新类中给出虚函数的实现,而且这种新的实现具有多态特征。
#include <iostream>
using namespace std;
class A
{
private:
int a;
public:
//纯虚函数
virtual void print()=0;
void func1(){ cout<<"A:func1"<<endl; };
};
class B:public A
{
public:
void print();
void func1(){ cout<<"B:func1"<<endl; };
};
void B::print(){ cout<<"B:print"<<endl; };
int main()
{
//A a; //错误,抽象类不能实例化。
//A b[2]; //错误,不能声明抽象类的数组。
//A *p=new A; //错误,不能创建类A的实例。
A *pa; //正确,可以声明抽象类的指针。
A *pb=new B; //正确,使用基类指针指向派生类对象。
//多态,调用的时类B中的函数,输出B:print
pb->print();
B b;
A *pc = &b;
//因为不是虚函数,调用的是类A中的函数,输出A:func1
pc->func1();
return 0;
}
第七节 虚基类
- 为了避免产生二义性,C++提供虚基类机制,使得在派生类中,继承同一个间接基类的成员仅保留一个版本。【例:A类为B、C的基类,A中有个public成员name;D类又继承与B、C时,在D类不可直接调用A中公共成员name。此时要在B、C继承A时添加virtual,使A为B、C的虚基类,便可解决这样的二义性。】
定义虚基类的一般格式如下:
class 派生类名:virtual 派生方式 基类名
{
派生类体
};
例如,图6-3所示的各类的继承关系如下:
class A
class B : virtual public A
class C : virtual public A
class D : public B, public C
#include <iostream>
using namespace std;
class A
{
public:
int a;
void showa(){ cout<<"a="<<a<<endl; };
};
//对类A进行虚继承
class B: virtual public A
{
public:
int b;
};
class C: virtual public A
{
public:
int c;
};
/** 派生类D的两个基类B、C具有共同的基类A,
采用了虚继承,从而使类D的对象中只包含着类A的1个实例 */
class D:public B,public C
{
public:
int d;
};
int main()
{
D Dobj; //说明派生类D的对象
Dobj.a=11; //若不是虚继承,此行会报错,因为“D::a”具有二义性
Dobj.b=22;
//若不是虚继承,此行会报错,因为“D::showa”具有二义性
Dobj.showa(); //输出a=11
}