【问题标题】:Why is it allowed to call derived class' private virtual method via pointer of base class?为什么允许通过基类的指针调用派生类的私有虚方法?
【发布时间】:2011-06-26 20:24:30
【问题描述】:
# include <iostream>
using namespace std;

class A
{
    public:
    virtual void f()
    {
        cout << "A::f()" << endl;
    }
};
class B:public A
{
    private:
    virtual void f()
    {
        cout << "B::f()" << endl;
    }
};
int main()
{
    A *ptr = new B;
    ptr->f();
    return 0;
}

此代码正常工作并打印 B::f()。我知道它是如何工作的,但为什么允许这段代码?

【问题讨论】:

    标签: c++ inheritance virtual access-specifier


    【解决方案1】:

    访问控制在编译时执行,而不是运行时。对f() 的调用通常无法知道ptr 指向的对象的运行时类型,因此不会检查派生类的访问说明符。这就是允许调用的原因。

    至于为什么允许使用私有函数覆盖 B 类 - 我不确定。当然 B 违反了从 A 继承的接口,但通常 C++ 语言并不总是强制接口继承,所以它只是完全错误的事实并不意味着 C++ 会阻止你。

    所以我猜这个类 B 可能有一些用例 - 替换仍然适用于动态多态性,但静态 B 不能替代 A(例如,可以有调用 f 的模板,这会起作用以 A 作为参数,但不以 B 作为参数)。在某些情况下,这正是您想要的。当然,这可能只是其他考虑的意外结果。

    【讨论】:

    • 这不违反 LSP,因为 B 对象可以在 A 可以使用的任何地方使用。这样的上下文会触发隐式 Derived-to-Base 转换,可能会切割额外的部分。经过这样的转换,B::f 被访问为A::f,这是公开的。
    • @MSalters:C++ 中有两种不同的替换,因为 C++ 中有(至少)两种不同的多态性。我们可以分别评估 LSP 是否适用于每个。如果您的 B 对象是例如在使用模板参数推导从模板实例化函数的函数调用中按值传递,那么它很可能不会被切片,并且确实会以B::f 访问f。基本上,B 并不满足 A 满足的所有概念,因此对象只有在类型被剥离后才可替换(例如,使用 A*,或转换为 A)。
    • LSP 是一种约束命名一致性的 OOP 原则。模板上下文不是普通的 OOP 上下文。它是通用编程,通常忽略继承。相反,它使用结构一致性。因此 OOP 的 LSP 原则不适用于模板参数。
    • 另外,仅仅因为 B 可转换为 A 仍然不会使 B 对象替代 C++ 中的 A 对象。类型 A 的对象可以传递给采用 A&amp; 的函数,或采用 C 的函数,其中 A 转换为 C。 B 类型的对象不能在没有显式转换的情况下传递给任何一个(并且它不会与传递的对象相同,除了A &amp;a_ref = b_obj)。
    • @MSalters:如果您将 LSP 限制为仅适用于动态多态性(指针或引用显式类型化为基类),那么可以,但如果您声称派生->基切片在任何方式相关,那么我认为模板推导也应该相关。但是,如果 Liskov 对静态多态性不感兴趣,我很可能在谈论它们时不应该妄称 Liskov 的名字。
    【解决方案2】:

    这个代码是允许的,因为 f 在 A 的接口中是公共的。派生类不能更改基类的接口。 (重写虚方法不会改变接口,也不会隐藏基类的成员,尽管两者都可以这样做。)如果派生类可以更改基类的接口,则违反"is a" relationship

    如果 A 的设计者想让 f 不可访问,则应将其标记为受保护或私有。

    【讨论】:

      【解决方案3】:

      您的基类正在为所有继承的子类定义接口。我不明白为什么它应该阻止提到的访问。您可以尝试从 'B' 向下派生一个类并使用 Base 接口调用 ,这会导致错误。

      干杯!

      【讨论】:

        【解决方案4】:

        除了史蒂夫的回答:

        • B 公开地衍生自 A。这意味着 Liskov 可替代性
        • 将 f 重写为私有似乎违反了该原则,但实际上并不一定 - 您仍然可以将 B 用作 A 而不会妨碍代码,因此如果 f 的私有实现仍然适用于 B,没问题
        • 您可能想要使用此模式,B 应该是 Liskov 可替代 A,但 B 也是另一个与 A 不真正相关(以 Liskov 可替代方式)的层次结构的根,其中 f 不再是公共接口。换句话说,从 B 派生的类 C,通过指向 B 的指针使用,会隐藏 f。
        • 但是,这实际上不太可能,从 A 中导出 B 可能是一个更好的主意

        【讨论】:

          【解决方案5】:

          函数访问控制检查发生在 c++ 函数调用的后期。 高级别的顺序是名称查找、模板参数推导(如果有)、重载解析,然后是访问控制(公共/保护/私有)检查。

          但是在你的 sn-p 中,你使用了一个指向基类的指针,而基类中的函数 f() 确实是公共的,这是编译器在编译器时可以看到的,所以编译器肯定会让你的 sn-p通过。

          A *ptr = new B;
          ptr->f();
          

          但以上所有这些都是在编译时发生的,所以它们实际上是静态的。虽然通常由 vtable 和 vpointer 提供支持的虚函数调用是在运行时发生的动态内容,因此虚函数调用与访问控制正交(虚函数调用发生在访问控制之后),这就是对 f() 的调用实际上结束的原因 B:: f() 不管访问控制是私有的。

          但是如果你尝试使用

          B* ptr = new B;
          ptr->f()
          

          尽管有 vpointer 和 vtable 这不会通过,编译器不会允许它在编译时编译。

          但如果你尝试:

          B* ptr = new B;
          ((static_cast<A*>(ptr))->f();
          

          这样就可以了。

          【讨论】:

            【解决方案6】:

            与 Java 非常相似,在 C++ 中,您可以增加方法的可见性,但不能降低它。

            【讨论】:

            • 我不了解 Java,但 B::f 确实隐藏了 A::f 和 B::f is private
            • 在Java中,你不能在派生类中将继承的方法设为私有,它不会编译。
            猜你喜欢
            • 2012-08-08
            • 2019-01-08
            • 1970-01-01
            • 2011-04-07
            • 2014-02-15
            • 2015-03-04
            • 1970-01-01
            • 2016-10-06
            • 2016-07-03
            相关资源
            最近更新 更多