• 虚函数


    可用于实现多态公有继承的机制:

    • 在派生类中重新定义基类的方法。
    • 使用虚方法

    类Brass和类BrassPlus如下所示:

    class Brass
    {
    private:
    char fullName[MAX];
    long acctNum;
    double balance;
    public:
    virtual void WithDraw(double amt);
    virtual void ViewAcct()const;
    virtual ~Brass(){}
    };
    class BrassPlus: public Brass
    {
    private:
    double rate;
    public:
    virtual void ViewAcct()const;
    virtual void WithDraw(double amt);
    };

    如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。如果ViewAcct()不是虚拟的,则程序的行为如下:

    Brass dom("Dominic Banker",11224,4183.45);
    BrassPlus dot("Dorothy Banker",12118,2592.00);
    Brass & b1_ref = dom;
    Brass & b2_ref = dot;
    b1_ref.ViewAcct();//调用Brass::ViewAcct()
    b2_ref.ViewAcct();//调用Brass::ViewAcct()

    如果ViewAcct是虚拟的,则行为如下:

    1 Brass dom("Dominic Banker",11224,4183.45);
    2 BrassPlus dot("Dorothy Banker",12118,2592.00);
    3 Brass & b1_ref = dom;
    4 Brass & b2_ref = dot;
    5 b1_ref.ViewAcct();//调用Brass::ViewAcct()
    6 b2_ref.ViewAcct();//调用BrassPlus::ViewAcct()

    用途:

    假设要同时管理Brass和BrassPlus账户,我们无法用同一个数组来保存Brass和BrassPlus对象,但是我们可以创建一个指向Brass的指针数组。这样,每个元素的类型都相同,而且Brass指针既可以纸箱Brass对象,也可以纸箱BrassPlus对象。因此,可以用一个数组来表示多种类型的对象。这就是多态性。

     虚函数的工作原理:

    编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(vtbl)。虚函数表中存储了为类对象声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则虚函数的地址也将被添加到vtbl中。注意,无论类中包含多少个虚函数,都只要再对象中添加1个地址成员,只是表的大小不同而已。

    使用虚函数时,在内存和执行速度方面有一定的成本,包括:

    • 每个对象都将增大,增大量为存储地址的空间。
    • 对每个类,编译器都创建一个虚函数地址表。
    • 每个函数调用都需要执行一步额外的操作,即到表中查找地址。

    虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

    虚函数要点:

    • 在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚拟的。
    • 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而使用为引用或指针类型定义的方法。这称为动态联编。这种行为使得基类指针或引用可以指向派生类对象。
    • 如果定义的类将被用做基类,则应将那些要在派生类中重新定义的类方法声明为虚函数。

    注意事项:

    1. 构造函数不能是虚函数。
    2. 析构函数应当是虚函数,除非类不用做基类。
    3. 友元不能是虚函数,因为友元不是类成员,而只有成员才能使虚函数。
    4. 如果派生类没有重新定义函数,将使用该函数的基类版本。
    5. 重新定义隐藏方法。

    *Tip:通常应该给基类提供一个虚拟析构函数,即使它并不需要析构函数。

    析构函数应当是虚函数,除非类不用做基类。例如假设给BrassPlus添加一个char*成员,该成员指向由new分配的内存。当BrassPlus对象过期时,必须调用~BrassPlus析构函数来释放内存。

    请看下面的代码:

    Brass* p = new BrassPlus;
    delete p;

    delete语句将调用~Brass()析构函数。这将释放由BrassPlus对象中的Brass部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚拟的,则上述代码将先调用~BrassPlus析构函数释放由BrassPlus组件指向的内存,然后调用~Brass()析构函数来释放由Brass组件指向的内存。这意味着,即使基类不需要显示析构函数提供服务,也不应该依赖于默认构造函数,而应提供虚拟析构函数,即使它不执行任何操作:

    virtual ~Brass(){}

    给类定义一个虚拟析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。

     *Tip:重新定义隐藏方法

    假设创建了如下所示的代码:

    class Dwelling
    {
    public:
    virtual void showperks(int a)const;
    ...
    };
    class Hovel: public Dwelling
    {
    public:
    virtual void showperks()const;
    ...
    };

     这可能会出现类似于下面这样的警告:

    Warning: Hovel::showperks(void) hides Dwelling::showperks(int)

    也可能不会出现警告。但不管结果如何,代码将具有如下含义:

    Hove trump;
    trump.showperks();//valid
    trump.showperks(5);//invalid

    重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。也就是说重新定义方法并不是重载。派生类重新定义函数,将不是使用相同的函数特征覆盖基类声明,而是隐藏同名的基类函数,不管函数的参数特征如何。

    这引出了两条经验规则:

    第一,如果重新定义继承的方法,应确保与原来的原型完全相同。但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特征被称为返回类型协变。如:

    class Dwelling
    {
    public:
    virtual Dweling& build(int n);
    ...
    };
    class Hovel: public Dwelling
    {
    public:
    virtual Hovel & build(int n);
    ...
    };

     注意,这种例外只适用于返回值,而不适用于参数。

    第二,如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。如:

    class Dwelling
    {
    public:
    virtual void showperks(int a)const;
    virtual void showperks(double x)const;
    virtual void showperks()const;
    ...
    };
    class Hovel: public Dwelling
    {
    public:
    virtual void showperks(int a)const;
    virtual void showperks(double x)const;
    virtual void showperks()const;
    ...
    };

     如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用它们。如果不需要修改,则新定义可调用基类版本。

  • 相关阅读:
    LeetCode 295. Find Median from Data Stream (堆)
    LeetCode 292. Nim Game(博弈论)
    《JavaScript 模式》读书笔记(4)— 函数2
    《JavaScript 模式》读书笔记(4)— 函数1
    《JavaScript 模式》读书笔记(3)— 字面量和构造函数3
    《JavaScript 模式》读书笔记(3)— 字面量和构造函数2
    《JavaScript 模式》读书笔记(3)— 字面量和构造函数1
    《JavaScript 模式》读书笔记(2)— 基本技巧3
    《JavaScript 模式》读书笔记(2)— 基本技巧2
    《JavaScript 模式》读书笔记(2)— 基本技巧1
  • 原文地址:https://www.cnblogs.com/happygirl-zjj/p/4641810.html
Copyright © 2020-2023  润新知