多态与虚函数
多态
面向对象3种机制之一(封装、继承、多态),提高程序的可读性,可扩展性和可重用性。
概念
不同对象可以调用相同名称的函数,但可导致完全不同的行为的现象。
多态实现的是接口的复用。不论传递过来是哪个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
使用
编译时多态(静态多态):即函数重载,在编译时就能绑定调用语句与调用函数入口地址。
运行时多态(动态多态)函数调用与代码入口地址的绑定需要在运行时刻才能确定。
运行时多态通过基类的指针或基类引用调用虚函数。
基类指针多态实质:
用基类指针调用相同的函数实现不同的功能。
基类引用多态:
基类的引用调用基类和派生类中同名、同参数表的虚函数时,若其引用的是一个基类的对象,则调用的是基类的虚函数,若引用是一个派生类的对象,则调用提派生类的虚函数。
普通成员函数实现多态
(静态成员函数、构造函数和析构函数除外)调用其他虚成员函数实现多态。
虚函数
类定义用virtual关键字来限定成员函数。只能写在成员函数声明,也就是virtual不能写在类外。
使用
- 虚函数一般不声明为内联函数。内联函数是编译阶段进行静态处理的,而对虚函数的调用是动态绑定的。
- 构造函数不能声明为虚函数。
- 静态函数不能声明为虚函数
- 友元函数不能声明为虚函数,友元函数不属于类成员函数,不能被继承,没有声明为虚函数的必要。
- 全局函数不能声明为虚函数。虚函数是为了与继承机制配合实现多态的,而全局函数(非成员函数)不属于某个类,没有继承关系,只能被重载不能被覆盖,声明为虚函数也起不到多态的作用,因此编译器会在编译时绑定全局函数。
- 派生类重写基类的虚函数实现多态,要求函数名、参数列表及返回值类型要完全相同。
- 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。考点
- 不要在构造和析构函数中调用虚函数。
- 最好将基类的析构函数声明为虚函数。
使用例子
#include <iostream>
using namespace std;
class CBase
{
public:
void func1()
{
cout << "CBase::func1()" << endl;
func2();
func3();
}
virtual void func2() //虚函数
{
cout << "CBase::func2()" << endl;
}
void func3() //不是虚函数
{
cout << "CBase::func3()" << endl;
}
};
class CDerived: public CBase //派生类
{
public:
virtual void func2() //虚函数
{
cout << "CDerived::func2()" << endl;
}
void func3()
{
cout << "CDerived::func3()" << endl;
}
};
int main()
{
CDerived d;
d.func1();//打印结果为:CBase::func1() CDerived::func2() CBase::func3()
return 0;
}
虚函数原理(重点)
多态是用父类型的指针,指向其子类的实例。然后通过父类型的指针调用实际子类的成员函数。这种技术可以让父类指针有多种形态。实际上是一种泛型技术。使用不变的代码来实际不同的算法。要么在编译时决议(函数重载),要么在运行时决议(虚函数)。
虚函数实现多态
通过一张虚函数表来实现,这张表先由基类定义的虚函数地址。当派生类继承时,再加上子类的虚函数地址。定义一个子类对象后,会综合两部分的虚函数的地址形成一张虚函数表。c++编译时,将这个实例的虚函数表的指针,放在最前面的位置,方便查找里面虚函数地址入口。如下图所示:
Base *b = new Derive();
b->f()//调用的是子类的driver.f()函数
多继承的情况
会生成多个虚函数指针,指向多个虚函数表。但子类的虚函数会放在第一个虚函数指针所指向的虚函数表里。也就是按顺序继承的第一个父类的虚函数表里。如下图所示:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
这样通过任一静态类型的父类来指向子类
虚析构函数
格式:
virtual ~类名()
只要基类的析构函数被说明为虚函数,则派生类的析构函数,都自动成为虚函数
目的:为了使对象消亡时使用多态。保证使用基类类型的指针或引用时可以动态绑定,保证使用基类指针能够调用适当的析构函数针对不同的对象进行清理工作,以避免造成内存泄漏。
纯虚函数和抽象类
纯虚函数是声明在基类中的虚函数,没有具体的定义。而由各派生类根据实际需要给出各自的定义。要求在任何派生类中都定义自己的版本。纯虚函数为各派生类提供一个公共界面
一般格式:
virtual 函数类型 函数名(参数列表)=0
必须有=0,不能有函数体,大括号也不能有。
使用例子
#include <iostream>
using namespace std;
//抽象类
class A
{
public:
virtual void func() = 0;
};
class B: public A
{
public:
virtual void func()
{
cout << "实现父类的纯虚函数" << endl;
}
};
int main()
{
B b;
b.func();
return 0;
}
抽象类
包含纯虚函数的类称为抽象类。
抽象类的派生类中,给出全部纯虚函数的定义,那么派生类就不是抽象类,若没有全部给出,那么抽象类继续是抽象类
- 抽象类不能实例化一个对象,即不能创建抽象类的对象。不能作为函数的参数,也不能作为函数的返回值。
- 可以定义抽象类的指针和引用,这样指针和引用可以指向并访问派生类的成员,这种访问具有多态性。
A * a; //可以声明抽象类的指针
A & a; //可以声明抽象类的引用 - 抽象类至少包含一个虚函数,而且至少有一个是纯虚函数,以便将它与空的虚函数区分开来。
虚基类
定义
虚基类是在多重继承中,被虚继承的祖父类
例子
#include <iostream>
using namespace std;
class A
{
public:
A()
{
cout << "无参构造A" << endl;
};
A(int a): m_a(a)
{
cout << "有参构造A" << endl;
};
protected:
int m_a;
};
class B1: virtual public A //使用virtural关键字实现虚继承
{
public:
B1()
{
cout << "B1\n";
};
B1(int a): A(a)
{
cout << "有参构造B1\n";
}
protected:
};
class B2: virtual public A //使用virtural关键字实现虚继承
{
public:
B2()
{
cout << "B2\n";
};
B2(int a): A(a)
{
cout << "有参构造B2\n";
}
protected:
};
class C: public B2, public B1 //:之后称为类派生表,表的顺序决定基类构造函数调用的顺序,析构函数的调用顺序正好相反
{
public:
C(int a): A(a)
{
cout << "C\n";
}
C(int a, int ba2, int ba1): A(a), B2(ba2), B1(ba1)
{
cout << "三参构造C\n";
}
void Print()
{
cout << "B1::m_a = " << B1::m_a << endl;
cout << "B2::m_a = " << B2::m_a << endl;
cout << "m_a = " << m_a << endl;
}
protected:
};
int main()
{
C c1(4, 5, 6);
c1.Print(); //有参构造A
//有参构造B2
// 有参构造B1
// 三参构造C
// B1::m_a = 4
// B2::m_a = 4
// m_a = 4
return 0;
}
总结
virtual base(虚基类)的初始化责任是由继承体系中的最底层(most derived)class负责
特殊应用
通过添加类名::也无法排除二义性问题的特殊情形如下图所示:
提供虚基类机制,使得在派生类中,继承同一个间接基类成员仅保留一个版本。
格式:
class 派生类名:virtual 派生方式 基类名{派生类体}