• C++学习之虚函数与纯虚函数


    面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承、动态绑定通过数据抽象,可以使类的接口与实现分离,使用继承,可以更容易地定义与其他类相似但不完全相同的新类,使用动态绑定,可以在一定程度上忽略相似类的区别,而以统一的方式使用它们的对象。

    虚函数的作用是实现多态性(Polymorphism),多态性是将接口与实现进行分离,采用共同的方法,但因个体差异而采用不同的策略。纯虚函数则是一种特殊的虚函数。虚函数联系到多态,多态联系到继承。所以本文中都是在继承层次上做文章。没了继承,什么都没得谈。

    一、虚函数

    1 . 定义

    在C++中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通过在函数之前加上virtual关键字将其定义为虚函数(virtual)

    class Base{  // 基类
    public:
    	virtual int func(int n) const;
    };
    
    class Derive_Class : public Base{
    public:
    	int func(int n) const;  // 默认也为虚函数
    };
    当我们在派生类中覆盖某个函数时,可以在函数前加virtual关键字。然而这不是必须的,因为一旦某个函数被声明成虚函数,则所有派生类中它都是虚函数。任何构造函数之外的非静态函数都可以是虚函数。派生类经常(但不总是)覆盖它继承的虚函数,如果派生类没有覆盖其基类中某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。

    2 . 动态绑定

    当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定(dynamic binding)。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,可能是基类中的版本也可能是派生类中的版本,判断的依据是引用(或指针)所绑定的对象的真实类型。与非虚函数在编译时绑定不同,虚函数是在运行时选择函数的版本,所以动态绑定也叫运行时绑定(run-time binding)

    3 . 静态类型与动态类型

    静态类型指的是变量声明时的类型或表达式生成的类型,它在编译时总是已知的;动态类型指的是变量或表达式表示的内存中的对象的类型,它直到运行时才可知。当且仅当通过基类的指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致。

    4 . final和override

    派生类中如果定义了一个函数与基类中虚函数同名但形参列表不同,编译器会认为这是派生类新定义的函数。如果我们的意图本是覆盖虚函数,则这种错误很难发现。通过在派生类中的虚函数最后加override关键字使得意图更加清晰。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在的虚函数,编译器将报错。

    class Base{  // 基类
    public:
    	virtual int func(int a, int b) const;
    };
    
    class Derive_Class : public Base{
    public:
    	int func(int a) const override;  // 报错,没有覆盖虚函数
    };
    如果我们定义一个类,并不希望它被继承。或者希望某个函数不被覆盖,则可以把类或者函数指定为final,则之后任何尝试继承该类或覆盖该函数的操作将引发错误。

    class Base final { /*   */ };     // 基类不能被继承
    class Derive_Class : public Base { /*  */ };     // 报错
    
    void func(int) const final;   // 不允许后续的其他类覆盖func(int) 

    5 . 回避虚函数的机制

    在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。可以使用作用域运算符实现这一目的。

    // 强行调用基类中定义的函数版本而不管baseP的动态类型是什么
    int a = baseP->Base::func(42);

    如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

    二、纯虚函数

    1 . 定义

    为了方便使用多态特性,我们常常需要在基类中定义虚函数。在许多情况下,在基类中不能对虚函数给出有意义的实现。为了让虚函数在基类什么也不做,引进了“纯虚函数”的概念,使函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数(pure virtual)。其中,=0只能出现在类内部的虚函数声明语句处:

    class Base{  // 抽象基类
    public:
    	virtual int func(int n) const =0;
    };

    需要注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。

    2 . 抽象基类

    含有(或者未经覆盖直接继承)纯虚函数的类叫抽象基类(abstract base class)。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象基类。因为抽象基类含有纯虚函数(没有定义),所以我们不能创建一个抽象基类的对象,但可以声明指向抽象基类的指针或引用。

    Base base;   // 错误,不能实例化抽象基类
    


    总结:

    1. 虚函数必须实现,不实现编译器会报错。
    2. 父类和子类都有各自的虚函数版本。由多态方式在运行时动态绑定。
    3. 通过作用域运算符可以强行调用指定的虚函数版本。
    4. 纯虚函数声明如下:virtual void funtion()=0; 纯虚函数无需定义。包含纯虚函数的类是抽象基类,抽象基类不能创建对象,但可以声明指向抽象基类的指针或引用。
    5. 派生类实现了纯虚函数以后,该纯虚函数在派生类中就变成了虚函数,其子类可以再对该函数进行覆盖。
    6. 析构函数通常应该是虚函数,这样就能确保在析构时调用正确的析构函数版本。


  • 相关阅读:
    项目中遇到的IE8浏览器访问页面过慢问题
    linux下安装Oracle时交换空间不足的解决方法
    linux下为目录和文件设置权限
    启动TDS LDAP 服务器遇到的问题总结
    sql1032n sql6048n db2start启动不了 db2修改hostname
    IBM CE 错误集之(FNRCS0005E)
    Oracle同一个用户下启动多个数据库实例
    几种任务调度的 Java 实现方法与比较
    EXP-00056: ORACLE error 12154 encountered
    oracle删除数据库中的所有表
  • 原文地址:https://www.cnblogs.com/songlee/p/5738135.html
Copyright © 2020-2023  润新知