调用继承函数与覆写行为

默认情况下,派生类会继承基类中定义的全部行为。本节将更详细地考察成员函数的选择机制,以及如何利用这一机制在派生类中改变行为。

当在派生类对象上调用某成员函数时,编译器首先查找该派生类中是否存在同名函数。若存在,则考虑所有同名重载函数,并通过函数重载解析过程确定最佳匹配;若不存在,则编译器沿继承链逐级向上,以相同方式检查每一个父类。

换言之,编译器将在“包含至少一个同名函数的最派生类”中选择最佳匹配函数。

调用基类函数

首先探讨当派生类无匹配函数而基类有匹配函数时的情形:

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

输出:

Base::identify()
Base::identify()

调用 base.identify() 时,编译器在 Base 中找到名为 identify 的函数,经匹配无误后予以调用。

调用 derived.identify() 时,编译器先在 Derived 类中查找同名函数,未找到;于是转向父类 Base,找到 identify 函数并使用之。简言之,因 Derived::identify() 不存在,故使用 Base::identify()

这意味着,若基类提供的行为已满足需求,可直接复用基类行为。

重定义行为

若我们在 Derived 类中定义了 Derived::identify(),则该版本将被优先选用。

因此,通过在派生类中重新定义同名函数,即可使派生类表现出不同的行为。

示例:希望 derived.identify() 打印 Derived::identify(),只需在 Derived 类中添加同名函数:

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const { std::cout << "Derived::identify()\n"; }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

输出:

Base::identify()
Derived::identify()

注意:在派生类中重定义函数时,该函数不会继承基类同名函数的访问说明符,而是使用派生类中定义的访问说明符。因此,基类中为 private 的函数可在派生类中重定义为 public,反之亦然。

示例:

#include <iostream>

class Base
{
private:
    void print() const
    {
        std::cout << "Base";
    }
};

class Derived : public Base
{
public:
    void print() const
    {
        std::cout << "Derived ";
    }
};

int main()
{
    Derived derived {};
    derived.print(); // 调用 Derived::print(),其为 public
    return 0;
}

在既有功能基础上进行扩展

有时我们并不想完全替换基类函数,而希望在调用派生类对象时为其增加额外功能。上述示例中,Derived::identify() 完全隐藏了 Base::identify()。若这不是我们期望的行为,可让派生函数先调用同名基类函数(以复用代码),再补充新功能。

若要在派生函数中调用同名基类函数,只需在函数名前加基类作用域限定符即可:

#include <iostream>

class Base
{
public:
    Base() { }

    void identify() const { std::cout << "Base::identify()\n"; }
};

class Derived: public Base
{
public:
    Derived() { }

    void identify() const
    {
        std::cout << "Derived::identify()\n";
        Base::identify(); // 显式调用 Base::identify()
    }
};

int main()
{
    Base base {};
    base.identify();

    Derived derived {};
    derived.identify();

    return 0;
}

输出:

Base::identify()
Derived::identify()
Base::identify()

执行 derived.identify() 时,实际调用 Derived::identify();打印 Derived::identify() 后,再调用 Base::identify() 打印 Base::identify()

为何必须使用作用域解析运算符 ::?若写成:

void identify() const
{
    std::cout << "Derived::identify()\n";
    identify(); // 无作用域限定,导致自调用并无限递归
}

未加作用域限定符时,默认调用当前类的 identify(),即 Derived::identify(),从而导致无限递归。

当需要调用基类的友元函数(如 operator<<)时,会略有技巧:基类友元并非基类成员,使用作用域限定符无效。解决方案是将派生类对象临时转换为基类引用,以便调用正确的函数版本。借助 static_cast 即可实现:

#include <iostream>

class Base
{
public:
    Base() { }

    friend std::ostream& operator<< (std::ostream& out, const Base&)
    {
        out << "In Base\n";
        return out;
    }
};

class Derived: public Base
{
public:
    Derived() { }

    friend std::ostream& operator<< (std::ostream& out, const Derived& d)
    {
        out << "In Derived\n";
        // 将 Derived 对象 static_cast 为 Base 引用,以调用正确的 operator<<
        out << static_cast<const Base&>(d);
        return out;
    }
};

int main()
{
    Derived derived {};
    std::cout << derived << '\n';
    return 0;
}

由于 Derived 是一种 Base,可将 Derived 对象 static_cast 为 Base 引用,从而调用形参为 Base 的 operator<<

输出:

In Derived
In Base

派生类中的重载解析

如本节开头所述,编译器将在“包含至少一个同名函数的最派生类”中选择最佳匹配。

首先考察一个简单的重载示例:

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
};

int main()
{
    Derived d {};
    d.print(5); // 调用 Base::print(int)
    return 0;
}

调用 d.print(5) 时,编译器在 Derived 中未发现 print 函数,于是检查 Base,发现两个同名函数,通过重载解析确定 Base::print(int) 为最佳匹配,故调用之。

接下来观察一个可能出乎意料的情况:

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    void print(double) { std::cout << "Derived::print(double)"; }
};

int main()
{
    Derived d {};
    d.print(5); // 调用 Derived::print(double),而非 Base::print(int)
    return 0;
}

调用 d.print(5) 时,编译器在 Derived 中找到名为 print 的函数,因此仅考虑 Derived 中的候选函数。该函数亦为最佳匹配,故调用 Derived::print(double)

尽管 Base::print(int) 的参数类型与实参 5 更匹配,但因 d 是 Derived 类型,且 Derived 中存在 print 函数,Derived 比 Base 更派生,Base 中的函数因而被忽略。

若确需 d.print(5) 解析为 Base::print(int),一种不甚理想的方法是在 Derived 中定义 Derived::print(int)

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    void print(int n) { Base::print(n); } // 可行但欠佳,每需转发一个重载就得添加一个函数
    void print(double) { std::cout << "Derived::print(double)"; }
};

int main()
{
    Derived d {};
    d.print(5); // 调用 Derived::print(int),其内部调用 Base::print(int)
    return 0;
}

该方法可行,但每想透传一个重载就必须在 Derived 中新增一个转发函数,冗长且易出错。

更佳方案是使用 using 声明,将 Base 中同名函数全部引入 Derived:

#include <iostream>

class Base
{
public:
    void print(int)    { std::cout << "Base::print(int)\n"; }
    void print(double) { std::cout << "Base::print(double)\n"; }
};

class Derived: public Base
{
public:
    using Base::print; // 使所有 Base::print() 函数在 Derived 中可见
    void print(double) { std::cout << "Derived::print(double)"; }
};

int main()
{
    Derived d {};
    d.print(5); // 调用 Base::print(int),因其是 Derived 中可见的最佳匹配
    return 0;
}

通过在 Derived 中添加 using 声明 using Base::print;,我们告知编译器:所有名为 print 的 Base 函数在 Derived 中均可见,从而参与重载解析。结果,Base::print(int) 被选中,而非 Derived::print(double)

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"