1.背景:
用qt实验,新建一个最简单的qt工程,只要一个main.cpp的那种。把下面代码复制过去全部覆盖,运行即可。注释完整,直接说明非虚函数、虚函数、纯虚函数的应用特性。
我的看法是从使用角度出发。与很多大师说的不一样,有虚函数的地方才不会多态,没有虚函数才多态。也许这不严谨,但容易懂。这句话的前提是,只看父类和子类之间。也就是在继承方向上纵向看待。
纵向看虚函数不是多态的。根据声明类型的不同(基类或者子类),非虚函数可以有不同的功能。虚函数默认以子类定义的为准。
横向看虚函数是多态的。按基类声明一个对象,它可以根据各子类的定义,自动决定要干什么。
2.打个比方:
基类是动物,派生出各种具体动物的子类,有猫、兔、泥鳅等。
2.1.如果基类“动物”里“吃”是个非虚函数:
如果各子类中不特别说明(没有重写),就永远是个模糊的概念。这叫继承。
如果各子类特殊说明了(重写了),那就每种动物自己说了算,猫吃肉,兔吃草,泥鳅吃泥巴。这叫重写覆盖。
在非虚函数 重写的情况下:
如果对着一只兔子说,这只动物要吃饭,那么还是一种模糊的概念。这叫用基类声明子类,以基类为准。
如果对着一只兔子说,这只兔子要吃饭,那就是吃草。这叫用子类声明子类,以子类为准。
2.2.如果基类“动物”里“吃”是个虚函数:
如果各子类中不特别说明(没有重写),就是个模糊的概念。还是叫继承。
如果各子类特殊说明了(重写了),那就每种动物自己说了算,猫吃肉,兔吃草,泥鳅吃泥巴。这叫虚函数重写。从此时起,基类的虚函数就“虚”了,它只是个基本要求,比如放进嘴巴、咀嚼、咽下。
在虚函数重写的情况下:
无论称呼兔子为动物还是兔子,它都是吃草。以子类为准。除非就故意不明确地说让它吃饭,这就是显式调用基类函数。
2.3.如果基类“动物”里“吃”是个纯虚函数:
相当于基类“动物”的“吃”只是个口号,只是告诉你动物要吃饭,但没有任何指示。所以每一种动物必须说自己吃什么怎么吃(重写),且以子类重写的为准。否则等于没有定义,编译报错。
无论称呼兔子为动物还是兔子,它就必须吃草。
3.代码:
/* 非虚函数:
* 如果子类实现,相当于重写,父类指针调用父类实现,子类指针调用子类实现。
* 如果子类不实现,无论怎样都是调用的父类实现。
*
* 虚函数:(父类声明为virtual,且需要实现。) *
* 如果子类实现,相当于重写覆盖。无论怎样,调用的都是子类实现,所以父类才叫虚函数。
* 如果子类不实现,无论怎样,调用的都是父类实现。
*
* 纯虚函数:(父类声明为virtual,且不用实现。) *
* 如果子类实现,相当于重写覆盖。无论怎样,调用的都是子类实现,所以父类才叫虚函数。
* 其实这点与“虚函数”相同,没任何区别。
* 如果子类不实现,编译报错。
*/
#include <QApplication>
#include <QDebug>
//父类
class Parent
{
public:
//非虚函数:
void f_childhas() { qDebug() << "base " << __FUNCTION__; }
void f_childnone() { qDebug() << "base " << __FUNCTION__; }
//虚函数:
virtual void f_virtual_childhas() { qDebug() << "base " << __FUNCTION__; }
virtual void f_virtual_childnone() { qDebug() << "base " << __FUNCTION__; }
//纯虚函数:
virtual void f_virtual_pure() = 0;
};
//子类
class Child : public Parent
{
public:
//非虚函数重写:
void f_childhas() { qDebug() << "child" << __FUNCTION__; }
//虚函数实现(重写):
void f_virtual_childhas() { qDebug() << "child" << __FUNCTION__; }
//纯虚函数实现:如果注释掉这里,编译报错。
void f_virtual_pure() { qDebug() << "child" << __FUNCTION__; }
};
int main(int argc, char *argv[])
{
Q_UNUSED(argc)
Q_UNUSED(argv)
Parent *c1 = new Child;
c1->f_childhas();
c1->f_childnone();
c1->f_virtual_childhas();
c1->f_virtual_childnone();
c1->f_virtual_pure();
qDebug() << "-------------------------";
Child *c2 = new Child;
c2->f_childhas();
c2->f_childnone();
c2->f_virtual_childhas();
c2->f_virtual_childnone();
c2->f_virtual_pure();
return 0;
}
main函数有效代码10行,一个子类,分别用父类和子类的类型来声明它,而后调用其函数。
而这些函数无非就三种:非虚函数,虚函数,纯虚函数。
这里不用猜,直接看结果:
Debugging starts
base f_childhas
base f_childnone
child f_virtual_childhas
base f_virtual_childnone
child f_virtual_pure
-------------------------
child f_childhas
base f_childnone
child f_virtual_childhas
base f_virtual_childnone
child f_virtual_pure
Debugging has finished
3.1.非虚函数继承:
子类不用管它,直接调用就是继承父类的行为。
3.2.非虚函数重写:
子类实现就会直接覆盖父类实现,会根据声明类型的不同,调用了父类或者子类的函数。我认为这才叫使用角度的多态。
3.3.虚函数继承:
子类不用管它,相当于继承。
3.4.虚函数重写:
子类实现就会直接覆盖父类实现,无论声明类型怎样,都是调用子类重写的函数。所以父类才叫它“虚”,父类的实现默认没用。相当于子类造次,一旦父类虚,就是子类说了算。如果要实现父类的行为,必须显式调用。
3.5.纯虚函数继承:
不存在的,子类必须实现,否则报错。
3.6.纯虚函数重写:
与虚函数重写基本一样没区别。但因为父类没有实现行为,所以不能显式调用,也没意义。
4.使用场景和意义:
4.1.非虚函数:
仅仅是传统的用法,派生类里可以定义与基类同名的函数,进行扩展和改造。
4.2.虚函数:
具备非虚函数的特征以外,为基类提供了灵活功能。比如写基类的时候,可以在不知道派生类会定义什么功能的情况下,安排这个函数运行的时机。
4.3.纯虚函数:
在具备虚函数的特征以外,增加了强制限定,必须要求派生类重写这个函数,否则编译不通过。
5.完结:
当然,上面的描述不严谨。因为我是在继承关系上纵向看待。这里不是要纠结概念和理论,仅仅在使用角度考虑,用不用virtual,是否重写,会有什么效果,仅此而已。