讲解之前先举一个例子:
#include <iostream>
using namespace std;
class Base1 {
public:
void display() {
cout << "Base1::display()" << endl;
}
};
class Base2 :public Base1 {
public:
void display() {
cout << "Base2::display()" << endl;
}
};
class Deriver:public Base2 {
public:
void display() {
cout << "Deriver::display()" << endl;
}
};
void func(Base1 *ptr) {
ptr->display();
}
int main(void)
{
Base1 base1;
Base2 base2;
Deriver deriver;
func(&base1);
func(&base2);
func(&deriver);
return 0;
}
程序运行结果:
Base1::display()
Base1::display()
Base1::display()
请按任意键继续. . .
程序运行的结果并没有像我们预想的那样,想要输出指定的显示函数。这是因为编译器在编译阶段无法判断指针指向对象是什么类型,所以参数指针是什么类型它就指向那个类指向的成员函数,想要达到目的,就需要告诉编译器,在编译阶段告诉编译器要推迟判断,留到运行时再去判断具体指针类型,这样就能判断指针指向的具体对象是什么。怎么做呢:使用
virtual
关键字,使用virtual
关键字,不能把函数体写在类体内作为内联函数。因为内联函数在编译阶段就会被编译,我们需要做的是推迟对函数体的判断,这样矛盾,所以虚函数函数体需要在类外进行声明。
修改后的代码:
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display();
};
void Base1::display() {
cout << "Base1::display()" << endl;
}
class Base2 :public Base1 {
public:
virtual void display();
};
void Base2::display() {
cout << "Base2::display()" << endl;
}
class Deriver:public Base2 {
public:
virtual void display();
};
void Deriver::display() {
cout << "Deriver::display()" << endl;
}
void func(Base1 *ptr) {
ptr->display();
}
int main(void)
{
Base1 base1;
Base2 base2;
Deriver deriver;
func(&base1);
func(&base2);
func(&deriver);
return 0;
}
程序运行效果:
Base1::display()
Base2::display()
Deriver::display()
请按任意键继续. . .
初识虚函数
- 用
virtual
关键字说明的函数- 虚函数是实现运行时多态性的基础
C++中
的虚函数是动态绑定的函数- 虚函数必须是非静态成员函数,虚函数经过派生之后,就可以实现运行过程中的多态
什么函数可以是虚函数
- 一般成员函数可以是虚函数
- 构造函数不能是虚函数
- 析构函数可以是虚函数
一般虚函数成员
- 虚函数的声明
virtual
函数类型函数名(形参表)- 虚函数声明只能出现在类定义中的函数原型声明中而不能在成员函数实现的时候
- 在派生巻中可以对基类中的成员函数进行覆盖。
- 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的
virtual关键字
- 派生类可以不显式地用
virtual
声明虚函数,这时系统就会用以下规则来判断派生类的一个函数成员是不是虚函数
- 该函数是否与基类的虚函数有相同的名称、参数个数及对应参数类型
- 该函数是否与基类的虚函数有相同的返回值或者满足类型兼容规则的指针引用型的返回值
- 如果从名称、参数及返回值三个方面检查之后,派生类的函数满足上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数
派生类中的虚函数还会隐藏基类中同名函数的所有其它重載形式。- 一般习惯于在派生类的函数中也使用
virtual
关键字,以增加程序的可读性
虚析构函数
如果你打算允许其他人通过基类指针调用对象的析构函数(通过 deletel
这样做是正常的),就需要让基类的析构函数成为虚函数,否则执行 delete
的结果是不确定的。
示例代码:
#include <iostream>
using namespace std;
class Base {
public:
~Base();//不是虚函数
};
Base::~Base() {
cout << "Base destructor" << endl;
}
class Deriver:public Base {
public:
Deriver();
~Deriver();//不是虚函数
private:
int *p;
};
Deriver::Deriver() {
p = new int(0);
}
Deriver::~Deriver() {
cout << "Deriver destructor" << endl;
delete p;
}
void fun(Base *b) {
delete b;//静态绑定,只会调用~Base()
}
int main(){
Base *b = new Deriver();
fun(b);
return 0;
}
代码运行效果:
Base destructor
请按任意键继续. . .
修改后的代码:
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base();//不是虚函数
};
Base::~Base() {
cout << "Base destructor" << endl;
}
class Deriver:public Base {
public:
Deriver();
virtual ~Deriver();//不是虚函数
private:
int *p;
};
Deriver::Deriver() {
p = new int(0);
}
Deriver::~Deriver() {
cout << "Deriver destructor" << endl;
delete p;
}
void fun(Base *b) {
delete b;//静态绑定,只会调用~Base()
}
int main(){
Base *b = new Deriver();
fun(b);
return 0;
}
代码运行效果:
Deriver destructor
Base destructor
请按任意键继续. . .
程序运行时已经没有编译器了,那么怎样实现的程序的动态绑定呢?其实这个工作,还是编译器为我们完成的,使用了虚表
虚表
- 每个多态类有一个虚表(
virtual table
)- 虚表中有当前类的各个虚函数的入口地址
- 每个对象有一个指向当前类的虚表的指针(虚指针
vptr
)动态绑定的实现
- 构造函数中为对象的虚指针赋值
- 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到所调用的虚函数的入口地址
- 通过该入口地址调用虚函数
实现示意图
抽象类
纯虚函数
- 纯虚函数是一个在基类中声明的虚函数,它在该基类中没有定义
具体的操作内容,要求各派生类根据实际需要定义自己的版本
纯虚函数的声明格式为
virtual
函数类型型函数名(参数表)=0
- 抽象类语法
带有纯虚函数的类称为抽象类
class 类名 { virtual 类型 函数名(参数表)=0; //其他成员 }
抽象类作用
- 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
- 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。
注意
- 抽象类只能作为基类来使用。
- 不能定义抽象类的对象。
代码例程
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void display()=0;
};
class Base2 :public Base1 {
public:
virtual void display();
};
void Base2::display() {
cout << "Base2::display()" << endl;
}
class Deriver :public Base2 {
public:
virtual void display();
};
void Deriver::display() {
cout << "Deriver::display()" << endl;
}
void func(Base1 *ptr) {
ptr->display();
}
int main(void)
{
Base2 base2;
Deriver deriver;
func(&base2);
func(&deriver);
return 0;
}
运行结果:
Base2::display()
Deriver::display()
请按任意键继续. . .
override
- 多态性为的基础:基类声明虚函数,派生类声明一个函数覆盖该函数
- 覆盖要求:函数签名(
signature
)完全一致- 函数签名包括:
函数名 参数列表 const
C++11
引入显式函数覆盖,在编译期而非运行期捕获此类错误- 在虚函数显式重载中运用,编译器会检查基类是否存在一虚拟函数,与派生类中带有声明
override
的虚拟函数有相同的函数签名(signature
),若不存在则会回报错误final
- 指明对象不能被继承修改