【问题标题】:Virtual functions and polymorphism虚函数和多态
【发布时间】:2010-03-13 22:27:04
【问题描述】:

假设我有这个:

class A
{
    public:
    virtual int hello(A a);
};

class B : public A
{
   public:
   int hello(B b){ bla bla };
};

所以,A 是一个抽象类。

1) 在 B 类中,我定义了一个方法,假设它会覆盖 A 类。但参数略有不同。我不确定这一点,这是正确的吗?也许是因为多态性,这没关系,但它相当混乱。 2) 如果我这样做:A a = new B;,然后是 a.hello(lol);如果 "lol" 它不是 B 类型,那么它会给出编译错误?如果它是另一个 C 类(C 类:公共 A)的 A 类型,会发生什么?

我对覆盖和虚拟的东西感到困惑。我发现的所有示例都适用于没有参数的方法。

任何答案、链接或任何值得赞赏的内容。

谢谢

pd:对不起我的英语

【问题讨论】:

  • 请注意,new 运算符返回一个指针,因此A a = new B; 的类型不正确。您想要A a = B();A *a = new B();,尽管它们的作用不同。一个很大的不同是前者将 B 切片 (en.wikipedia.org/wiki/Object_slicing)。如果 A 是抽象的,则只有后者是合法的。
  • 要拥有一个纯虚拟基类,至少需要这样定义一个函数: virtual int hello(A a) = 0;并且从该基类派生的所有类都必须覆盖此函数。不能实例化纯虚基类的对象。

标签: c++ polymorphism virtual


【解决方案1】:

您的 B 类不会覆盖 A 中的成员函数,它会重载它。或者无论如何都要尝试,稍后再看看关于隐藏的内容。

重写是指派生类从基类定义自己的虚拟成员函数版本。重载是当您定义具有相同名称的不同函数时。

当对具有基类类型的指针或引用进行虚拟调用时,它只会“考虑”派生类中的覆盖,而不是重载。这是必不可少的 - 对于调用者处理 B 的实例,就好像它做了 A 可以做的所有事情(这是动态多态性和虚函数的重点),它的 hello 函数需要能够接受任何对象A 类型。hello 函数只接受 B 类型的对象,而不是任何 A,限制性更强。不能起到A的hello函数的作用,所以不是override。

如果您尝试在 A 和 B 上调用 hello 并传递 A 或 B 类型的对象,您应该能够看到差异。 A 有一个接受 A 的函数(你没有定义它,所以如果你调用它,那么你的程序将无法链接,但你可以修复它)。 B 有一个采用 B 的函数。它们恰好具有相同的名称,当然,由于 B 派生自 A,因此您可以将 B 传递给采用 A 的函数。但是 B 的函数在虚拟调用中不充当覆盖.

可以在 B 对象上调用 A 的函数,但只能通过指向 A 的引用或指针。C++ 的一个特点是 B 中 hello 的定义隐藏了 A 中的定义。如果重载是你的想要,可以通过将using A::hello; 添加到 B 类来取消隐藏基类函数。如果您想要覆盖,则必须定义一个采用相同参数的函数。例如:

#include <iostream>

class A
{
    public:
    virtual int hello(A a) {std::cout << "A\n"; }
    virtual int foo(int i) { std::cout << "A::Foo " << i << "\n"; }
};

class B : public A
{
   public:
   using A::hello;
   // here's an overload
   int hello(B b){ std::cout << "B\n"; };
   // here's an override:
   virtual int foo(int i) { std::cout << "B::Foo " << i << "\n"; }
};

int main() {
    A a;
    B b;
    a.hello(a);  // calls the function exactly as defined in A
    a.hello(b);  // B "is an" A, so this is allowed and slices the parameter
    b.hello(a);  // OK, but only because of `using`
    b.hello(b);  // calls the function exactly as defined in B
    A &ab = b;   // a reference to a B object, but as an A
    ab.hello(a); // calls the function in A
    ab.hello(b); // *also* calls the function in A, proving B has not overridden it
    a.foo(1);    // calls the function in A
    b.foo(2);    // calls the function in B
    ab.foo(3);   // calls the function in B, because it is overridden
}

输出:

A
A
A
B
A
A
A::Foo 1
B::Foo 2
B::Foo 3

如果从B中去掉using A::hello;这一行,那么调用b.hello(a);编译失败:

