简洁明了,十行代码,从使用角度理解c++非虚函数、虚函数、纯虚函数、重写、多态

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,是否重写,会有什么效果,仅此而已。

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
下面是一个使用C++虚函数实现多态代码示例: ```cpp #include <iostream> using namespace std; class A { public: virtual void foo() { cout << "A::foo()" << endl; } virtual ~A() { cout << "~A()" << endl; } }; class B : public A { public: void foo() { cout << "B::foo()" << endl; } ~B() { cout << "~B()" << endl; } }; int main() { A* p = new B(); p->foo(); delete p; return 0; } ``` 在这个示例中,类A和类B都有一个名为foo()的虚函数。当我们通过基类指针p调用foo()函数时,实际上会根据指针所指向的对象的类型来确定调用哪个类的foo()函数。这就是多态的实现原理。 输出结果为: ``` B::foo() ~B() ~A() ``` 可以看到,通过虚函数实现的多态,调用的是派生类B的foo()函数,而不是基类A的foo()函数。同时,析构函数也是虚函数,确保在删除指针p时,会先调用派生类B的析构函数,再调用基类A的析构函数。这是因为在C++中,如果基类的析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。因此,为了确保正确的析构顺序,析构函数应该声明为虚函数。 请注意,虚函数只能在类的成员函数中声明,构造函数不能声明为虚函数。这是因为在创建对象时,需要先调用构造函数来初始化对象,而虚函数的机制是在运行时根据对象的类型来确定调用哪个函数,而构造函数在对象创建时就已经确定了。因此,构造函数不能声明为虚函数。 #### 引用[.reference_title] - *1* [虚函数实现多态原理](https://blog.csdn.net/qq_24309981/article/details/89102183)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [C++虚函数多态实现](https://blog.csdn.net/qq_27576655/article/details/124535530)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值