• C++中的虚函数、重写与多态


    在C++中顺利使用虚函数需知道的细节

    • 如函数在派生类中的定义有别于基类中的定义,而且你希望它成为虚函数,就要为基类的函数声明添加保留字virtual。在派生类的函数声明中,则可以不添加virtual。函数在基类中virtual,在派生类中自动virtual(但为了澄清,最好派生类中也将函数声明标记为virtual,尽管这非必须)。
    • 保留字virtual在函数声明中添加,不要再函数定义中添加。
    • 除非使用保留字virtual,否则不能获得虚函数,也不能获得虚函数的任何好处。
    • 既然虚函数如此好用,为何不将所有成员函数都设为virtual?这似乎只有一个理由——效率。编译器和“运行时”环境要为虚函数做多得多的工作。所以,无谓地将成员函数为virtual会影响程序执行效率。

    重写

    虚函数定义在派生类中发生改变时我们说函数定义被重写。一些C++书籍区分了重定义(redefine)和重写(override)。两者都是在派生类更改函数定义。函数是虚函数,就称为重写。如果不是,就称为重定义。对于我们程序员而言,这种区分似乎有点无聊,因为程序员在两种情况下做的事情是一样的。不过,编译器对于这两种情况确定是区别对待的。

    多态

    多态性是指借助晚期绑定技术,为一个函数名关联多种含义的能力。因此,多态性、晚期绑定和虚函数其实是同一个主题。

    虚函数和扩展类型兼容性、切割问题

    #include <iostream>
    #include <string>
    using std::cout;
    using std::endl;
    using std::string;
    
    class Pet
    {
    public:
        virtual void print();
        string name;
    };
    
    class Dog : public Pet
    {
    public:
        virtual void print();
        string breed; // 品种
    };
    
    void Pet::print()
    {
        cout << "Pet name: " << name << endl;
    }
    
    void Dog::print()
    {
        cout << "Dog name: " << name << ", breed: " << breed << endl;
    }
    
    int main()
    {
        Pet vPet;
        Dog vDog;
        vDog.name = "Tiny";
        vDog.breed = "Great Dane";
        vPet = vDog;
        // cout << vPet.breed;
        return 0;
    }
    

    上述代码vPet = vDog;的赋值是允许的,但赋给变量vPet的值会丢失其breed字段。这称为切割问题(slicing problem)。例如,cout << vPet.breed会报错。

    切割问题:在将派生类对象赋给基类变量时,派生类对象有、基类没有的数据成员会在赋值过程中丢失,基类没有的成员函数也会丢失。在最终的基类对象中,将无法使用这些丢失的成员。

    切割测试:

    #include <iostream>
    #include <string>
    
    using std::cout;
    using std::endl;
    using std::string;
    
    class Demo
    {
    public:
        Demo(const string& s): str(s)
        {
            cout << "Demo constructor called (" + str + ").
    ";
        }
        ~Demo()
        {
            cout << "Demo deconstructor called (" + str + ").
    ";
        }
        Demo(const Demo& other)
        {
            str = other.str;
            cout << "Demo copy constructor called (" + str + ").
    ";
        }
        Demo& operator=(const Demo& other)
        {
            str = other.str;
            cout << "Demo operator= called (" + str + ").
    ";
            return *this;
        }
    private:
        string str;
    };
    
    class Base
    {
    public:
        Demo member1 = Demo("member1");
    };
    
    class Derived : public Base
    {
    public:
        Demo member2 = Demo("member2");
    };
    
    int main()
    {
        Derived derived;
        Base base;
        base = derived;
    }
    /* Output
    Demo constructor called (member1).
    Demo constructor called (member2).
    Demo constructor called (member1).
    Demo operator= called (member1).
    Demo deconstructor called (member1).
    Demo deconstructor called (member2).
    Demo deconstructor called (member1).
    */
    

    幸好,C++提供了一种方式,允许在将一个Dog视为Pet的同时不丢失品种名称:

    Pet *pPet;
    Dog *pDog;
    pDog = new Dog;
    pDog->name = "Tiny";
    pDog->breed = "Great Dane";
    pPet = pDog;
    pPet->print(); // prints "Dog name: Tiny, breed: Great Dane"
    

    基类Petprint()声明为virtual。所以一旦编译器看到pPet->print();就会检查PetDogvirtual表,判断pPet指向的是Dog类型的对象。因此,它会使用Dog::print(),而不是Pet::print()

    配合动态变量进行OOP是一种全然不同的编程方式。只要记住以下两条简单的规则,理解起来就容易得多。

    1. 如果指针pAncestor的域类型是指针pDescendant的域类型的基类,则以下指针赋值操作允许:pAncestor = pDescendant;。此外,pDescendant指向的动态变量的任何数据成员或成员函数都不会丢失。
    2. 虽然动态变量所有附加字段(成员)都没有丢,但要用virtual成员函数访问。

    视图对虚成员函数定义不齐全的类进行编译

    编译前,如果还有任何尚未实现的virtual成员函数,编译就会失败,并产生形如undefined reference to Class_Name virtual table的错误信息。即使没有派生类,只有一个virtual成员,并且没有调用该虚函数,只要函数没有定义,就会产生这种形式的消息。此外,可能还会产生进一步的错误消息,声称程序对默认构造函数进行了未定义的引用,即使确实已定义了这些构造函数。

    始终/尽量使析构函数成为虚函数(主要讲述把析构函数声明为虚函数的优点)

    这里主要阐述让析构函数称为虚函数的好处,但实际上也有坏处。在《Effective C++》条款07中有提到具体内容,见本文后记

    析构函数最好都是虚函数。但在解释它为什么好之前,首先解释一下析构函数和指针如何交互,以及虚析构函数的具体含义。如以下代码,其中SomeClass是含有非虚析构函数的类:

    SomeClass *p = new SomeClass;
    // ...
    delete p;
    

    p调用delete,会自动调用SomeClass类的析构函数,现在看看将析构函数标记为virtual之后会发生什么。为了描述析构函数与虚函数机制的交互,最简单的方式是将所有析构函数都视为同名(即使它们并非真的同名)。如假定Derived类是Base类的派生类,并假定Base类的析构函数标记为virtual,现在分析以下代码:

    Base *pBase = new Derived;
    // ...
    delete pBase;
    

    pBase调用delete时,会调用一个析构函数。由于Base类中的析构函数标记为virtual,且指向的对象是Derived类型,故会调用Derived的析构函数(它进而调用Base类的析构函数)。若Base类的析构函数没有标记为virtual,则只调用Base类的析构函数。

    还要注意一点,将析构函数标记为virtual后,派生类的所有析构函数都自动成为virtual的(不管是否用virtual标记)。同样,这种行为就好比所有析构函数具有相同的名称(即使事实上不同名)

    现在,已准备好解释为什么所有析构函数都应该是虚函数。假定Base类有一个指针类型的成员变量pBBase类的构造函数会创建由pB指向的一个动态变量,而Base类的析构函数会删除之;另外,假定Base类的析构函数没有标记为virtual,并假定Derived类(从Base派生)有一个指针类型的成员变量pDDerived类的构造函数会创建由pD指向的一个动态变量,而Derived类的析构函数会删除之。则以下代码

    Base *pBase = new Derived;
    // ...
    delete pBase;
    

    由于基类析构函数未标记为virtual,所以只会调用Base类的析构函数。这会将pB指向的动态变量的内存返还给自由存储;但pD指向的动态变量占用的内存永远不会返还给自由存储直到程序终止。

    另一方面,将基类Base析构函数标记为virtualdelete pBase;时会调用Derived类的析构函数(因为指向的对象是Derived类型)。Derived类的析构函数会删除pD指向的动态变量,再自动调用基类Base的析构函数删除pB指向的动态变量。

    测试代码:

    #include <iostream>
    
    class Base
    {
    public:
        Base()
        {
            baseData = new int;
            std::cout << "baseData allocated.
    ";
        }
        ~Base()
        {
            delete baseData;
            std::cout << "baseData deleted.
    ";
        }
    private:
        int *baseData;
    };
    
    class Derived : public Base
    {
    public:
        Derived()
        {
            derivedData = new int;
            std::cout << "derivedData allocated.
    ";
        }
        ~Derived()
        {
            delete derivedData;
            std::cout << "derivedData deleted.
    ";
        }
    private:
        int *derivedData;
    
    };
    
    int main()
    {
        Base *base = new Derived;
        delete base;
    }
    /* Output
    baseData allocated.
    derivedData allocated.
    baseData deleted.
    */
    

    将第11行的~Base()改为virtual ~Base(),程序输出为

    /* Output
    baseData allocated.
    derivedData allocated.
    derivedData deleted.
    baseData deleted.
    */
    

    后记

    参考:Walter Savitch《Problem Solving with C++, Tenth Edition》《Effective C++》。

    《Effective C++》条款07:“为多态基类声明virtual析构函数”中提到:

    • 带多态性质的基类应该声明一个virtual析构函数;如果类带有任何virtual函数,则它就应该拥有一个virtual析构函数。
    • 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。(如标准库input_iterator_tag等)
  • 相关阅读:
    java多线程
    java垃圾回收
    java研发常见问题总结 1
    js获取时间加多山天和时间戳转换成日期
    php时间选择器亲测可以自己修改
    html5时间选择器
    php生成员工编号,产品编号
    桌面远程链接
    SQL 左外连接查询 将右表中的多行变为左表的一列或多列
    PHPMailer发匿名邮件及Extension missing: openssl的解决
  • 原文地址:https://www.cnblogs.com/sandychn/p/12421961.html
Copyright © 2020-2023  润新知