error: no matching function for call to `B::hello(A&)'
note: candidates are: int B::hello(B)

【讨论】:

    【解决方案2】:

    一堆很好的答案告诉你发生了什么,我想我会加入为什么。

    有一个叫做Liskov Substitution Principle的东西,它表示子类中的函数必须在与基类相同的preconditionspostconditions下工作。在这种情况下,该函数必须能够对 A 类型的任何对象进行操作。请注意,由于继承关系,每个 B 都是-a A,但不是每个 A 都是-a B。所以要替换基本方法,派生类中的新函数可以削弱前置条件或加强后置条件,但不能加强前置条件或削弱后置条件。

    您的覆盖尝试加强了前提条件,它接受 Bs,而不是所有 As。

    请注意,covariance 在返回类型上是允许的。如果你的基类返回 A,那么它保证返回值是 -a A。然后基类可以返回 B,因为每个 B 都是 -a A。

    但是对于输入参数,只有逆变满足LSP的理论要求,输入/输出参数是不变量。特别是在 C++ 中,所有参数类型在重载时都是不变的。

    【讨论】:

      【解决方案3】:

      首先,A 不是代码中的抽象类。它必须至少有一个纯虚函数才能成为抽象的。

      1. 不同的参数意味着完全不同的方法,即使名称相同。把它想象成一个不同的名字。这就是为什么它被称为“签名”。如果 A 是一个抽象类,则这段代码根本无法编译。

      2. A::hello() 将被调用。没问题,而且参数必须是A类型,好像没有继承一样。

      【讨论】:

      • 我从未在 A 类中定义 hello,因此函数是纯虚拟的,因此是抽象类。 (请注意,我在 B 中声明并定义了 hello)。
      • @ritmbo:不,这是不正确的。要声明一个纯虚函数,你必须写int hello(A a) = 0;。您所做的是声明一个函数,但从未定义它。这根本不是一回事。
      【解决方案4】:

      当您覆盖一个方法时,它会重新定义该方法的作用。您只能覆盖已定义的虚拟成员(使用它们的参数集)。如果类型是 A,则调用 A 上的方法。如果类型是 B,即使变量类型是 A 但包含类型 B 的实例,也会调用 B 上的方法。

      您不能更改被覆盖方法的参数定义,否则它将不再是覆盖。

      【讨论】:

        【解决方案5】:

        你正在做的是 重载 没有覆盖,即好像 B 类是:

        class B
        {
        public:
          int hello(A a) {...}
          int hello(B b) {...}
        };
        

        您有两个同名的函数,但具有不同的签名,这使它们成为不同的函数(就像标准库对 abs(float)abs(double) 等具有不同的函数一样)

        如果你想重写,那么你需要有相同的签名,即B类的hello需要带一个A类型的参数。这样,​​当你在B类的对象上调用hello时,它会使用B 类的 hello 而不是 A 类的。

        如果您真的希望 B 类的 hello 只接受 B 类型的对象,那么您所拥有的就可以了,尽管您可能希望将 A 类的 hello 设为非虚拟,因为您并不是真的想要覆盖它——您正在定义具有新参数和新行为的新函数。

        【讨论】:

        • 如果A 派生自B,那么对hello 的调用在编译器眼中岂不是模棱两可?
        • @Zach,不,它使用最衍生的那个。请注意,我假设按值传递错误已修复(无法按值传递抽象类)。
        • 它特别没有重载,因为两个函数声明都在不同的范围内。 B::hello 隐藏 A 中的你好。在您的示例中,它只是重载。
        【解决方案6】:

        谢谢你的答案,但我必须澄清一些事情才能得到我的最终答案。

        假设我的 A 类与我在原始问题中的定义完全一致。我添加了另一种方法:

        class A {
            ...
            int yeah();
        }
        

        然后我将B类定义如下:

        class B : public A {
            int hello(A a);
        };
        

        还有另一个类似于 B 的 C 类。

        我知道,因为我是程序员,B 和 C 的 hello 方法显然会有 A 类型的对象作为参数,但是同一个类的实例。 例如:

        B b; 
        b.hello(some_other_b_instance);
        

        C c; 
        c.hello(some_other_c_instance);
        

        问题是,在 B 类和 C 类的每个 hello 函数中,我想用特定 B 类或 C 类的属性做特定的事情。由于参数是 A 类型,我不能使用它们。

        我需要的是一种逆多态,但它是错误的,因为根据定义,我可以将 C 实例发送到 B hello 类,但我知道这不会发生。

        我希望你能理解代码的概念... A 类是抽象的,而真正的工作在特定的 B 类和 C 类中是有意义的,每个人都以他们特定的方式完成工作以使yeah 函数工作。但是 B 和 C 需要访问他们的成员才能正常工作。

        【讨论】:

        • 对问题的澄清应该进入问题,而不是作为答案发布。 SO 是一个问答网站,而不是论坛。
        • 如果将B::hello声明为int hello(A a)并传递B的实例,该实例将进行切片并成为A(任何特定于B的都将被丢弃); dynamic_cast 无济于事。通过 const 引用传递(或使用指针,但首选 const 引用)以防止切片:int B::hello(const A&amp; a).
        • 请注意,动态转换是危险的:没有什么能阻止其他编码人员将 A 或 C 传递给 B::hello。打字的目的是防止这种事情发生。以这种方式使用dynamic_cast,您还不如使用无类型语言。密切关注 Ben Voigt 的回答。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-11-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-07-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多