C++中虚函数,纯虚函数以及多态
在整理排序算法的时候想到可以借此熟悉一下类的构造以及继承等知识点,就写了一个排序基类声明一个排序函数,然后写多个排序算法的子类重写这个排序函数,过程中顺便研究了一下virtual和多态的概念。
虚函数和纯虚函数
首先回顾一下虚函数和纯虚函数的八股文,虚函数是多态的实现机制,声明一个虚函数就是在一个函数的声明(不需要定义)前加上virtual
,而纯虚函数则还要函数声明后加上=0
来表明纯虚函数身份,如果想要定义纯虚函数的实现,则必须在类外部进行定义,例子:
// 纯虚函数
virtual void A()=0;
// 虚函数
virtual void B(){cout<<"foo"<<endl;}
虚函数
先看虚函数,虚函数使得一个指向子类对象的基类类型的指针可以重写基类函数,当然各自类型的对象都是能实现本类的函数的,看一下例子:
class foo{
public:
// 虚函数
virtual void B(){cout<<"foo"<<endl;}
// 非虚函数
void C(){cout<<"FOO"<<endl;}
};
class bar : public foo {
public:
// 重写虚函数bar
void B(){cout<<"bar"<<endl;}
// 重写非虚函数
void C(){cout<<"BAR"<<endl;}
};
如果通过创建两个类的对象调用函数:
int main() {
foo a;
bar b;
a.B(); // 输出 foo
a.C(); // 输出 FOO
b.B(); // 输出 bar
b.C(); // 输出 BAR
}
但如果利用指针来调用函数:
int main() {
foo a;
bar b;
foo *p = &a;
p->B(); // 输出 foo
p->C(); // 输出 FOO
p = &b;
p->B(); // 输出 bar
p->C(); // 输出 FOO
}
也就是说如果是通过指向子类对象的基类类型指针来调用函数,可以调用子类重写的虚函数,但非虚函数还是会调用基类本身的。然后这里注意一下,指针用->
调用函数。
虚函数和多态
在我的理解中,多态分为编译时多态和运行时多态,编译时多态的话主要就是指方法的重载,即多个同名不同参的函数,而运行时多态基本上就是指父类指针指向子类对象的情况。
多态的实现依赖于动态绑定技术,而动态绑定技术的核心是虚函数表。一个类如果包含虚函数,就会同时有一张对应的虚函数表,虚函数表里存储了指向各个虚函数的指针,但非虚函数不会被包含在虚函数表里。如果一个类的子类也包含虚函数表,那么子类中的虚函数表会指向子类的虚函数,也就是说如果子类重写了基类的虚函数,那么子类的虚函数表内指向该函数的指针就会指向子类所写的虚函数,若没有重写,那么就指向基类的虚函数。
如图:
图片来源
上图中B是A的子类,C是B的子类,B重写了vfunc1(),C改写了vfunc2()。
纯虚函数
包含纯虚函数的类叫做纯虚类,纯虚类是不能创建对象的,必须要创建子类通过子类创建对象:
class foo{
public:
foo(){cout<<"foo constructor"<<endl;}
~foo(){cout<<"foo destructor"<<endl;}
// 纯虚函数
virtual void A()=0;
// 虚函数
virtual void B(){cout<<"foo"<<endl;}
void C(){cout<<"FOO"<<endl;}
};
class bar : public foo {
public:
bar(){cout<<"bar constructor"<<endl;}
~bar(){cout<<"bar destructor"<<endl;}
// 重写纯虚函数
void A(){cout<<"override"<<endl;}
// 重写虚函数bar
void B(){cout<<"bar"<<endl;}
// 重写非虚函数
void C(){cout<<"BAR"<<endl;}
};
假如有以上两个类,当我们为两个类创建对象时,要注意:
- 不可以直接为foo类创建对象,比如
foo a;
就会报错,因为foo是一个纯虚类不能直接创建对象,但是我们可以创建一个foo类的指针,因为指针有可能指向foo的子类bar对象,比如foo *a = new bar;
这是被允许的。 - 在bar子类里,必须重写纯虚函数A,但B可以不重写,因为基类已经提供了B函数的实现。
这里是如何体现多态的呢,包括以下两个方面:
-
子类如果不提供虚函数的实现,将会自动调用基类的虚函数实现,若提供虚函数的实现,就调用子类的虚函数实现;
-
子类如果不提供纯虚函数的实现,编译将会失败,因为是不能通过指向子类对象的基类类型指针来调用纯虚函数的。
关于构造函数和析构函数
构造函数不能是虚函数,析构函数可以是虚函数且最好为虚函数。