C++ 中的多态性:调用重写的方法

Polymorphism in C++: Calling an overridden method

首先,我是 Java 编码员,想了解 C++ 中的多态性。我为了学习目的写了例子:

#include<iostream>

using namespace std;

class A
{
public:
    virtual void foo(){ std::cout << "foo" << std::endl; }
};

class B : public A
{
public:
    void foo(){ std::cout << "overriden foo" << std::endl; }
};

A c = B(); 

int main(){ c.foo(); } //prints foo, not overriden foo

我预计 overriden foo 会被打印出来,但事实并非如此。为什么?我们覆盖了 class B 中的方法 foo,我认为应该根据对象的运行时类型来决定应该调用哪个方法,在我的例子中是 B,但不是静态类型(A 在我的例子中)。

实例是there

当你这样做时:

A c = B(); 

您正在 B 值转换为A。你不想要那个。

您应该创建一个 B 对象并通过 A 指针或引用 访问它以获得多态行为:

B b;
A& c = b;

orlp 说的正是。你也应该学习使用指针(它们很有趣)

A* c = new B();
c->foo(); // overriden foo
delete c;

在 java 中,您有 值语义 ,类型如 intfloat,并且您有 reference语义 与其他一切。

C++ 情况并非如此:类型系统是统一的,您可以请求.[=34= 获得任何值或引用语义。 ]

使用您编写的代码

A c = B()

您已告诉编译器创建一个 B 类型的新值,然后 将其转换A 类型的值,存储c 中的 。在这种情况下,转换意味着从您创建的新 B 实例中取出 A 数据,并将其 复制 到新的 A 实例中存储在 c.

您可以这样做:

B b;
A &c = b;

这仍然会创建 b,但现在 c 是对 [=18 的 引用 =],这意味着 c 现在将引用您创建的 B 实例,而不是其 A 部分的副本。

现在,这仍然会创建 b 作为局部变量,一旦 b 超出范围,存储在 b 中的对象就会被销毁。如果你想要更持久的东西,你需要使用 pointers;例如像

shared_ptr<A> c = make_shared<B>();
c->foo();

你可以做更多'raw'比如

A *b = new B();

但这是一个'dumb'指针; shared_ptr 更聪明,当没有其他对象引用它时,您的对象将被销毁。如果你做后者,你必须在适当的时候自己进行破坏(把它搞砸是错误的常见来源)

感兴趣的行是这个(改为使用统一初始化语法):

A c = B{};

重要的是要注意,当以这种方式声明时,c 的行为就像一个值。

您的代码从 B 的实例中构造了一个名为 c 的本地 A。这称为切片:B 中不属于 A 的任何部分都已被 "sliced" 移除,只留下 A.

在 C++ 中提供符号引用语义(称为间接)需要不同的表示法。

例如:

A &c = B{};
A d = B{}; // Casts the intermediate B instance to const A &,
           // then copy-constructs an A

c.foo(); // calls B::foo because c points to a B through an A interface

d.foo(); // calls A::foo because d is only an instance of an A

注意c指向的中间体B的生命周期自动延长到c的范围。另一方面,第二个中间体 Bd 的构造完成后被销毁。

在 C++ 中,引用是不可变的(它们在初始化后不能更改)。在表达式中使用时,就好像使用了它们指向的对象(值):

A &c = B{};

c = A{}; // Calls the compiler-provided A::operator = (const A &)
         // (a virtual assignment operator with this signature
         // was not defined).
         // This DOES NOT change where c points.

另一方面,指针是可以改变的:

A a{};
B b{};

A *cptr = &b;

cptr->foo(); // calls B::foo

cptr = &a;

cptr->foo(); // calls A::foo

您的困惑源于 Java 和 C++ 之间的关键区别。

在Java如果你写

MyClass var = whatever;

您的变量 var 是对 whatever 返回对象的 引用 。但是,在 C++ 中,此语法表示“通过将表达式 whatever 的结果传递给适当的构造函数来创建 类型 MyClass 的新对象,并复制结果对象放入变量 var.

特别是,您的代码创建了一个类型为 A 的新对象,名为 c,并将类型为 B 的临时默认构造对象传递给它的复制构造函数(因为那是唯一适合的构造函数)。由于新建的对象是A类型,不是B类型,显然调用了A的方法foo

如果你想引用一个对象,你必须在 C++ 中通过向类型添加 & 来明确请求。但是,对非常量对象的引用不能绑定到临时对象。因此,您还需要显式声明绑定到的对象(或者,使用对 const 对象的引用,并将 foo 成员函数固定为 const,因为它们不会更改对象反正)。因此,执行您想要的代码的最简单版本将是:

// your original definitions of A and B assumed here

B b; // The object of type B
A& c = b; // c is now a *reference* to b
int main() { c.foo(); } // calls B::foo() thanks to polymorphism

但是更好的版本应该是 const-correct,然后可以使用您原来的构造:

#include <iostream>

class A
{
public:
    virtual void foo() const  // note the additional const here!
    { std::cout << "foo" << std::endl; }
};

class B : public A
{
public:
    void foo() const // and also const here
    { std::cout << "overridden foo" << std::endl; }
};

A const& c = B(); // Since we bind to a const reference,
                  // the lifetime of the temporary is extended to the
                  // lifetime of the reference

int main() { c.foo(); } //prints overridden foo

(请注意,我删除了 using namespace std;,因为这是一件坏事(而且您的代码无论如何都使用显式 std::,所以它只是多余的)。

但是请注意,C++ 引用仍然不同于 Java 引用,因为它们无法重新分配;任何赋值都会转到底层对象。例如:

#include <iostream>

class A { public: virtual void foo() const { std::cout << "I'm an A\n"; } };
class B: public A { public: void foo() const { std::cout << "I'm a B\n"; } };
class C: public A { public: void foo() const { std::cout << "I'm a C\n"; } };

B b;
C c;

int main()
{
   A& ref = b; // bind reference ref to object b
   ref.foo(); // outputs "I'm a B"
   ref = c; // does *not* re-bind the reference to c, but calls A::operator= (which in this case is a no-op)
   ref.foo(); // again outputs "I'm a B"
}

如果要更改引用的对象,则必须使用指针:

// definitions of A, B and C as above
int main()
{
  A* prt = &b; // pointer ptr points to b
  prt->foo();  // outputs "I'm a B"
  prt = &c;    // reassign ptr to point to c
  prt->foo();  // outputs "I'm a C"
}